Compare commits
85 Commits
1735ba25a8
...
1fd7d76412
Author | SHA1 | Date |
---|---|---|
ynerant | 1fd7d76412 | |
Yohann D'ANELLO | 51fbde23b9 | |
Yohann D'ANELLO | 36f1a3f0d4 | |
Yohann D'ANELLO | cc04fa5555 | |
ynerant | f38b9801d0 | |
Yohann D'ANELLO | 43a22cbed4 | |
Yohann D'ANELLO | 5c2df41640 | |
ynerant | e50bff8e14 | |
Yohann D'ANELLO | b5586c647b | |
Yohann D'ANELLO | 377397b319 | |
Yohann D'ANELLO | 1abb40953f | |
Yohann D'ANELLO | f114265662 | |
Yohann D'ANELLO | 7c369bd264 | |
Yohann D'ANELLO | b6453ce03d | |
Yohann D'ANELLO | 59bfdbbfc7 | |
Yohann D'ANELLO | 82aa0182e3 | |
Yohann D'ANELLO | 23b775447c | |
Yohann D'ANELLO | d7b834d908 | |
Yohann D'ANELLO | dca655949e | |
Yohann D'ANELLO | 932a546213 | |
Yohann D'ANELLO | d8127e8936 | |
Yohann D'ANELLO | 8409ee4cc4 | |
Yohann D'ANELLO | 9008baad3a | |
Yohann D'ANELLO | fd705adb05 | |
Yohann D'ANELLO | bd35e4e21e | |
Yohann D'ANELLO | 72dcc93136 | |
Yohann D'ANELLO | cb38ceb2c6 | |
Yohann D'ANELLO | ad19b64b3a | |
Yohann D'ANELLO | 2642ae3a1a | |
Yohann D'ANELLO | ad2cc22964 | |
Yohann D'ANELLO | ae629b55ad | |
Yohann D'ANELLO | a5e50e5de6 | |
Yohann D'ANELLO | 9da8d49223 | |
Yohann D'ANELLO | aa66361ac7 | |
Yohann D'ANELLO | c14d37eaeb | |
Yohann D'ANELLO | e9cbc8e623 | |
Yohann D'ANELLO | 9d8c588b78 | |
Yohann D'ANELLO | 484560fe4b | |
Yohann D'ANELLO | 9361f3f2f0 | |
Yohann D'ANELLO | e63219f7ad | |
Yohann D'ANELLO | 0c0aed0234 | |
Yohann D'ANELLO | fb775de923 | |
Yohann D'ANELLO | b49db39080 | |
Yohann D'ANELLO | da1063862e | |
Yohann D'ANELLO | 224ef5b2f0 | |
Yohann D'ANELLO | cbd36f110a | |
Yohann D'ANELLO | c9e68ca66b | |
Yohann D'ANELLO | 20011db37e | |
Yohann D'ANELLO | 5a91cac08d | |
Yohann D'ANELLO | fa9159bb28 | |
Yohann D'ANELLO | 4549255198 | |
Yohann D'ANELLO | 750bdcb2c5 | |
Yohann D'ANELLO | 5c93301358 | |
Yohann D'ANELLO | b8a88eeda4 | |
Alexandre Iooss | d455c5c533 | |
Yohann D'ANELLO | f597b6dbd8 | |
Yohann D'ANELLO | 3a4145e4d9 | |
Yohann D'ANELLO | 54ce157019 | |
Yohann D'ANELLO | 7c6bab88f4 | |
Yohann D'ANELLO | 12ebf9d12a | |
Yohann D'ANELLO | 76a6260b18 | |
Yohann D'ANELLO | 7b3512c0be | |
Yohann D'ANELLO | 0bfc3b9454 | |
Yohann D'ANELLO | 84e8b02594 | |
Yohann D'ANELLO | 09027ea35e | |
Yohann D'ANELLO | 2f334e0707 | |
Yohann D'ANELLO | e163f86f69 | |
Yohann D'ANELLO | 0335a47667 | |
Yohann D'ANELLO | b8d4fb9df1 | |
Yohann D'ANELLO | 8cde94c5f5 | |
Yohann D'ANELLO | ab5d5a6e94 | |
Yohann D'ANELLO | 4c29d855d2 | |
Yohann D'ANELLO | 55bc288deb | |
Yohann D'ANELLO | fb5e2578af | |
Yohann D'ANELLO | 0b6cb4ef19 | |
Yohann D'ANELLO | 18bdc8044b | |
Yohann D'ANELLO | f0bca69825 | |
Yohann D'ANELLO | b2e1777fe0 | |
Yohann D'ANELLO | afb35d7ae0 | |
Yohann D'ANELLO | b212bf4093 | |
Yohann D'ANELLO | 3fea17c555 | |
Yohann D'ANELLO | 2eb601bd66 | |
Yohann D'ANELLO | 50024dc03d | |
Yohann D'ANELLO | e3045522d1 | |
Yohann D'ANELLO | a098f70424 |
|
@ -36,8 +36,11 @@ coverage
|
||||||
|
|
||||||
# Local data
|
# Local data
|
||||||
secrets.py
|
secrets.py
|
||||||
|
.env
|
||||||
|
map.json
|
||||||
*.log
|
*.log
|
||||||
media/
|
media/
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
image: python:3.6
|
image: python:3.8
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
@ -17,8 +17,13 @@ py37-django22:
|
||||||
stage: test
|
stage: test
|
||||||
script: tox -e py37-django22
|
script: tox -e py37-django22
|
||||||
|
|
||||||
|
py38-django22:
|
||||||
|
image: python:3.8
|
||||||
|
stage: test
|
||||||
|
script: tox -e py38-django22
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
image: python:3.6
|
image: python:3.8
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- hosts: bde-nk20-beta.adh.crans.org
|
- hosts: bde-nk20-beta.adh.crans.org
|
||||||
|
vars_prompt:
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
prompt: "Password of the database"
|
||||||
|
private: yes
|
||||||
roles:
|
roles:
|
||||||
- 1-apt-basic
|
- 1-apt-basic
|
||||||
- 2-nk20
|
- 2-nk20
|
||||||
|
|
|
@ -11,11 +11,15 @@
|
||||||
git:
|
git:
|
||||||
repo: https://gitlab.crans.org/bde/nk20.git
|
repo: https://gitlab.crans.org/bde/nk20.git
|
||||||
dest: /var/www/note_kfet
|
dest: /var/www/note_kfet
|
||||||
version: beta-soon
|
version: master
|
||||||
force: true
|
force: true
|
||||||
|
|
||||||
- name: Use default env vars (should be updated!)
|
- name: Use default env vars (should be updated!)
|
||||||
command: cp /var/www/note_kfet/.env_example /var/www/note_kfet/.env
|
template:
|
||||||
|
src: "env_example"
|
||||||
|
dest: "/var/www/note_kfet/.env"
|
||||||
|
mode: 0644
|
||||||
|
force: false
|
||||||
|
|
||||||
- name: Update permissions for note_kfet dir
|
- name: Update permissions for note_kfet dir
|
||||||
file:
|
file:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../.env_example
|
|
@ -15,6 +15,11 @@
|
||||||
group: www-data
|
group: www-data
|
||||||
state: link
|
state: link
|
||||||
|
|
||||||
|
- name: Disable default Nginx site
|
||||||
|
file:
|
||||||
|
dest: /etc/nginx/sites-enabled/default
|
||||||
|
state: absent
|
||||||
|
|
||||||
- name: Copy conf of UWSGI
|
- name: Copy conf of UWSGI
|
||||||
file:
|
file:
|
||||||
src: /var/www/note_kfet/uwsgi_note.ini
|
src: /var/www/note_kfet/uwsgi_note.ini
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
# the upstream component nginx needs to connect to
|
||||||
|
upstream note{
|
||||||
|
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP to nk20 HTTPS
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://nk20-beta.crans.org$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect all HTTPS to nk20 HTTPS
|
||||||
|
server {
|
||||||
|
listen 443 ssl default_server;
|
||||||
|
listen [::]:443 ssl default_server;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://nk20-beta.crans.org$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
# configuration of the server
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
# the port your site will be served on
|
||||||
|
# the domain name it will serve for
|
||||||
|
server_name nk20-beta.crans.org; # substitute your machine's IP address or FQDN
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
# max upload size
|
||||||
|
client_max_body_size 75M; # adjust to taste
|
||||||
|
|
||||||
|
# Django media
|
||||||
|
location /media {
|
||||||
|
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static {
|
||||||
|
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
||||||
|
}
|
||||||
|
|
||||||
|
# Finally, send all non-media requests to the Django server.
|
||||||
|
location / {
|
||||||
|
uwsgi_pass note;
|
||||||
|
include /var/www/note_kfet/uwsgi_params; # the uwsgi_params file you installed
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
- name: Create role note
|
- name: Create role note
|
||||||
postgresql_user:
|
postgresql_user:
|
||||||
name: note
|
name: note
|
||||||
password: "CHANGE_ME"
|
password: "{{ DB_PASSWORD }}"
|
||||||
become_user: postgres
|
become_user: postgres
|
||||||
|
|
||||||
- name: Create NK20 database
|
- name: Create NK20 database
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py migrate
|
command: /var/www/note_kfet/env/bin/python manage.py migrate
|
||||||
args:
|
args:
|
||||||
chdir: /var/www/note_kfet
|
chdir: /var/www/note_kfet
|
||||||
become_user: www-data
|
become_user: postgres
|
||||||
|
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py compilemessages
|
command: /var/www/note_kfet/env/bin/python manage.py compilemessages
|
||||||
|
@ -21,4 +21,4 @@
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
|
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
|
||||||
args:
|
args:
|
||||||
chdir: /var/www/note_kfet
|
chdir: /var/www/note_kfet
|
||||||
become_user: www-data
|
become_user: postgres
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Activity, ActivityType, Guest
|
from note_kfet.admin import admin_site
|
||||||
|
from .models import Activity, ActivityType, Guest, Entry
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Activity, site=admin_site)
|
||||||
class ActivityAdmin(admin.ModelAdmin):
|
class ActivityAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for Activity
|
Admin customisation for Activity
|
||||||
|
@ -19,6 +21,7 @@ class ActivityAdmin(admin.ModelAdmin):
|
||||||
ordering = ['-date_start']
|
ordering = ['-date_start']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ActivityType, site=admin_site)
|
||||||
class ActivityTypeAdmin(admin.ModelAdmin):
|
class ActivityTypeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for ActivityType
|
Admin customisation for ActivityType
|
||||||
|
@ -26,7 +29,17 @@ class ActivityTypeAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'can_invite', 'guest_entry_fee')
|
list_display = ('name', 'can_invite', 'guest_entry_fee')
|
||||||
|
|
||||||
|
|
||||||
# Register your models here.
|
@admin.register(Guest, site=admin_site)
|
||||||
admin.site.register(Activity, ActivityAdmin)
|
class GuestAdmin(admin.ModelAdmin):
|
||||||
admin.site.register(ActivityType, ActivityTypeAdmin)
|
"""
|
||||||
admin.site.register(Guest)
|
Admin customisation for Guest
|
||||||
|
"""
|
||||||
|
list_display = ('last_name', 'first_name', 'activity', 'inviter')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Entry, site=admin_site)
|
||||||
|
class EntryAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin customisation for Entry
|
||||||
|
"""
|
||||||
|
list_display = ('note', 'activity', 'time', 'guest')
|
||||||
|
|
|
@ -5,6 +5,7 @@ from datetime import timedelta, datetime
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from note.models import NoteUser, Transaction
|
from note.models import NoteUser, Transaction
|
||||||
|
@ -98,6 +99,9 @@ class Activity(models.Model):
|
||||||
verbose_name=_('open'),
|
verbose_name=_('open'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("activity")
|
verbose_name = _("activity")
|
||||||
verbose_name_plural = _("activities")
|
verbose_name_plural = _("activities")
|
||||||
|
@ -118,7 +122,7 @@ class Entry(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
time = models.DateTimeField(
|
time = models.DateTimeField(
|
||||||
auto_now_add=True,
|
default=timezone.now,
|
||||||
verbose_name=_("entry time"),
|
verbose_name=_("entry time"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -163,7 +167,7 @@ class Entry(models.Model):
|
||||||
amount=self.activity.activity_type.guest_entry_fee,
|
amount=self.activity.activity_type.guest_entry_fee,
|
||||||
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
|
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
|
||||||
valid=True,
|
valid=True,
|
||||||
guest=self.guest,
|
entry=self,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -233,6 +237,9 @@ class Guest(models.Model):
|
||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.first_name + " " + self.last_name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("guest")
|
verbose_name = _("guest")
|
||||||
verbose_name_plural = _("guests")
|
verbose_name_plural = _("guests")
|
||||||
|
@ -240,8 +247,8 @@ class Guest(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class GuestTransaction(Transaction):
|
class GuestTransaction(Transaction):
|
||||||
guest = models.OneToOneField(
|
entry = models.OneToOneField(
|
||||||
Guest,
|
Entry,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ from .tables import ActivityTable, GuestTable, EntryTable
|
||||||
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
extra_context = {"title": _("Create new activity")}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.creater = self.request.user
|
form.instance.creater = self.request.user
|
||||||
|
@ -37,12 +38,14 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
table_class = ActivityTable
|
||||||
ordering = ('-date_start',)
|
ordering = ('-date_start',)
|
||||||
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().distinct()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
context['title'] = _("Activities")
|
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
|
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
|
||||||
context['upcoming'] = ActivityTable(
|
context['upcoming'] = ActivityTable(
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
||||||
|
@ -55,6 +58,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = Activity
|
model = Activity
|
||||||
context_object_name = "activity"
|
context_object_name = "activity"
|
||||||
|
extra_context = {"title": _("Activity detail")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
@ -71,6 +75,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
extra_context = {"title": _("Update activity")}
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||||
|
@ -81,6 +86,12 @@ class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
form_class = GuestForm
|
form_class = GuestForm
|
||||||
template_name = "activity/activity_invite.html"
|
template_name = "activity/activity_invite.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
activity = context["form"].activity
|
||||||
|
context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
|
||||||
|
return context
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
||||||
|
|
|
@ -14,9 +14,11 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
@ -26,6 +28,8 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +69,7 @@ class Changelog(models.Model):
|
||||||
timestamp = models.DateTimeField(
|
timestamp = models.DateTimeField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
auto_now_add=True,
|
default=timezone.now,
|
||||||
name='timestamp',
|
name='timestamp',
|
||||||
verbose_name=_('timestamp'),
|
verbose_name=_('timestamp'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,9 +4,12 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note.templatetags.pretty_money import pretty_money
|
||||||
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .forms import ProfileForm
|
from .forms import ProfileForm
|
||||||
from .models import Club, Membership, Profile, Role
|
from .models import Club, Membership, Profile
|
||||||
|
|
||||||
|
|
||||||
class ProfileInline(admin.StackedInline):
|
class ProfileInline(admin.StackedInline):
|
||||||
|
@ -17,6 +20,7 @@ class ProfileInline(admin.StackedInline):
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User, site=admin_site)
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
inlines = (ProfileInline,)
|
inlines = (ProfileInline,)
|
||||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||||
|
@ -32,11 +36,33 @@ class CustomUserAdmin(UserAdmin):
|
||||||
return super().get_inline_instances(request, obj)
|
return super().get_inline_instances(request, obj)
|
||||||
|
|
||||||
|
|
||||||
# Update Django User with profile
|
@admin.register(Club, site=admin_site)
|
||||||
admin.site.unregister(User)
|
class ClubAdmin(admin.ModelAdmin):
|
||||||
admin.site.register(User, CustomUserAdmin)
|
list_display = ('name', 'parent_club', 'email', 'require_memberships', 'pretty_fee_paid',
|
||||||
|
'pretty_fee_unpaid', 'membership_start', 'membership_end',)
|
||||||
|
ordering = ('name',)
|
||||||
|
search_fields = ('name', 'email',)
|
||||||
|
|
||||||
# Add other models
|
def pretty_fee_paid(self, obj):
|
||||||
admin.site.register(Club)
|
return pretty_money(obj.membership_fee_paid)
|
||||||
admin.site.register(Membership)
|
|
||||||
admin.site.register(Role)
|
def pretty_fee_unpaid(self, obj):
|
||||||
|
return pretty_money(obj.membership_fee_unpaid)
|
||||||
|
|
||||||
|
pretty_fee_paid.short_description = _("membership fee (paid students)")
|
||||||
|
pretty_fee_unpaid.short_description = _("membership fee (unpaid students)")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Membership, site=admin_site)
|
||||||
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'club', 'date_start', 'date_end', 'view_roles', 'pretty_fee',)
|
||||||
|
ordering = ('-date_start', 'club')
|
||||||
|
|
||||||
|
def view_roles(self, obj):
|
||||||
|
return ", ".join(role.name for role in obj.roles.all())
|
||||||
|
|
||||||
|
def pretty_fee(self, obj):
|
||||||
|
return pretty_money(obj.fee)
|
||||||
|
|
||||||
|
view_roles.short_description = _("roles")
|
||||||
|
pretty_fee.short_description = _("fee")
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models import Profile, Club, Role, Membership
|
from ..models import Profile, Club, Membership
|
||||||
|
|
||||||
|
|
||||||
class ProfileSerializer(serializers.ModelSerializer):
|
class ProfileSerializer(serializers.ModelSerializer):
|
||||||
|
@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class RoleSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
REST API Serializer for Roles.
|
|
||||||
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Role
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipSerializer(serializers.ModelSerializer):
|
class MembershipSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for Memberships.
|
REST API Serializer for Memberships.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
|
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_members_urls(router, path):
|
def register_members_urls(router, path):
|
||||||
|
@ -10,5 +10,4 @@ def register_members_urls(router, path):
|
||||||
"""
|
"""
|
||||||
router.register(path + '/profile', ProfileViewSet)
|
router.register(path + '/profile', ProfileViewSet)
|
||||||
router.register(path + '/club', ClubViewSet)
|
router.register(path + '/club', ClubViewSet)
|
||||||
router.register(path + '/role', RoleViewSet)
|
|
||||||
router.register(path + '/membership', MembershipViewSet)
|
router.register(path + '/membership', MembershipViewSet)
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.filters import SearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
|
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||||
from ..models import Profile, Club, Role, Membership
|
from ..models import Profile, Club, Membership
|
||||||
|
|
||||||
|
|
||||||
class ProfileViewSet(ReadProtectedModelViewSet):
|
class ProfileViewSet(ReadProtectedModelViewSet):
|
||||||
|
@ -30,18 +30,6 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$name', ]
|
||||||
|
|
||||||
|
|
||||||
class RoleViewSet(ReadProtectedModelViewSet):
|
|
||||||
"""
|
|
||||||
REST API View set.
|
|
||||||
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
|
|
||||||
then render it on /api/members/role/
|
|
||||||
"""
|
|
||||||
queryset = Role.objects.all()
|
|
||||||
serializer_class = RoleSerializer
|
|
||||||
filter_backends = [SearchFilter]
|
|
||||||
search_fields = ['$name', ]
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
|
|
|
@ -5,11 +5,11 @@ from django import forms
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteSpecial
|
from note.models import NoteSpecial, Alias
|
||||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||||
from permission.models import PermissionMask
|
from permission.models import PermissionMask, Role
|
||||||
|
|
||||||
from .models import Profile, Club, Membership, Role
|
from .models import Profile, Club, Membership
|
||||||
|
|
||||||
|
|
||||||
class CustomAuthenticationForm(AuthenticationForm):
|
class CustomAuthenticationForm(AuthenticationForm):
|
||||||
|
@ -20,6 +20,18 @@ class CustomAuthenticationForm(AuthenticationForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(forms.ModelForm):
|
||||||
|
def _get_validation_exclusions(self):
|
||||||
|
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
||||||
|
# We want to allow users to have uncommon and unpractical usernames:
|
||||||
|
# That is their problem, and we have normalized aliases for us.
|
||||||
|
return super()._get_validation_exclusions() + ["username"]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('first_name', 'last_name', 'username', 'email',)
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(forms.ModelForm):
|
class ProfileForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
A form for the extras field provided by the :model:`member.Profile` model.
|
A form for the extras field provided by the :model:`member.Profile` model.
|
||||||
|
@ -38,6 +50,15 @@ class ProfileForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class ClubForm(forms.ModelForm):
|
class ClubForm(forms.ModelForm):
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
if not self.instance.pk: # Creating a club
|
||||||
|
if Alias.objects.filter(normalized_name=Alias.normalize(self.cleaned_data["name"])).exists():
|
||||||
|
self.add_error('name', _("An alias with a similar name already exists."))
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
@ -56,8 +77,6 @@ class ClubForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class MembershipForm(forms.ModelForm):
|
class MembershipForm(forms.ModelForm):
|
||||||
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
|
|
||||||
|
|
||||||
soge = forms.BooleanField(
|
soge = forms.BooleanField(
|
||||||
label=_("Inscription paid by Société Générale"),
|
label=_("Inscription paid by Société Générale"),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -96,7 +115,7 @@ class MembershipForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Membership
|
model = Membership
|
||||||
fields = ('user', 'roles', 'date_start')
|
fields = ('user', 'date_start')
|
||||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||||
# et récupère les noms d'utilisateur valides
|
# et récupère les noms d'utilisateur valides
|
||||||
|
@ -112,3 +131,28 @@ class MembershipForm(forms.ModelForm):
|
||||||
),
|
),
|
||||||
'date_start': DatePickerInput(),
|
'date_start': DatePickerInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipRolesForm(forms.ModelForm):
|
||||||
|
user = forms.ModelChoiceField(
|
||||||
|
queryset=User.objects,
|
||||||
|
label=_("User"),
|
||||||
|
disabled=True,
|
||||||
|
widget=Autocomplete(
|
||||||
|
User,
|
||||||
|
attrs={
|
||||||
|
'api_url': '/api/user/',
|
||||||
|
'name_field': 'username',
|
||||||
|
'placeholder': 'Nom ...',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
roles = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Role.objects.filter(weirole=None).all(),
|
||||||
|
label=_("Roles"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Membership
|
||||||
|
fields = ('user', 'roles')
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
|
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
||||||
|
|
||||||
|
|
||||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||||
|
@ -20,8 +22,37 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||||
"""
|
"""
|
||||||
algorithm = "custom_nk15"
|
algorithm = "custom_nk15"
|
||||||
|
|
||||||
|
def must_update(self, encoded):
|
||||||
|
if settings.DEBUG:
|
||||||
|
current_user = get_current_authenticated_user()
|
||||||
|
if current_user is not None and current_user.is_superuser:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
|
if settings.DEBUG:
|
||||||
|
current_user = get_current_authenticated_user()
|
||||||
|
if current_user is not None and current_user.is_superuser\
|
||||||
|
and get_current_session().get("permission_mask", -1) >= 42:
|
||||||
|
return True
|
||||||
|
|
||||||
if '|' in encoded:
|
if '|' in encoded:
|
||||||
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||||
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
||||||
return super().verify(password, encoded)
|
return super().verify(password, encoded)
|
||||||
|
|
||||||
|
|
||||||
|
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||||
|
"""
|
||||||
|
In debug mode and during the beta, superusers can login into other accounts for tests.
|
||||||
|
"""
|
||||||
|
def must_update(self, encoded):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def verify(self, password, encoded):
|
||||||
|
if settings.DEBUG:
|
||||||
|
current_user = get_current_authenticated_user()
|
||||||
|
if current_user is not None and current_user.is_superuser\
|
||||||
|
and get_current_session().get("permission_mask", -1) >= 42:
|
||||||
|
return True
|
||||||
|
return super().verify(password, encoded)
|
||||||
|
|
|
@ -131,7 +131,7 @@ class Profile(models.Model):
|
||||||
return reverse('user_detail', args=(self.pk,))
|
return reverse('user_detail', args=(self.pk,))
|
||||||
|
|
||||||
def send_email_validation_link(self):
|
def send_email_validation_link(self):
|
||||||
subject = "Activate your Note Kfet account"
|
subject = _("Activate your Note Kfet account")
|
||||||
message = loader.render_to_string('registration/mails/email_validation_email.html',
|
message = loader.render_to_string('registration/mails/email_validation_email.html',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
|
@ -247,24 +247,6 @@ class Club(models.Model):
|
||||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
|
||||||
class Role(models.Model):
|
|
||||||
"""
|
|
||||||
Role that an :model:`auth.User` can have in a :model:`member.Club`
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
verbose_name=_('name'),
|
|
||||||
max_length=255,
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('role')
|
|
||||||
verbose_name_plural = _('roles')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""
|
||||||
Register the membership of a user to a club, including roles and membership duration.
|
Register the membership of a user to a club, including roles and membership duration.
|
||||||
|
@ -284,7 +266,7 @@ class Membership(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
roles = models.ManyToManyField(
|
roles = models.ManyToManyField(
|
||||||
Role,
|
"permission.Role",
|
||||||
verbose_name=_("roles"),
|
verbose_name=_("roles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -302,6 +284,7 @@ class Membership(models.Model):
|
||||||
verbose_name=_('fee'),
|
verbose_name=_('fee'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
def valid(self):
|
def valid(self):
|
||||||
"""
|
"""
|
||||||
A membership is valid if today is between the start and the end date.
|
A membership is valid if today is between the start and the end date.
|
||||||
|
@ -319,6 +302,14 @@ class Membership(models.Model):
|
||||||
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
|
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
|
||||||
raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
|
raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
|
||||||
|
|
||||||
|
if self.pk:
|
||||||
|
for role in self.roles.all():
|
||||||
|
club = role.for_club
|
||||||
|
if club is not None:
|
||||||
|
if club.pk != self.club_id:
|
||||||
|
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||||
|
.format(role=role.name, club=club.name))
|
||||||
|
|
||||||
created = not self.pk
|
created = not self.pk
|
||||||
if created:
|
if created:
|
||||||
if Membership.objects.filter(
|
if Membership.objects.filter(
|
||||||
|
|
|
@ -131,3 +131,31 @@ class MembershipTable(tables.Table):
|
||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
|
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
|
||||||
model = Membership
|
model = Membership
|
||||||
|
|
||||||
|
|
||||||
|
class ClubManagerTable(tables.Table):
|
||||||
|
"""
|
||||||
|
List managers of a club.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render_user(self, value):
|
||||||
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
|
s = value.username
|
||||||
|
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
||||||
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
def render_roles(self, record):
|
||||||
|
roles = record.roles.all()
|
||||||
|
return ", ".join(str(role) for role in roles)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped table-hover',
|
||||||
|
'style': 'table-layout: fixed;'
|
||||||
|
}
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
fields = ('user', 'user.first_name', 'user.last_name', 'roles', )
|
||||||
|
model = Membership
|
||||||
|
|
|
@ -16,6 +16,7 @@ urlpatterns = [
|
||||||
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
|
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
|
||||||
path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
|
path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
|
||||||
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
|
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
|
||||||
|
path('club/<int:pk>/members/', views.ClubMembersListView.as_view(), name="club_members"),
|
||||||
|
|
||||||
path('user/', views.UserListView.as_view(), name="user_list"),
|
path('user/', views.UserListView.as_view(), name="user_list"),
|
||||||
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
|
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
|
||||||
|
|
|
@ -6,12 +6,14 @@ from datetime import datetime, timedelta
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import logout
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
@ -21,12 +23,14 @@ from note.forms import ImageForm
|
||||||
from note.models import Alias, NoteUser
|
from note.models import Alias, NoteUser
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable
|
from note.tables import HistoryTable, AliasTable
|
||||||
|
from note_kfet.middlewares import _set_current_user_and_ip
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
from permission.models import Role
|
||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
|
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm
|
||||||
from .models import Club, Membership, Role
|
from .models import Club, Membership
|
||||||
from .tables import ClubTable, UserTable, MembershipTable
|
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
|
||||||
|
|
||||||
|
|
||||||
class CustomLoginView(LoginView):
|
class CustomLoginView(LoginView):
|
||||||
|
@ -36,6 +40,8 @@ class CustomLoginView(LoginView):
|
||||||
form_class = CustomAuthenticationForm
|
form_class = CustomAuthenticationForm
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
logout(self.request)
|
||||||
|
_set_current_user_and_ip(form.get_user(), self.request.session, None)
|
||||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -45,9 +51,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
Update the user information.
|
Update the user information.
|
||||||
"""
|
"""
|
||||||
model = User
|
model = User
|
||||||
fields = ['first_name', 'last_name', 'username', 'email']
|
form_class = UserForm
|
||||||
template_name = 'member/profile_update.html'
|
template_name = 'member/profile_update.html'
|
||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
|
extra_context = {"title": _("Update Profile")}
|
||||||
|
|
||||||
profile_form = ProfileForm
|
profile_form = ProfileForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -62,7 +70,6 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
form.fields['email'].help_text = _("This address must be valid.")
|
form.fields['email'].help_text = _("This address must be valid.")
|
||||||
|
|
||||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
||||||
context['title'] = _("Update Profile")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -101,6 +108,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
if olduser.email != user.email:
|
if olduser.email != user.email:
|
||||||
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
|
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
|
||||||
user.profile.email_confirmed = False
|
user.profile.email_confirmed = False
|
||||||
|
user.profile.save()
|
||||||
user.profile.send_email_validation_link()
|
user.profile.send_email_validation_link()
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
@ -117,6 +125,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = User
|
model = User
|
||||||
context_object_name = "user_object"
|
context_object_name = "user_object"
|
||||||
template_name = "member/profile_detail.html"
|
template_name = "member/profile_detail.html"
|
||||||
|
extra_context = {"title": _("Profile detail")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -129,7 +138,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
user = context['user_object']
|
user = context['user_object']
|
||||||
history_list = \
|
history_list = \
|
||||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||||
.order_by("-created_at", "-id")\
|
.order_by("-created_at")\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
||||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||||
|
@ -150,12 +159,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
model = User
|
model = User
|
||||||
table_class = UserTable
|
table_class = UserTable
|
||||||
template_name = 'member/user_list.html'
|
template_name = 'member/user_list.html'
|
||||||
|
extra_context = {"title": _("Search user")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Filter the user list with the given pattern.
|
Filter the user list with the given pattern.
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset().filter(profile__registration_valid=True)
|
qs = super().get_queryset().distinct().filter(profile__registration_valid=True)
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
@ -175,13 +185,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
|
|
||||||
return qs[:20]
|
return qs[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context["title"] = _("Search user")
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
|
@ -190,6 +193,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = User
|
model = User
|
||||||
template_name = 'member/profile_alias.html'
|
template_name = 'member/profile_alias.html'
|
||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -203,6 +207,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||||
Update profile picture of the user note.
|
Update profile picture of the user note.
|
||||||
"""
|
"""
|
||||||
form_class = ImageForm
|
form_class = ImageForm
|
||||||
|
extra_context = {"title": _("Update note picture")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -260,6 +265,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
model = Token
|
model = Token
|
||||||
template_name = "member/manage_auth_tokens.html"
|
template_name = "member/manage_auth_tokens.html"
|
||||||
|
extra_context = {"title": _("Manage auth token")}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||||
|
@ -287,6 +293,7 @@ class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Club
|
model = Club
|
||||||
form_class = ClubForm
|
form_class = ClubForm
|
||||||
success_url = reverse_lazy('member:club_list')
|
success_url = reverse_lazy('member:club_list')
|
||||||
|
extra_context = {"title": _("Create new club")}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
@ -298,12 +305,13 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
model = Club
|
model = Club
|
||||||
table_class = ClubTable
|
table_class = ClubTable
|
||||||
|
extra_context = {"title": _("Search club")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Filter the user list with the given pattern.
|
Filter the user list with the given pattern.
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset().filter()
|
qs = super().get_queryset().distinct()
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
@ -322,6 +330,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
model = Club
|
model = Club
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
|
extra_context = {"title": _("Club detail")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -330,9 +339,13 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
||||||
club.update_membership_dates()
|
club.update_membership_dates()
|
||||||
|
|
||||||
|
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\
|
||||||
|
.order_by('user__last_name').all()
|
||||||
|
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
||||||
|
|
||||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||||
.order_by('-created_at', '-id')
|
.order_by('-created_at')
|
||||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||||
context['history_list'] = history_table
|
context['history_list'] = history_table
|
||||||
|
@ -342,7 +355,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
|
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
|
||||||
|
|
||||||
membership_table = MembershipTable(data=club_member, prefix="membership-")
|
membership_table = MembershipTable(data=club_member, prefix="membership-")
|
||||||
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
|
||||||
context['member_list'] = membership_table
|
context['member_list'] = membership_table
|
||||||
|
|
||||||
# Check if the user has the right to create a membership, to display the button.
|
# Check if the user has the right to create a membership, to display the button.
|
||||||
|
@ -366,6 +379,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = Club
|
model = Club
|
||||||
template_name = 'member/club_alias.html'
|
template_name = 'member/club_alias.html'
|
||||||
context_object_name = 'club'
|
context_object_name = 'club'
|
||||||
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -382,6 +396,7 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
form_class = ClubForm
|
form_class = ClubForm
|
||||||
template_name = "member/club_form.html"
|
template_name = "member/club_form.html"
|
||||||
|
extra_context = {"title": _("Update club")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
|
@ -415,6 +430,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Membership
|
model = Membership
|
||||||
form_class = MembershipForm
|
form_class = MembershipForm
|
||||||
template_name = 'member/add_members.html'
|
template_name = 'member/add_members.html'
|
||||||
|
extra_context = {"title": _("Add new member to the club")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -425,7 +441,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||||
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
|
|
||||||
|
|
||||||
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
|
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
|
||||||
if club.name != "BDE":
|
if club.name != "BDE":
|
||||||
|
@ -444,7 +459,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
user = old_membership.user
|
user = old_membership.user
|
||||||
form.fields['user'].initial = user
|
form.fields['user'].initial = user
|
||||||
form.fields['user'].disabled = True
|
form.fields['user'].disabled = True
|
||||||
form.fields['roles'].initial = old_membership.roles.all()
|
|
||||||
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
|
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
|
||||||
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
|
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
|
||||||
else club.membership_fee_unpaid
|
else club.membership_fee_unpaid
|
||||||
|
@ -560,7 +574,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
form.add_error('bank', _("This field is required."))
|
form.add_error('bank', _("This field is required."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
SpecialTransaction.objects.create(
|
transaction = SpecialTransaction(
|
||||||
source=credit_type,
|
source=credit_type,
|
||||||
destination=user.note,
|
destination=user.note,
|
||||||
quantity=1,
|
quantity=1,
|
||||||
|
@ -571,9 +585,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
bank=bank,
|
bank=bank,
|
||||||
valid=True,
|
valid=True,
|
||||||
)
|
)
|
||||||
|
transaction._force_save = True
|
||||||
|
transaction.save()
|
||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
|
member_role = Role.objects.filter(name="Membre de club").all()
|
||||||
|
form.instance.roles.set(member_role)
|
||||||
|
form.instance._force_save = True
|
||||||
|
form.instance.save()
|
||||||
|
|
||||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||||
# Kfet membership.
|
# Kfet membership.
|
||||||
if soge:
|
if soge:
|
||||||
|
@ -595,6 +616,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
date_start=old_membership.get().date_end + timedelta(days=1)
|
date_start=old_membership.get().date_end + timedelta(days=1)
|
||||||
if old_membership.exists() else form.instance.date_start,
|
if old_membership.exists() else form.instance.date_start,
|
||||||
)
|
)
|
||||||
|
membership._force_save = True
|
||||||
membership._soge = True
|
membership._soge = True
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
|
@ -615,8 +637,9 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
Manage the roles of a user in a club
|
Manage the roles of a user in a club
|
||||||
"""
|
"""
|
||||||
model = Membership
|
model = Membership
|
||||||
form_class = MembershipForm
|
form_class = MembershipRolesForm
|
||||||
template_name = 'member/add_members.html'
|
template_name = 'member/add_members.html'
|
||||||
|
extra_context = {"title": _("Manage roles of an user in the club")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -626,15 +649,61 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
# We don't create a full membership, we only update one field
|
|
||||||
form.fields['user'].disabled = True
|
club = self.object.club
|
||||||
del form.fields['date_start']
|
form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
|
||||||
del form.fields['credit_type']
|
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
|
||||||
del form.fields['credit_amount']
|
|
||||||
del form.fields['last_name']
|
|
||||||
del form.fields['first_name']
|
|
||||||
del form.fields['bank']
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
|
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
|
model = Membership
|
||||||
|
table_class = MembershipTable
|
||||||
|
template_name = "member/club_members.html"
|
||||||
|
extra_context = {"title": _("Members of the club")}
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
|
||||||
|
|
||||||
|
if 'search' in self.request.GET:
|
||||||
|
pattern = self.request.GET['search']
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(user__first_name__iregex='^' + pattern)
|
||||||
|
| Q(user__last_name__iregex='^' + pattern)
|
||||||
|
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
||||||
|
)
|
||||||
|
|
||||||
|
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||||
|
|
||||||
|
if only_active:
|
||||||
|
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
|
||||||
|
|
||||||
|
if "roles" in self.request.GET:
|
||||||
|
if not self.request.GET["roles"]:
|
||||||
|
return qs.none()
|
||||||
|
roles_str = self.request.GET["roles"].replace(' ', '').split(',')
|
||||||
|
roles_int = map(int, roles_str)
|
||||||
|
qs = qs.filter(roles__in=roles_int)
|
||||||
|
|
||||||
|
qs = qs.order_by('-date_start', 'user__username')
|
||||||
|
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
club = Club.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request.user, Club, "view")
|
||||||
|
).get(pk=self.kwargs["pk"])
|
||||||
|
context["club"] = club
|
||||||
|
|
||||||
|
applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
|
||||||
|
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
|
||||||
|
context["applicable_roles"] = applicable_roles
|
||||||
|
|
||||||
|
context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
|
@ -5,10 +5,12 @@ from django.contrib import admin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from polymorphic.admin import PolymorphicChildModelAdmin, \
|
from polymorphic.admin import PolymorphicChildModelAdmin, \
|
||||||
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||||
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
||||||
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
||||||
|
from .templatetags.pretty_money import pretty_money
|
||||||
|
|
||||||
|
|
||||||
class AliasInlines(admin.TabularInline):
|
class AliasInlines(admin.TabularInline):
|
||||||
|
@ -19,7 +21,7 @@ class AliasInlines(admin.TabularInline):
|
||||||
model = Alias
|
model = Alias
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Note)
|
@admin.register(Note, site=admin_site)
|
||||||
class NoteAdmin(PolymorphicParentModelAdmin):
|
class NoteAdmin(PolymorphicParentModelAdmin):
|
||||||
"""
|
"""
|
||||||
Parent regrouping all note types as children
|
Parent regrouping all note types as children
|
||||||
|
@ -36,13 +38,12 @@ class NoteAdmin(PolymorphicParentModelAdmin):
|
||||||
|
|
||||||
# Organize notes by registration date
|
# Organize notes by registration date
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
ordering = ['-created_at']
|
|
||||||
|
|
||||||
# Search by aliases
|
# Search by aliases
|
||||||
search_fields = ['alias__name']
|
search_fields = ['alias__name']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(NoteClub)
|
@admin.register(NoteClub, site=admin_site)
|
||||||
class NoteClubAdmin(PolymorphicChildModelAdmin):
|
class NoteClubAdmin(PolymorphicChildModelAdmin):
|
||||||
"""
|
"""
|
||||||
Child for a club note, see NoteAdmin
|
Child for a club note, see NoteAdmin
|
||||||
|
@ -66,15 +67,27 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@admin.register(NoteSpecial)
|
@admin.register(NoteSpecial, site=admin_site)
|
||||||
class NoteSpecialAdmin(PolymorphicChildModelAdmin):
|
class NoteSpecialAdmin(PolymorphicChildModelAdmin):
|
||||||
"""
|
"""
|
||||||
Child for a special note, see NoteAdmin
|
Child for a special note, see NoteAdmin
|
||||||
"""
|
"""
|
||||||
readonly_fields = ('balance',)
|
readonly_fields = ('balance',)
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
"""
|
||||||
|
A club note should not be manually added
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@admin.register(NoteUser)
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
"""
|
||||||
|
A club note should not be manually removed
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(NoteUser, site=admin_site)
|
||||||
class NoteUserAdmin(PolymorphicChildModelAdmin):
|
class NoteUserAdmin(PolymorphicChildModelAdmin):
|
||||||
"""
|
"""
|
||||||
Child for an user note, see NoteAdmin
|
Child for an user note, see NoteAdmin
|
||||||
|
@ -97,16 +110,16 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Transaction)
|
@admin.register(Transaction, site=admin_site)
|
||||||
class TransactionAdmin(PolymorphicParentModelAdmin):
|
class TransactionAdmin(PolymorphicParentModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for Transaction
|
Admin customisation for Transaction
|
||||||
"""
|
"""
|
||||||
child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction)
|
child_models = (Transaction, RecurrentTransaction, MembershipTransaction, SpecialTransaction)
|
||||||
list_display = ('created_at', 'poly_source', 'poly_destination',
|
list_display = ('created_at', 'poly_source', 'poly_destination',
|
||||||
'quantity', 'amount', 'valid')
|
'quantity', 'amount', 'valid')
|
||||||
list_filter = ('valid',)
|
list_filter = ('valid',)
|
||||||
autocomplete_fields = (
|
readonly_fields = (
|
||||||
'source',
|
'source',
|
||||||
'destination',
|
'destination',
|
||||||
)
|
)
|
||||||
|
@ -138,27 +151,35 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@admin.register(MembershipTransaction)
|
@admin.register(MembershipTransaction, site=admin_site)
|
||||||
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
|
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for MembershipTransaction
|
Admin customisation for MembershipTransaction
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SpecialTransaction)
|
@admin.register(RecurrentTransaction, site=admin_site)
|
||||||
|
class RecurrentTransactionAdmin(PolymorphicChildModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin customisation for RecurrentTransaction
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SpecialTransaction, site=admin_site)
|
||||||
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
|
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for SpecialTransaction
|
Admin customisation for SpecialTransaction
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TransactionTemplate)
|
@admin.register(TransactionTemplate, site=admin_site)
|
||||||
class TransactionTemplateAdmin(admin.ModelAdmin):
|
class TransactionTemplateAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for TransactionTemplate
|
Admin customisation for TransactionTemplate
|
||||||
"""
|
"""
|
||||||
list_display = ('name', 'poly_destination', 'amount', 'category', 'display',)
|
list_display = ('name', 'poly_destination', 'pretty_amount', 'category', 'display', 'highlighted',)
|
||||||
list_filter = ('category', 'display')
|
list_filter = ('category', 'display', 'highlighted',)
|
||||||
|
search_fields = ('name', 'destination__club__name', 'amount',)
|
||||||
autocomplete_fields = ('destination',)
|
autocomplete_fields = ('destination',)
|
||||||
|
|
||||||
def poly_destination(self, obj):
|
def poly_destination(self, obj):
|
||||||
|
@ -169,11 +190,15 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
poly_destination.short_description = _('destination')
|
poly_destination.short_description = _('destination')
|
||||||
|
|
||||||
|
def pretty_amount(self, obj):
|
||||||
|
return pretty_money(obj.amount)
|
||||||
|
|
||||||
@admin.register(TemplateCategory)
|
pretty_amount.short_description = _("amount")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(TemplateCategory, site=admin_site)
|
||||||
class TemplateCategoryAdmin(admin.ModelAdmin):
|
class TemplateCategoryAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for TransactionTemplate
|
Admin customisation for TransactionTemplate
|
||||||
"""
|
"""
|
||||||
list_display = ('name',)
|
list_display = ('name',)
|
||||||
list_filter = ('name',)
|
|
||||||
|
|
|
@ -118,9 +118,8 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
# If the user has no right to see the note, then we only display the note identifier
|
# If the user has no right to see the note, then we only display the note identifier
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
|
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
|
||||||
print(obj.pk)
|
|
||||||
return NotePolymorphicSerializer().to_representation(obj.note)
|
return NotePolymorphicSerializer().to_representation(obj.note)
|
||||||
return dict(id=obj.id)
|
return dict(id=obj.note.id, name=str(obj.note))
|
||||||
|
|
||||||
def get_email_confirmed(self, obj):
|
def get_email_confirmed(self, obj):
|
||||||
if isinstance(obj.note, NoteUser):
|
if isinstance(obj.note, NoteUser):
|
||||||
|
|
|
@ -109,7 +109,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(name__regex="^" + alias)
|
Q(name__regex="^" + alias)
|
||||||
| Q(normalized_name__regex="^" + Alias.normalize(alias))
|
| Q(normalized_name__regex="^" + Alias.normalize(alias))
|
||||||
| Q(normalized_name__regex="^" + alias.lower()))
|
| Q(normalized_name__regex="^" + alias.lower()))\
|
||||||
|
.order_by('name').prefetch_related('note')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ class Note(PolymorphicModel):
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(
|
created_at = models.DateTimeField(
|
||||||
verbose_name=_('created at'),
|
verbose_name=_('created at'),
|
||||||
auto_now_add=True,
|
default=timezone.now,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -62,6 +62,7 @@ class TransactionTemplate(models.Model):
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
related_name='templates',
|
||||||
verbose_name=_('type'),
|
verbose_name=_('type'),
|
||||||
max_length=31,
|
max_length=31,
|
||||||
)
|
)
|
||||||
|
@ -71,6 +72,11 @@ class TransactionTemplate(models.Model):
|
||||||
verbose_name=_("display"),
|
verbose_name=_("display"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
highlighted = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("highlighted"),
|
||||||
|
)
|
||||||
|
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
@ -202,7 +208,9 @@ class Transaction(PolymorphicModel):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Save notes
|
# Save notes
|
||||||
|
self.source._force_save = True
|
||||||
self.source.save()
|
self.source.save()
|
||||||
|
self.destination._force_save = True
|
||||||
self.destination.save()
|
self.destination.save()
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
|
|
|
@ -8,6 +8,8 @@ from django.db.models import F
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django_tables2.utils import A
|
from django_tables2.utils import A
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note_kfet.middlewares import get_current_authenticated_user
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models.notes import Alias
|
from .models.notes import Alias
|
||||||
from .models.transactions import Transaction, TransactionTemplate
|
from .models.transactions import Transaction, TransactionTemplate
|
||||||
|
@ -52,14 +54,26 @@ class HistoryTable(tables.Table):
|
||||||
attrs={
|
attrs={
|
||||||
"td": {
|
"td": {
|
||||||
"id": lambda record: "validate_" + str(record.id),
|
"id": lambda record: "validate_" + str(record.id),
|
||||||
"class": lambda record: str(record.valid).lower() + ' validate',
|
"class": lambda record:
|
||||||
|
str(record.valid).lower()
|
||||||
|
+ (' validate' if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
"note.change_transaction_invalidity_reason",
|
||||||
|
record) else ''),
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"),
|
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
||||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
|
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
"note.change_transaction_invalidity_reason", record) else None,
|
||||||
|
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')'
|
||||||
|
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
"note.change_transaction_invalidity_reason", record) else None,
|
||||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||||
+ str(record.id) + '").show();$("#invalidity_reason_'
|
+ str(record.id) + '").show();$("#invalidity_reason_'
|
||||||
+ str(record.id) + '").focus();',
|
+ str(record.id) + '").focus();'
|
||||||
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()',
|
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
"note.change_transaction_invalidity_reason", record) else None,
|
||||||
|
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()'
|
||||||
|
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
"note.change_transaction_invalidity_reason", record) else None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -88,6 +102,10 @@ class HistoryTable(tables.Table):
|
||||||
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
||||||
"""
|
"""
|
||||||
val = "✔" if value else "✖"
|
val = "✔" if value else "✖"
|
||||||
|
if not PermissionBackend\
|
||||||
|
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record):
|
||||||
|
return val
|
||||||
|
|
||||||
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
|
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
|
||||||
+ "' value='" + (html.escape(record.invalidity_reason)
|
+ "' value='" + (html.escape(record.invalidity_reason)
|
||||||
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
|
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
|
||||||
|
@ -131,12 +149,10 @@ class ButtonTable(tables.Table):
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
||||||
'id': lambda record: "row-" + str(record.pk),
|
'id': lambda record: "row-" + str(record.pk),
|
||||||
'data-href': lambda record: record.pk
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
exclude = ('id',)
|
exclude = ('id',)
|
||||||
order_by = ('type', '-display', 'destination__name', 'name',)
|
|
||||||
|
|
||||||
edit = tables.LinkColumn('note:template_update',
|
edit = tables.LinkColumn('note:template_update',
|
||||||
args=[A('pk')],
|
args=[A('pk')],
|
||||||
|
|
|
@ -5,6 +5,7 @@ import json
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, UpdateView
|
from django.views.generic import CreateView, UpdateView
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
|
@ -14,7 +15,7 @@ from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .forms import TransactionTemplateForm
|
from .forms import TransactionTemplateForm
|
||||||
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
||||||
from .models.transactions import SpecialTransaction
|
from .models.transactions import SpecialTransaction
|
||||||
from .tables import HistoryTable, ButtonTable
|
from .tables import HistoryTable, ButtonTable
|
||||||
|
|
||||||
|
@ -29,16 +30,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
||||||
model = Transaction
|
model = Transaction
|
||||||
# Transaction history table
|
# Transaction history table
|
||||||
table_class = HistoryTable
|
table_class = HistoryTable
|
||||||
|
extra_context = {"title": _("Transfer money")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
|
return super().get_queryset(**kwargs).order_by("-created_at").all()[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add some context variables in template such as page title
|
Add some context variables in template such as page title
|
||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['title'] = _('Transfer money')
|
|
||||||
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||||
|
@ -63,6 +64,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
form_class = TransactionTemplateForm
|
form_class = TransactionTemplateForm
|
||||||
success_url = reverse_lazy('note:template_list')
|
success_url = reverse_lazy('note:template_list')
|
||||||
|
extra_context = {"title": _("Create new button")}
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
|
@ -71,6 +73,20 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||||
"""
|
"""
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
table_class = ButtonTable
|
table_class = ButtonTable
|
||||||
|
extra_context = {"title": _("Search button")}
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Filter the user list with the given pattern.
|
||||||
|
"""
|
||||||
|
qs = super().get_queryset().distinct()
|
||||||
|
if "search" in self.request.GET:
|
||||||
|
pattern = self.request.GET["search"]
|
||||||
|
qs = qs.filter(Q(name__iregex="^" + pattern) | Q(destination__club__name__iregex="^" + pattern))
|
||||||
|
|
||||||
|
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
@ -80,6 +96,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
form_class = TransactionTemplateForm
|
form_class = TransactionTemplateForm
|
||||||
success_url = reverse_lazy('note:template_list')
|
success_url = reverse_lazy('note:template_list')
|
||||||
|
extra_context = {"title": _("Update button")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -116,25 +133,28 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
model = Transaction
|
model = Transaction
|
||||||
template_name = "note/conso_form.html"
|
template_name = "note/conso_form.html"
|
||||||
|
extra_context = {"title": _("Consumptions")}
|
||||||
|
|
||||||
# Transaction history table
|
# Transaction history table
|
||||||
table_class = HistoryTable
|
table_class = HistoryTable
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id")[:20]
|
return super().get_queryset(**kwargs).order_by("-created_at")[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add some context variables in template such as page title
|
Add some context variables in template such as page title
|
||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
from django.db.models import Count
|
categories = TemplateCategory.objects.order_by('name').all()
|
||||||
buttons = TransactionTemplate.objects.filter(
|
for category in categories:
|
||||||
|
category.templates_filtered = category.templates.filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||||
).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name')
|
).filter(display=True).order_by('name').all()
|
||||||
context['transaction_templates'] = buttons
|
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
||||||
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
|
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
||||||
context['title'] = _("Consumptions")
|
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||||
|
).order_by('name').all()
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||||
|
|
||||||
# select2 compatibility
|
# select2 compatibility
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models import Permission, PermissionMask, RolePermissions
|
from .models import Permission, PermissionMask, Role
|
||||||
|
|
||||||
|
|
||||||
@admin.register(PermissionMask)
|
@admin.register(PermissionMask, site=admin_site)
|
||||||
class PermissionMaskAdmin(admin.ModelAdmin):
|
class PermissionMaskAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for PermissionMask
|
Admin customisation for PermissionMask
|
||||||
|
@ -14,17 +15,19 @@ class PermissionMaskAdmin(admin.ModelAdmin):
|
||||||
list_display = ('description', 'rank', )
|
list_display = ('description', 'rank', )
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Permission)
|
@admin.register(Permission, site=admin_site)
|
||||||
class PermissionAdmin(admin.ModelAdmin):
|
class PermissionAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for Permission
|
Admin customisation for Permission
|
||||||
"""
|
"""
|
||||||
list_display = ('type', 'model', 'field', 'mask', 'description', )
|
list_display = ('description', 'type', 'model', 'field', 'mask', )
|
||||||
|
list_filter = ('type', 'mask', 'model',)
|
||||||
|
search_fields = ('description', 'field',)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RolePermissions)
|
@admin.register(Role, site=admin_site)
|
||||||
class RolePermissionsAdmin(admin.ModelAdmin):
|
class RoleAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for RolePermissions
|
Admin customisation for Role
|
||||||
"""
|
"""
|
||||||
list_display = ('role', )
|
list_display = ('name', )
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models import Permission, RolePermissions
|
from ..models import Permission, Role
|
||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(serializers.ModelSerializer):
|
class PermissionSerializer(serializers.ModelSerializer):
|
||||||
|
@ -17,12 +17,12 @@ class PermissionSerializer(serializers.ModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class RolePermissionsSerializer(serializers.ModelSerializer):
|
class RoleSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for RolePermissions types.
|
REST API Serializer for Role types.
|
||||||
The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API.
|
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RolePermissions
|
model = Role
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import PermissionViewSet, RolePermissionsViewSet
|
from .views import PermissionViewSet, RoleViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_permission_urls(router, path):
|
def register_permission_urls(router, path):
|
||||||
|
@ -9,4 +9,4 @@ def register_permission_urls(router, path):
|
||||||
Configure router for permission REST API.
|
Configure router for permission REST API.
|
||||||
"""
|
"""
|
||||||
router.register(path + "/permission", PermissionViewSet)
|
router.register(path + "/permission", PermissionViewSet)
|
||||||
router.register(path + "/roles", RolePermissionsViewSet)
|
router.register(path + "/roles", RoleViewSet)
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import PermissionSerializer, RolePermissionsSerializer
|
from .serializers import PermissionSerializer, RoleSerializer
|
||||||
from ..models import Permission, RolePermissions
|
from ..models import Permission, Role
|
||||||
|
|
||||||
|
|
||||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
|
@ -20,13 +20,13 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
filterset_fields = ['model', 'type', ]
|
filterset_fields = ['model', 'type', ]
|
||||||
|
|
||||||
|
|
||||||
class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet):
|
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
|
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
|
||||||
then render it on /api/permission/roles/
|
then render it on /api/permission/roles/
|
||||||
"""
|
"""
|
||||||
queryset = RolePermissions.objects.all()
|
queryset = Role.objects.all()
|
||||||
serializer_class = RolePermissionsSerializer
|
serializer_class = RoleSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['role', ]
|
filterset_fields = ['role', ]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.contrib.auth.models import User, AnonymousUser
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
@ -36,27 +35,19 @@ class PermissionBackend(ModelBackend):
|
||||||
# Unauthenticated users have no permissions
|
# Unauthenticated users have no permissions
|
||||||
return Permission.objects.none()
|
return Permission.objects.none()
|
||||||
|
|
||||||
qs = Permission.objects.annotate(
|
memberships = Membership.objects.filter(user=user).all()
|
||||||
club=F("rolepermissions__role__membership__club"),
|
|
||||||
membership=F("rolepermissions__role__membership"),
|
|
||||||
).filter(
|
|
||||||
(
|
|
||||||
Q(
|
|
||||||
rolepermissions__role__membership__date_start__lte=timezone.now().today(),
|
|
||||||
rolepermissions__role__membership__date_end__gte=timezone.now().today(),
|
|
||||||
)
|
|
||||||
| Q(permanent=True)
|
|
||||||
)
|
|
||||||
& Q(rolepermissions__role__membership__user=user)
|
|
||||||
& Q(type=t)
|
|
||||||
& Q(mask__rank__lte=get_current_session().get("permission_mask", 0))
|
|
||||||
)
|
|
||||||
|
|
||||||
if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
|
perms = []
|
||||||
qs = qs.distinct('pk', 'club')
|
|
||||||
else: # SQLite doesn't support distinct fields.
|
for membership in memberships:
|
||||||
qs = qs.distinct()
|
for role in membership.roles.all():
|
||||||
return qs
|
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
|
||||||
|
if not perm.permanent:
|
||||||
|
if membership.date_start > timezone.now().date() or membership.date_end < timezone.now().date():
|
||||||
|
continue
|
||||||
|
perm.membership = membership
|
||||||
|
perms.append(perm)
|
||||||
|
return perms
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def permissions(user, model, type):
|
def permissions(user, model, type):
|
||||||
|
@ -67,22 +58,13 @@ class PermissionBackend(ModelBackend):
|
||||||
:param type: The type of the permissions: view, change, add or delete
|
:param type: The type of the permissions: view, change, add or delete
|
||||||
:return: A generator of the requested permissions
|
:return: A generator of the requested permissions
|
||||||
"""
|
"""
|
||||||
clubs = {}
|
|
||||||
memberships = {}
|
|
||||||
|
|
||||||
for permission in PermissionBackend.get_raw_permissions(user, type):
|
for permission in PermissionBackend.get_raw_permissions(user, type):
|
||||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
|
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if permission.club not in clubs:
|
membership = permission.membership
|
||||||
clubs[permission.club] = club = Club.objects.get(pk=permission.club)
|
club = membership.club
|
||||||
else:
|
|
||||||
club = clubs[permission.club]
|
|
||||||
|
|
||||||
if permission.membership not in memberships:
|
|
||||||
memberships[permission.membership] = membership = Membership.objects.get(pk=permission.membership)
|
|
||||||
else:
|
|
||||||
membership = memberships[permission.membership]
|
|
||||||
|
|
||||||
permission = permission.about(
|
permission = permission.about(
|
||||||
user=user,
|
user=user,
|
||||||
|
@ -113,12 +95,11 @@ class PermissionBackend(ModelBackend):
|
||||||
:param field: The field of the model to test, if concerned
|
:param field: The field of the model to test, if concerned
|
||||||
:return: A query that corresponds to the filter to give to a queryset
|
:return: A query that corresponds to the filter to give to a queryset
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if user is None or isinstance(user, AnonymousUser):
|
if user is None or isinstance(user, AnonymousUser):
|
||||||
# Anonymous users can't do anything
|
# Anonymous users can't do anything
|
||||||
return Q(pk=-1)
|
return Q(pk=-1)
|
||||||
|
|
||||||
if user.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
|
||||||
# Superusers have all rights
|
# Superusers have all rights
|
||||||
return Q()
|
return Q()
|
||||||
|
|
||||||
|
@ -154,7 +135,7 @@ class PermissionBackend(ModelBackend):
|
||||||
if sess is not None and sess.session_key is None:
|
if sess is not None and sess.session_key is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if user_obj.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
|
@ -163,6 +144,7 @@ class PermissionBackend(ModelBackend):
|
||||||
perm = perm.split('.')[-1].split('_', 2)
|
perm = perm.split('.')[-1].split('_', 2)
|
||||||
perm_type = perm[0]
|
perm_type = perm[0]
|
||||||
perm_field = perm[2] if len(perm) == 3 else None
|
perm_field = perm[2] if len(perm) == 3 else None
|
||||||
|
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
if any(permission.applies(obj, perm_type, perm_field)
|
if any(permission.applies(obj, perm_type, perm_field)
|
||||||
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
|
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from note_kfet.middlewares import get_current_session
|
from note_kfet.middlewares import get_current_session
|
||||||
|
|
||||||
|
@ -32,6 +33,10 @@ def memoize(f):
|
||||||
sess_funs = new_sess_funs
|
sess_funs = new_sess_funs
|
||||||
|
|
||||||
def func(*args, **kwargs):
|
def func(*args, **kwargs):
|
||||||
|
if settings.DEBUG:
|
||||||
|
# Don't memoize in DEBUG mode
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
nonlocal last_collect
|
nonlocal last_collect
|
||||||
|
|
||||||
if time() - last_collect > 60:
|
if time() - last_collect > 60:
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q, Model
|
from django.db.models import F, Q, Model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from member.models import Role
|
|
||||||
|
|
||||||
|
|
||||||
class InstancedPermission:
|
class InstancedPermission:
|
||||||
|
@ -45,7 +45,17 @@ class InstancedPermission:
|
||||||
else:
|
else:
|
||||||
oldpk = obj.pk
|
oldpk = obj.pk
|
||||||
# Ensure previous models are deleted
|
# Ensure previous models are deleted
|
||||||
self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
|
count = 0
|
||||||
|
while count < 1000:
|
||||||
|
if self.model.model_class().objects.filter(pk=obj.pk).exists():
|
||||||
|
# If the object exists, that means that one permission is currently checked.
|
||||||
|
# We wait before the other permission, at most 1 second.
|
||||||
|
sleep(1)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
for o in self.model.model_class().objects.filter(pk=obj.pk).all():
|
||||||
|
o._force_delete = True
|
||||||
|
Model.delete(o)
|
||||||
# Force insertion, no data verification, no trigger
|
# Force insertion, no data verification, no trigger
|
||||||
obj._force_save = True
|
obj._force_save = True
|
||||||
Model.save(obj, force_insert=True)
|
Model.save(obj, force_insert=True)
|
||||||
|
@ -114,10 +124,10 @@ class PermissionMask(models.Model):
|
||||||
class Permission(models.Model):
|
class Permission(models.Model):
|
||||||
|
|
||||||
PERMISSION_TYPES = [
|
PERMISSION_TYPES = [
|
||||||
('add', 'add'),
|
('add', _('add')),
|
||||||
('view', 'view'),
|
('view', _('view')),
|
||||||
('change', 'change'),
|
('change', _('change')),
|
||||||
('delete', 'delete')
|
('delete', _('delete'))
|
||||||
]
|
]
|
||||||
|
|
||||||
model = models.ForeignKey(
|
model = models.ForeignKey(
|
||||||
|
@ -239,6 +249,9 @@ class Permission(models.Model):
|
||||||
field = Permission.compute_param(value[i], **kwargs)
|
field = Permission.compute_param(value[i], **kwargs)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not hasattr(field, value[i][0]):
|
||||||
|
return False
|
||||||
|
|
||||||
field = getattr(field, value[i][0])
|
field = getattr(field, value[i][0])
|
||||||
params = []
|
params = []
|
||||||
call_kwargs = {}
|
call_kwargs = {}
|
||||||
|
@ -252,6 +265,9 @@ class Permission(models.Model):
|
||||||
params.append(param)
|
params.append(param)
|
||||||
field = field(*params, **call_kwargs)
|
field = field(*params, **call_kwargs)
|
||||||
else:
|
else:
|
||||||
|
if not hasattr(field, value[i]):
|
||||||
|
return False
|
||||||
|
|
||||||
field = getattr(field, value[i])
|
field = getattr(field, value[i])
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
@ -276,7 +292,7 @@ class Permission(models.Model):
|
||||||
elif query[0] == 'NOT':
|
elif query[0] == 'NOT':
|
||||||
return ~Permission._about(query[1], **kwargs)
|
return ~Permission._about(query[1], **kwargs)
|
||||||
else:
|
else:
|
||||||
return Q(pk=F("pk"))
|
return Q(pk=F("pk")) if Permission.compute_param(query, **kwargs) else ~Q(pk=F("pk"))
|
||||||
elif isinstance(query, dict):
|
elif isinstance(query, dict):
|
||||||
q_kwargs = {}
|
q_kwargs = {}
|
||||||
for key in query:
|
for key in query:
|
||||||
|
@ -307,23 +323,30 @@ class Permission(models.Model):
|
||||||
return self.description
|
return self.description
|
||||||
|
|
||||||
|
|
||||||
class RolePermissions(models.Model):
|
class Role(models.Model):
|
||||||
"""
|
"""
|
||||||
Permissions associated with a Role
|
Permissions associated with a Role
|
||||||
"""
|
"""
|
||||||
role = models.OneToOneField(
|
name = models.CharField(
|
||||||
Role,
|
max_length=255,
|
||||||
on_delete=models.PROTECT,
|
verbose_name=_("name"),
|
||||||
related_name='permissions',
|
|
||||||
verbose_name=_('role'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
permissions = models.ManyToManyField(
|
permissions = models.ManyToManyField(
|
||||||
Permission,
|
Permission,
|
||||||
verbose_name=_("permissions"),
|
verbose_name=_("permissions"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for_club = models.ForeignKey(
|
||||||
|
"member.Club",
|
||||||
|
verbose_name=_("for club"),
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.role)
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("role permissions")
|
verbose_name = _("role permissions")
|
||||||
|
|
|
@ -19,8 +19,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
||||||
'OPTIONS': [],
|
'OPTIONS': [],
|
||||||
'HEAD': [],
|
'HEAD': [],
|
||||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
'PUT': [], # ['%(app_label)s.change_%(model_name)s'],
|
||||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
'PATCH': [], # ['%(app_label)s.change_%(model_name)s'],
|
||||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ def pre_save_object(sender, instance, **kwargs):
|
||||||
|
|
||||||
# In the other case, we check if he/she has the right to change one field
|
# In the other case, we check if he/she has the right to change one field
|
||||||
previous = qs.get()
|
previous = qs.get()
|
||||||
|
|
||||||
for field in instance._meta.fields:
|
for field in instance._meta.fields:
|
||||||
field_name = field.name
|
field_name = field.name
|
||||||
old_value = getattr(previous, field.name)
|
old_value = getattr(previous, field.name)
|
||||||
|
@ -81,7 +82,8 @@ def pre_delete_object(instance, **kwargs):
|
||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED:
|
||||||
return
|
return
|
||||||
|
|
||||||
if hasattr(instance, "_force_delete"):
|
if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0:
|
||||||
|
# Don't check permissions on force-deleted objects
|
||||||
return
|
return
|
||||||
|
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django import template
|
from django import template
|
||||||
|
@ -16,9 +17,9 @@ def not_empty_model_list(model_name):
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
session = get_current_session()
|
session = get_current_session()
|
||||||
if user is None:
|
if user is None or isinstance(user, AnonymousUser):
|
||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
qs = model_list(model_name)
|
qs = model_list(model_name)
|
||||||
return qs.exists()
|
return qs.exists()
|
||||||
|
@ -31,28 +32,38 @@ def not_empty_model_change_list(model_name):
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
session = get_current_session()
|
session = get_current_session()
|
||||||
if user is None:
|
if user is None or isinstance(user, AnonymousUser):
|
||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
qs = model_list(model_name, "change")
|
qs = model_list(model_name, "change")
|
||||||
return qs.exists()
|
return qs.exists()
|
||||||
|
|
||||||
|
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def model_list(model_name, t="view"):
|
def model_list(model_name, t="view", fetch=True):
|
||||||
"""
|
"""
|
||||||
Return the queryset of all visible instances of the given model.
|
Return the queryset of all visible instances of the given model.
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
if user is None:
|
|
||||||
return False
|
|
||||||
spl = model_name.split(".")
|
spl = model_name.split(".")
|
||||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
|
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
|
||||||
|
if user is None or isinstance(user, AnonymousUser):
|
||||||
|
return qs.none()
|
||||||
|
if fetch:
|
||||||
|
qs = qs.all()
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
@stringfilter
|
||||||
|
def model_list_length(model_name, t="view"):
|
||||||
|
"""
|
||||||
|
Return the length of queryset of all visible instances of the given model.
|
||||||
|
"""
|
||||||
|
return model_list(model_name, t, False).count()
|
||||||
|
|
||||||
|
|
||||||
def has_perm(perm, obj):
|
def has_perm(perm, obj):
|
||||||
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
|
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
|
||||||
|
|
||||||
|
@ -63,9 +74,9 @@ def can_create_transaction():
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
session = get_current_session()
|
session = get_current_session()
|
||||||
if user is None:
|
if user is None or isinstance(user, AnonymousUser):
|
||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
if session.get("can_create_transaction", None):
|
if session.get("can_create_transaction", None):
|
||||||
return session.get("can_create_transaction", None) == 1
|
return session.get("can_create_transaction", None) == 1
|
||||||
|
@ -85,4 +96,5 @@ register = template.Library()
|
||||||
register.filter('not_empty_model_list', not_empty_model_list)
|
register.filter('not_empty_model_list', not_empty_model_list)
|
||||||
register.filter('not_empty_model_change_list', not_empty_model_change_list)
|
register.filter('not_empty_model_change_list', not_empty_model_change_list)
|
||||||
register.filter('model_list', model_list)
|
register.filter('model_list', model_list)
|
||||||
|
register.filter('model_list_length', model_list_length)
|
||||||
register.filter('has_perm', has_perm)
|
register.filter('has_perm', has_perm)
|
||||||
|
|
|
@ -76,7 +76,7 @@ class PermissionQueryTestCase(TestCase):
|
||||||
model = perm.model.model_class()
|
model = perm.model.model_class()
|
||||||
model.objects.filter(query).all()
|
model.objects.filter(query).all()
|
||||||
# print("Good query for permission", perm)
|
# print("Good query for permission", perm)
|
||||||
except (FieldError, AttributeError, ValueError):
|
except (FieldError, AttributeError, ValueError, TypeError):
|
||||||
print("Query error for permission", perm)
|
print("Query error for permission", perm)
|
||||||
print("Query:", perm.query)
|
print("Query:", perm.query)
|
||||||
if instanced.query:
|
if instanced.query:
|
||||||
|
|
|
@ -5,9 +5,10 @@ from datetime import date
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import UpdateView, TemplateView
|
from django.views.generic import UpdateView, TemplateView
|
||||||
from member.models import Role, Membership
|
from member.models import Membership
|
||||||
|
|
||||||
from .backends import PermissionBackend
|
from .backends import PermissionBackend
|
||||||
|
from .models import Role
|
||||||
|
|
||||||
|
|
||||||
class ProtectQuerysetMixin:
|
class ProtectQuerysetMixin:
|
||||||
|
@ -19,7 +20,7 @@ class ProtectQuerysetMixin:
|
||||||
"""
|
"""
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()
|
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
|
@ -40,6 +41,7 @@ class ProtectQuerysetMixin:
|
||||||
|
|
||||||
class RightsView(TemplateView):
|
class RightsView(TemplateView):
|
||||||
template_name = "permission/all_rights.html"
|
template_name = "permission/all_rights.html"
|
||||||
|
extra_context = {"title": _("Rights")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
|
@ -24,7 +24,8 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
|
||||||
# Truncate microseconds so that tokens are consistent even if the
|
# Truncate microseconds so that tokens are consistent even if the
|
||||||
# database doesn't support microseconds.
|
# database doesn't support microseconds.
|
||||||
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
||||||
return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
|
return str(user.pk) + str(user.email) + str(user.profile.email_confirmed)\
|
||||||
|
+ str(login_timestamp) + str(timestamp)
|
||||||
|
|
||||||
|
|
||||||
email_validation_token = AccountActivationTokenGenerator()
|
email_validation_token = AccountActivationTokenGenerator()
|
||||||
|
|
|
@ -15,10 +15,11 @@ from django.views.generic import CreateView, TemplateView, DetailView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from member.forms import ProfileForm
|
from member.forms import ProfileForm
|
||||||
from member.models import Membership, Club, Role
|
from member.models import Membership, Club
|
||||||
from note.models import SpecialTransaction
|
from note.models import SpecialTransaction
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
from permission.models import Role
|
||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .forms import SignUpForm, ValidationForm
|
from .forms import SignUpForm, ValidationForm
|
||||||
|
@ -34,6 +35,7 @@ class UserCreateView(CreateView):
|
||||||
form_class = SignUpForm
|
form_class = SignUpForm
|
||||||
template_name = 'registration/signup.html'
|
template_name = 'registration/signup.html'
|
||||||
second_form = ProfileForm
|
second_form = ProfileForm
|
||||||
|
extra_context = {"title": _("Register new user")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -77,6 +79,7 @@ class UserValidateView(TemplateView):
|
||||||
"""
|
"""
|
||||||
title = _("Email validation")
|
title = _("Email validation")
|
||||||
template_name = 'registration/email_validation_complete.html'
|
template_name = 'registration/email_validation_complete.html'
|
||||||
|
extra_context = {"title": _("Validate email")}
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -90,15 +93,12 @@ class UserValidateView(TemplateView):
|
||||||
|
|
||||||
# Validate the token
|
# Validate the token
|
||||||
if user is not None and email_validation_token.check_token(user, token):
|
if user is not None and email_validation_token.check_token(user, token):
|
||||||
self.validlink = True
|
|
||||||
# The user must wait that someone validates the account before the user can be active and login.
|
# The user must wait that someone validates the account before the user can be active and login.
|
||||||
|
self.validlink = True
|
||||||
user.is_active = user.profile.registration_valid or user.is_superuser
|
user.is_active = user.profile.registration_valid or user.is_superuser
|
||||||
user.profile.email_confirmed = True
|
user.profile.email_confirmed = True
|
||||||
user.save()
|
user.save()
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
return super().dispatch(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
# Display the "Email validation unsuccessful" page.
|
|
||||||
return self.render_to_response(self.get_context_data())
|
return self.render_to_response(self.get_context_data())
|
||||||
|
|
||||||
def get_user(self, uidb64):
|
def get_user(self, uidb64):
|
||||||
|
@ -132,7 +132,7 @@ class UserValidationEmailSentView(TemplateView):
|
||||||
Display the information that the validation link has been sent.
|
Display the information that the validation link has been sent.
|
||||||
"""
|
"""
|
||||||
template_name = 'registration/email_validation_email_sent.html'
|
template_name = 'registration/email_validation_email_sent.html'
|
||||||
title = _('Email validation email sent')
|
extra_context = {"title": _('Email validation email sent')}
|
||||||
|
|
||||||
|
|
||||||
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
|
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
|
||||||
|
@ -140,6 +140,7 @@ class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, De
|
||||||
Rensend the email validation link.
|
Rensend the email validation link.
|
||||||
"""
|
"""
|
||||||
model = User
|
model = User
|
||||||
|
extra_context = {"title": _("Resend email validation link")}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
|
@ -157,6 +158,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
model = User
|
model = User
|
||||||
table_class = FutureUserTable
|
table_class = FutureUserTable
|
||||||
template_name = 'registration/future_user_list.html'
|
template_name = 'registration/future_user_list.html'
|
||||||
|
extra_context = {"title": _("Pre-registered users list")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -164,7 +166,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset().filter(profile__registration_valid=False)
|
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
@ -198,6 +200,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
form_class = ValidationForm
|
form_class = ValidationForm
|
||||||
context_object_name = "user_object"
|
context_object_name = "user_object"
|
||||||
template_name = "registration/future_profile_detail.html"
|
template_name = "registration/future_profile_detail.html"
|
||||||
|
extra_context = {"title": _("Registration detail")}
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
@ -354,6 +357,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Delete a pre-registered user.
|
Delete a pre-registered user.
|
||||||
"""
|
"""
|
||||||
|
extra_context = {"title": _("Invalidate pre-registration")}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ee54fca89ee247a4ba4af080dd3036d92340eade
|
Subproject commit dce51ad26134d396d7cbfca7c63bd2ed391dd969
|
|
@ -2,11 +2,12 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models import RemittanceType, Remittance, SogeCredit
|
from .models import RemittanceType, Remittance, SogeCredit
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RemittanceType)
|
@admin.register(RemittanceType, site=admin_site)
|
||||||
class RemittanceTypeAdmin(admin.ModelAdmin):
|
class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for RemiitanceType
|
Admin customisation for RemiitanceType
|
||||||
|
@ -14,7 +15,7 @@ class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||||
list_display = ('note', )
|
list_display = ('note', )
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Remittance)
|
@admin.register(Remittance, site=admin_site)
|
||||||
class RemittanceAdmin(admin.ModelAdmin):
|
class RemittanceAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
Admin customisation for Remittance
|
Admin customisation for Remittance
|
||||||
|
@ -27,4 +28,14 @@ class RemittanceAdmin(admin.ModelAdmin):
|
||||||
return not obj.closed and super().has_change_permission(request, obj)
|
return not obj.closed and super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(SogeCredit)
|
@admin.register(SogeCredit, site=admin_site)
|
||||||
|
class SogeCreditAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin customisation for Remittance
|
||||||
|
"""
|
||||||
|
list_display = ('user', 'valid',)
|
||||||
|
readonly_fields = ('transactions', 'credit_transaction',)
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
# Don't create a credit manually
|
||||||
|
return False
|
||||||
|
|
|
@ -8,7 +8,6 @@ from crispy_forms.layout import Submit
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.inputs import DatePickerInput, AmountInput
|
from note_kfet.inputs import DatePickerInput, AmountInput
|
||||||
from permission.backends import PermissionBackend
|
|
||||||
|
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||||
|
|
||||||
|
@ -132,8 +131,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
|
||||||
# Add submit button
|
# Add submit button
|
||||||
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
||||||
|
|
||||||
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\
|
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
|
|
||||||
|
|
||||||
def clean_last_name(self):
|
def clean_last_name(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
|
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ class Invoice(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
date = models.DateField(
|
date = models.DateField(
|
||||||
auto_now_add=True,
|
default=timezone.now,
|
||||||
verbose_name=_("Place"),
|
verbose_name=_("Place"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -130,7 +131,7 @@ class Remittance(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
date = models.DateTimeField(
|
date = models.DateTimeField(
|
||||||
auto_now_add=True,
|
default=timezone.now,
|
||||||
verbose_name=_("Date"),
|
verbose_name=_("Date"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, UpdateView, DetailView
|
from django.views.generic import CreateView, UpdateView, DetailView
|
||||||
from django.views.generic.base import View, TemplateView
|
from django.views.generic.base import View, TemplateView
|
||||||
from django.views.generic.edit import BaseFormView
|
from django.views.generic.edit import BaseFormView
|
||||||
|
@ -35,6 +36,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = Invoice
|
model = Invoice
|
||||||
form_class = InvoiceForm
|
form_class = InvoiceForm
|
||||||
|
extra_context = {"title": _("Create new invoice")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -77,6 +79,7 @@ class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView)
|
||||||
"""
|
"""
|
||||||
model = Invoice
|
model = Invoice
|
||||||
table_class = InvoiceTable
|
table_class = InvoiceTable
|
||||||
|
extra_context = {"title": _("Invoices list")}
|
||||||
|
|
||||||
|
|
||||||
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
@ -85,6 +88,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
model = Invoice
|
model = Invoice
|
||||||
form_class = InvoiceForm
|
form_class = InvoiceForm
|
||||||
|
extra_context = {"title": _("Update an invoice")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -167,7 +171,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
||||||
del tex
|
del tex
|
||||||
|
|
||||||
# The file has to be rendered twice
|
# The file has to be rendered twice
|
||||||
for _ in range(2):
|
for ignored in range(2):
|
||||||
error = subprocess.Popen(
|
error = subprocess.Popen(
|
||||||
["pdflatex", "invoice-{}.tex".format(pk)],
|
["pdflatex", "invoice-{}.tex".format(pk)],
|
||||||
cwd=tmp_dir,
|
cwd=tmp_dir,
|
||||||
|
@ -198,6 +202,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
|
||||||
"""
|
"""
|
||||||
model = Remittance
|
model = Remittance
|
||||||
form_class = RemittanceForm
|
form_class = RemittanceForm
|
||||||
|
extra_context = {"title": _("Create a new remittance")}
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:remittance_list')
|
return reverse_lazy('treasury:remittance_list')
|
||||||
|
@ -218,27 +223,46 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
||||||
List existing Remittances
|
List existing Remittances
|
||||||
"""
|
"""
|
||||||
template_name = "treasury/remittance_list.html"
|
template_name = "treasury/remittance_list.html"
|
||||||
|
extra_context = {"title": _("Remittances list")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
context["opened_remittances"] = RemittanceTable(
|
opened_remittances = RemittanceTable(
|
||||||
data=Remittance.objects.filter(closed=False).filter(
|
data=Remittance.objects.filter(closed=False).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
|
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||||
context["closed_remittances"] = RemittanceTable(
|
prefix="opened-remittances-",
|
||||||
data=Remittance.objects.filter(closed=True).filter(
|
)
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all())
|
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
|
||||||
|
context["opened_remittances"] = opened_remittances
|
||||||
|
|
||||||
context["special_transactions_no_remittance"] = SpecialTransactionTable(
|
closed_remittances = RemittanceTable(
|
||||||
|
data=Remittance.objects.filter(closed=True).filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all(),
|
||||||
|
prefix="closed-remittances-",
|
||||||
|
)
|
||||||
|
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
|
||||||
|
context["closed_remittances"] = closed_remittances
|
||||||
|
|
||||||
|
no_remittance_tr = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
specialtransactionproxy__remittance=None).filter(
|
specialtransactionproxy__remittance=None).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||||
exclude=('remittance_remove', ))
|
exclude=('remittance_remove', ),
|
||||||
context["special_transactions_with_remittance"] = SpecialTransactionTable(
|
prefix="no-remittance-",
|
||||||
|
)
|
||||||
|
no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10)
|
||||||
|
context["special_transactions_no_remittance"] = no_remittance_tr
|
||||||
|
|
||||||
|
with_remittance_tr = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
specialtransactionproxy__remittance__closed=False).filter(
|
specialtransactionproxy__remittance__closed=False).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||||
exclude=('remittance_add', ))
|
exclude=('remittance_add', ),
|
||||||
|
prefix="with-remittance-",
|
||||||
|
)
|
||||||
|
with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10)
|
||||||
|
context["special_transactions_with_remittance"] = with_remittance_tr
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -249,6 +273,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
|
||||||
"""
|
"""
|
||||||
model = Remittance
|
model = Remittance
|
||||||
form_class = RemittanceForm
|
form_class = RemittanceForm
|
||||||
|
extra_context = {"title": _("Update a remittance")}
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:remittance_list')
|
return reverse_lazy('treasury:remittance_list')
|
||||||
|
@ -271,9 +296,9 @@ class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin,
|
||||||
"""
|
"""
|
||||||
Attach a special transaction to a remittance
|
Attach a special transaction to a remittance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = SpecialTransactionProxy
|
model = SpecialTransactionProxy
|
||||||
form_class = LinkTransactionToRemittanceForm
|
form_class = LinkTransactionToRemittanceForm
|
||||||
|
extra_context = {"title": _("Attach a transaction to a remittance")}
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:remittance_list')
|
return reverse_lazy('treasury:remittance_list')
|
||||||
|
@ -317,6 +342,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||||
"""
|
"""
|
||||||
model = SogeCredit
|
model = SogeCredit
|
||||||
table_class = SogeCreditTable
|
table_class = SogeCreditTable
|
||||||
|
extra_context = {"title": _("List of credits from the Société générale")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -355,6 +381,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie
|
||||||
"""
|
"""
|
||||||
model = SogeCredit
|
model = SogeCredit
|
||||||
form_class = Form
|
form_class = Form
|
||||||
|
extra_context = {"title": _("Manage credits from the Société générale")}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if "validate" in form.data:
|
if "validate" in form.data:
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
|
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
|
||||||
|
|
||||||
admin.site.register(WEIClub)
|
admin_site.register(WEIClub)
|
||||||
admin.site.register(WEIRegistration)
|
admin_site.register(WEIRegistration)
|
||||||
admin.site.register(WEIMembership)
|
admin_site.register(WEIMembership)
|
||||||
admin.site.register(WEIRole)
|
admin_site.register(WEIRole)
|
||||||
admin.site.register(Bus)
|
admin_site.register(Bus)
|
||||||
admin.site.register(BusTeam)
|
admin_site.register(BusTeam)
|
||||||
|
|
|
@ -96,7 +96,7 @@ class WEIMembershipForm(forms.ModelForm):
|
||||||
class BusForm(forms.ModelForm):
|
class BusForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bus
|
model = Bus
|
||||||
fields = '__all__'
|
exclude = ('information_json',)
|
||||||
widgets = {
|
widgets = {
|
||||||
"wei": Autocomplete(
|
"wei": Autocomplete(
|
||||||
WEIClub,
|
WEIClub,
|
||||||
|
|
|
@ -8,8 +8,9 @@ from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from member.models import Role, Club, Membership
|
from member.models import Club, Membership
|
||||||
from note.models import MembershipTransaction
|
from note.models import MembershipTransaction
|
||||||
|
from permission.models import Role
|
||||||
|
|
||||||
|
|
||||||
class WEIClub(Club):
|
class WEIClub(Club):
|
||||||
|
@ -113,6 +114,7 @@ class BusTeam(models.Model):
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
verbose_name=_("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
||||||
|
@ -188,6 +190,28 @@ class WEIRegistration(models.Model):
|
||||||
verbose_name=_("gender"),
|
verbose_name=_("gender"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clothing_cut = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=(
|
||||||
|
('male', _("Male")),
|
||||||
|
('female', _("Female")),
|
||||||
|
),
|
||||||
|
verbose_name=_("clothing cut"),
|
||||||
|
)
|
||||||
|
|
||||||
|
clothing_size = models.CharField(
|
||||||
|
max_length=4,
|
||||||
|
choices=(
|
||||||
|
('XS', "XS"),
|
||||||
|
('S', "S"),
|
||||||
|
('M', "M"),
|
||||||
|
('L', "L"),
|
||||||
|
('XL', "XL"),
|
||||||
|
('XXL', "XXL"),
|
||||||
|
),
|
||||||
|
verbose_name=_("clothing size"),
|
||||||
|
)
|
||||||
|
|
||||||
health_issues = models.TextField(
|
health_issues = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
|
|
|
@ -103,7 +103,7 @@ class WEIMembershipTable(tables.Table):
|
||||||
|
|
||||||
team = tables.LinkColumn(
|
team = tables.LinkColumn(
|
||||||
'wei:manage_bus_team',
|
'wei:manage_bus_team',
|
||||||
args=[A('bus.pk')],
|
args=[A('team.pk')],
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_year(self, record):
|
def render_year(self, record):
|
||||||
|
@ -144,10 +144,10 @@ class BusTable(tables.Table):
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_teams(self, value):
|
def render_teams(self, value):
|
||||||
return ", ".join(team.name for team in value.all())
|
return ", ".join(team.name for team in value.order_by('name').all())
|
||||||
|
|
||||||
def render_count(self, value):
|
def render_count(self, value):
|
||||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
|
@ -178,7 +178,7 @@ class BusTeamTable(tables.Table):
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_count(self, value):
|
def render_count(self, value):
|
||||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
|
||||||
|
|
||||||
count = tables.Column(
|
count = tables.Column(
|
||||||
verbose_name=_("Members count"),
|
verbose_name=_("Members count"),
|
||||||
|
|
|
@ -17,6 +17,7 @@ from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils import timezone
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
|
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -52,6 +53,16 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
model = WEIClub
|
model = WEIClub
|
||||||
table_class = WEITable
|
table_class = WEITable
|
||||||
ordering = '-year'
|
ordering = '-year'
|
||||||
|
extra_context = {"title": _("Search WEI")}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub(
|
||||||
|
year=0,
|
||||||
|
date_start=timezone.now().date(),
|
||||||
|
date_end=timezone.now().date(),
|
||||||
|
))
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
|
@ -60,6 +71,7 @@ class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = WEIClub
|
model = WEIClub
|
||||||
form_class = WEIForm
|
form_class = WEIForm
|
||||||
|
extra_context = {"title": _("Create WEI")}
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.requires_membership = True
|
form.instance.requires_membership = True
|
||||||
|
@ -79,6 +91,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
model = WEIClub
|
model = WEIClub
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
|
extra_context = {"title": _("WEI Detail")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -132,6 +145,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
else:
|
else:
|
||||||
# Check if the user has the right to create a registration of a random first year member.
|
# Check if the user has the right to create a registration of a random first year member.
|
||||||
empty_fy_registration = WEIRegistration(
|
empty_fy_registration = WEIRegistration(
|
||||||
|
wei=club,
|
||||||
user=random_user,
|
user=random_user,
|
||||||
first_year=True,
|
first_year=True,
|
||||||
birth_date="1970-01-01",
|
birth_date="1970-01-01",
|
||||||
|
@ -144,6 +158,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
|
|
||||||
# Check if the user has the right to create a registration of a random old member.
|
# Check if the user has the right to create a registration of a random old member.
|
||||||
empty_old_registration = WEIRegistration(
|
empty_old_registration = WEIRegistration(
|
||||||
|
wei=club,
|
||||||
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
|
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
|
||||||
first_year=False,
|
first_year=False,
|
||||||
birth_date="1970-01-01",
|
birth_date="1970-01-01",
|
||||||
|
@ -171,13 +186,14 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
"""
|
"""
|
||||||
model = WEIMembership
|
model = WEIMembership
|
||||||
table_class = WEIMembershipTable
|
table_class = WEIMembershipTable
|
||||||
|
extra_context = {"title": _("View members of the WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs).filter(club=self.club)
|
qs = super().get_queryset(**kwargs).filter(club=self.club).distinct()
|
||||||
|
|
||||||
pattern = self.request.GET.get("search", "")
|
pattern = self.request.GET.get("search", "")
|
||||||
|
|
||||||
|
@ -208,13 +224,14 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
|
||||||
"""
|
"""
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
table_class = WEIRegistrationTable
|
table_class = WEIRegistrationTable
|
||||||
|
extra_context = {"title": _("View registrations to the WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
|
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
|
||||||
|
|
||||||
pattern = self.request.GET.get("search", "")
|
pattern = self.request.GET.get("search", "")
|
||||||
|
|
||||||
|
@ -244,6 +261,7 @@ class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = WEIClub
|
model = WEIClub
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
form_class = WEIForm
|
form_class = WEIForm
|
||||||
|
extra_context = {"title": _("Update the WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = self.get_object()
|
wei = self.get_object()
|
||||||
|
@ -264,6 +282,7 @@ class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = Bus
|
model = Bus
|
||||||
form_class = BusForm
|
form_class = BusForm
|
||||||
|
extra_context = {"title": _("Create new bus")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
|
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||||
|
@ -294,6 +313,7 @@ class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
model = Bus
|
model = Bus
|
||||||
form_class = BusForm
|
form_class = BusForm
|
||||||
|
extra_context = {"title": _("Update bus")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = self.get_object().wei
|
wei = self.get_object().wei
|
||||||
|
@ -323,6 +343,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
Manage Bus
|
Manage Bus
|
||||||
"""
|
"""
|
||||||
model = Bus
|
model = Bus
|
||||||
|
extra_context = {"title": _("Manage bus")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -330,7 +351,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
|
|
||||||
bus = self.object
|
bus = self.object
|
||||||
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
||||||
.filter(bus=bus).annotate(count=Count("memberships"))
|
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
|
||||||
teams_table = BusTeamTable(data=teams, prefix="team-")
|
teams_table = BusTeamTable(data=teams, prefix="team-")
|
||||||
context["teams"] = teams_table
|
context["teams"] = teams_table
|
||||||
|
|
||||||
|
@ -349,6 +370,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = BusTeam
|
model = BusTeam
|
||||||
form_class = BusTeamForm
|
form_class = BusTeamForm
|
||||||
|
extra_context = {"title": _("Create new team")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
|
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
|
||||||
|
@ -380,6 +402,7 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
model = BusTeam
|
model = BusTeam
|
||||||
form_class = BusTeamForm
|
form_class = BusTeamForm
|
||||||
|
extra_context = {"title": _("Update team")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = self.get_object().bus.wei
|
wei = self.get_object().bus.wei
|
||||||
|
@ -410,6 +433,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
Manage Bus team
|
Manage Bus team
|
||||||
"""
|
"""
|
||||||
model = BusTeam
|
model = BusTeam
|
||||||
|
extra_context = {"title": _("Manage WEI team")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -431,6 +455,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
form_class = WEIRegistrationForm
|
form_class = WEIRegistrationForm
|
||||||
|
extra_context = {"title": _("Register first year student to the WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||||
|
@ -485,6 +510,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
form_class = WEIRegistrationForm
|
form_class = WEIRegistrationForm
|
||||||
|
extra_context = {"title": _("Register old student to the WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||||
|
@ -562,6 +588,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||||
"""
|
"""
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
form_class = WEIRegistrationForm
|
form_class = WEIRegistrationForm
|
||||||
|
extra_context = {"title": _("Update WEI Registration")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return WEIRegistration.objects
|
return WEIRegistration.objects
|
||||||
|
@ -651,6 +678,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
|
||||||
Delete a non-validated WEI registration
|
Delete a non-validated WEI registration
|
||||||
"""
|
"""
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
|
extra_context = {"title": _("Delete WEI registration")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
object = self.get_object()
|
object = self.get_object()
|
||||||
|
@ -680,6 +708,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea
|
||||||
"""
|
"""
|
||||||
model = WEIMembership
|
model = WEIMembership
|
||||||
form_class = WEIMembershipForm
|
form_class = WEIMembershipForm
|
||||||
|
extra_context = {"title": _("Validate WEI registration")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||||
|
@ -797,6 +826,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
template_name = "wei/survey.html"
|
template_name = "wei/survey.html"
|
||||||
survey = None
|
survey = None
|
||||||
|
extra_context = {"title": _("Survey WEI")}
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
@ -834,7 +864,6 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["club"] = self.object.wei
|
context["club"] = self.object.wei
|
||||||
context["title"] = _("Survey WEI")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -850,21 +879,21 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||||
|
|
||||||
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
|
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = "wei/survey_end.html"
|
template_name = "wei/survey_end.html"
|
||||||
|
extra_context = {"title": _("Survey WEI")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||||
context["title"] = _("Survey WEI")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WEIClosedView(LoginRequiredMixin, TemplateView):
|
class WEIClosedView(LoginRequiredMixin, TemplateView):
|
||||||
template_name = "wei/survey_closed.html"
|
template_name = "wei/survey_closed.html"
|
||||||
|
extra_context = {"title": _("Survey WEI")}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||||
context["title"] = _("Survey WEI")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.admin import AdminSite
|
||||||
|
from django.contrib.sites.admin import Site, SiteAdmin
|
||||||
|
|
||||||
|
from member.views import CustomLoginView
|
||||||
|
from .middlewares import get_current_session
|
||||||
|
|
||||||
|
|
||||||
|
class StrongAdminSite(AdminSite):
|
||||||
|
def has_permission(self, request):
|
||||||
|
"""
|
||||||
|
Authorize only staff that have the correct permission mask
|
||||||
|
"""
|
||||||
|
session = get_current_session()
|
||||||
|
return request.user.is_active and request.user.is_staff and session.get("permission_mask", -1) >= 42
|
||||||
|
|
||||||
|
def login(self, request, extra_context=None):
|
||||||
|
return CustomLoginView.as_view()(request)
|
||||||
|
|
||||||
|
|
||||||
|
# Instantiate admin site and register some defaults
|
||||||
|
admin_site = StrongAdminSite()
|
||||||
|
admin_site.register(Site, SiteAdmin)
|
|
@ -57,6 +57,8 @@ if "cas_server" in INSTALLED_APPS:
|
||||||
if "logs" in INSTALLED_APPS:
|
if "logs" in INSTALLED_APPS:
|
||||||
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
|
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor']
|
||||||
if "debug_toolbar" in INSTALLED_APPS:
|
if "debug_toolbar" in INSTALLED_APPS:
|
||||||
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
|
@ -61,6 +61,7 @@ INSTALLED_APPS = [
|
||||||
'note',
|
'note',
|
||||||
'permission',
|
'permission',
|
||||||
'registration',
|
'registration',
|
||||||
|
'scripts',
|
||||||
'treasury',
|
'treasury',
|
||||||
'wei',
|
'wei',
|
||||||
]
|
]
|
||||||
|
|
|
@ -25,7 +25,7 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Break it, fix it!
|
# Break it, fix it!
|
||||||
DEBUG = True
|
DEBUG = False
|
||||||
|
|
||||||
# Mandatory !
|
# Mandatory !
|
||||||
ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
|
ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
|
||||||
|
@ -36,11 +36,12 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_USE_SSL = False
|
EMAIL_USE_SSL = False
|
||||||
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org')
|
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org')
|
||||||
EMAIL_PORT = os.getenv('EMAIL_PORT', 443)
|
EMAIL_PORT = os.getenv('EMAIL_PORT', 465)
|
||||||
EMAIL_HOST_USER = os.getenv('EMAIL_USER', 'change_me')
|
EMAIL_HOST_USER = os.getenv('EMAIL_USER', None)
|
||||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', 'change_me')
|
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None)
|
||||||
|
|
||||||
SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com")
|
SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com")
|
||||||
|
DEFAULT_FROM_EMAIL = "NoteKfet2020 <" + SERVER_EMAIL + ">"
|
||||||
|
|
||||||
# Security settings
|
# Security settings
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = False
|
SECURE_CONTENT_TYPE_NOSNIFF = False
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
|
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from member.views import CustomLoginView
|
from member.views import CustomLoginView
|
||||||
|
|
||||||
|
from .admin import admin_site
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Dev so redirect to something random
|
# Dev so redirect to something random
|
||||||
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
|
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
|
||||||
|
@ -25,7 +26,7 @@ urlpatterns = [
|
||||||
# Include Django Contrib and Core routers
|
# Include Django Contrib and Core routers
|
||||||
path('i18n/', include('django.conf.urls.i18n')),
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
path('admin/', admin.site.urls, name="admin"),
|
path('admin/', admin_site.urls, name="admin"),
|
||||||
path('accounts/login/', CustomLoginView.as_view()),
|
path('accounts/login/', CustomLoginView.as_view()),
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
path('api/', include('api.urls')),
|
path('api/', include('api.urls')),
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
django-cas-server==1.1.0
|
django-cas-server==1.2.0
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
Copyright jQuery Foundation and other contributors, https://jquery.org/
|
Copyright JS Foundation and other contributors, https://js.foundation/
|
||||||
|
|
||||||
This software consists of voluntary contributions made by many
|
|
||||||
individuals. For exact contribution history, see the revision history
|
|
||||||
available at https://github.com/jquery/jquery
|
|
||||||
|
|
||||||
====
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
a copy of this software and associated documentation files (the
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#djDebug {
|
||||||
|
display: none !important;
|
||||||
|
}
|
|
@ -0,0 +1,646 @@
|
||||||
|
/* http://www.positioniseverything.net/easyclearing.html */
|
||||||
|
#djDebug .djdt-clearfix:after {
|
||||||
|
content: ".";
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-clearfix {display: inline-block;}
|
||||||
|
/* Hides from IE-mac \*/
|
||||||
|
#djDebug .djdt-clearfix {display: block;}
|
||||||
|
* html #djDebug .djdt-clearfix {height: 1%;}
|
||||||
|
/* end hide from IE-mac */
|
||||||
|
|
||||||
|
/* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */
|
||||||
|
#djDebug {color:#000;background:#FFF;}
|
||||||
|
#djDebug, #djDebug div, #djDebug span, #djDebug applet, #djDebug object, #djDebug iframe,
|
||||||
|
#djDebug h1, #djDebug h2, #djDebug h3, #djDebug h4, #djDebug h5, #djDebug h6, #djDebug p, #djDebug blockquote, #djDebug pre,
|
||||||
|
#djDebug a, #djDebug abbr, #djDebug acronym, #djDebug address, #djDebug big, #djDebug cite, #djDebug code,
|
||||||
|
#djDebug del, #djDebug dfn, #djDebug em, #djDebug font, #djDebug img, #djDebug ins, #djDebug kbd, #djDebug q, #djDebug s, #djDebug samp,
|
||||||
|
#djDebug small, #djDebug strike, #djDebug strong, #djDebug sub, #djDebug sup, #djDebug tt, #djDebug var,
|
||||||
|
#djDebug b, #djDebug u, #djDebug i, #djDebug center,
|
||||||
|
#djDebug dl, #djDebug dt, #djDebug dd, #djDebug ol, #djDebug ul, #djDebug li,
|
||||||
|
#djDebug fieldset, #djDebug form, #djDebug label, #djDebug legend,
|
||||||
|
#djDebug table, #djDebug caption, #djDebug tbody, #djDebug tfoot, #djDebug thead, #djDebug tr, #djDebug th, #djDebug td,
|
||||||
|
#djDebug button {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
min-width:0;
|
||||||
|
width:auto;
|
||||||
|
border:0;
|
||||||
|
outline:0;
|
||||||
|
font-size:12px;
|
||||||
|
line-height:1.5em;
|
||||||
|
color:#000;
|
||||||
|
vertical-align:baseline;
|
||||||
|
background-color:transparent;
|
||||||
|
font-family:sans-serif;
|
||||||
|
text-align:left;
|
||||||
|
text-shadow: none;
|
||||||
|
white-space: normal;
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
-o-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug button, #djDebug a.button {
|
||||||
|
background-color: #eee;
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #eee), color-stop(100%, #cccccc));
|
||||||
|
background-image: -webkit-linear-gradient(top, #eee, #cccccc);
|
||||||
|
background-image: -moz-linear-gradient(top, #eee, #cccccc);
|
||||||
|
background-image: -ms-linear-gradient(top, #eee, #cccccc);
|
||||||
|
background-image: -o-linear-gradient(top, #eee, #cccccc);
|
||||||
|
background-image: linear-gradient(top, #eee, #cccccc);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 1px 0 #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug button:hover, #djDebug a.button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ddd), color-stop(100%, #bbb));
|
||||||
|
background-image: -webkit-linear-gradient(top, #ddd, #bbb);
|
||||||
|
background-image: -moz-linear-gradient(top, #ddd, #bbb);
|
||||||
|
background-image: -ms-linear-gradient(top, #ddd, #bbb);
|
||||||
|
background-image: -o-linear-gradient(top, #ddd, #bbb);
|
||||||
|
background-image: linear-gradient(top, #ddd, #bbb);
|
||||||
|
border-color: #bbb;
|
||||||
|
border-bottom-color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
text-shadow: 0 1px 0 #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug button:active, #djDebug a.button:active {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-bottom: 1px solid #888;
|
||||||
|
-webkit-box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
|
||||||
|
-moz-box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
|
||||||
|
box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar {
|
||||||
|
background-color:#111;
|
||||||
|
width:200px;
|
||||||
|
z-index:100000000;
|
||||||
|
position:fixed;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
opacity:0.9;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar small {
|
||||||
|
color:#999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar ul {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
list-style:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li {
|
||||||
|
border-bottom:1px solid #222;
|
||||||
|
color:#fff;
|
||||||
|
display:block;
|
||||||
|
font-weight:bold;
|
||||||
|
float:none;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
position:relative;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar input[type=checkbox] {
|
||||||
|
float: right;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li>a,
|
||||||
|
#djDebug #djDebugToolbar li>div.djdt-contentless {
|
||||||
|
font-weight:normal;
|
||||||
|
font-style:normal;
|
||||||
|
text-decoration:none;
|
||||||
|
display:block;
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px 10px 5px 25px;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
#djDebug #djDebugToolbar li>div.djdt-disabled {
|
||||||
|
font-style: italic;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li a:hover {
|
||||||
|
color:#111;
|
||||||
|
background-color:#ffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li.djdt-active {
|
||||||
|
background: #333 no-repeat left center;
|
||||||
|
background-image: url(../img/indicator.png);
|
||||||
|
padding-left:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li.djdt-active a:hover {
|
||||||
|
color:#b36a60;
|
||||||
|
background-color:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbar li small {
|
||||||
|
font-size:12px;
|
||||||
|
color:#999;
|
||||||
|
font-style:normal;
|
||||||
|
text-decoration:none;
|
||||||
|
font-variant:small-caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djDebugToolbarHandle {
|
||||||
|
position:fixed;
|
||||||
|
background-color:#fff;
|
||||||
|
border:1px solid #111;
|
||||||
|
top:30px;
|
||||||
|
right:0;
|
||||||
|
z-index:100000000;
|
||||||
|
opacity:0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djShowToolBarButton {
|
||||||
|
display:block;
|
||||||
|
height:75px;
|
||||||
|
width:30px;
|
||||||
|
border-right:none;
|
||||||
|
border-bottom:4px solid #fff;
|
||||||
|
border-top:4px solid #fff;
|
||||||
|
border-left:4px solid #fff;
|
||||||
|
color:#fff;
|
||||||
|
font-size:10px;
|
||||||
|
font-weight:bold;
|
||||||
|
text-decoration:none;
|
||||||
|
text-align:center;
|
||||||
|
text-indent:-999999px;
|
||||||
|
background: #000 no-repeat left center;
|
||||||
|
background-image: url(../img/djdt_vertical.png);
|
||||||
|
opacity:0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug #djShowToolBarButton:hover {
|
||||||
|
background-color:#111;
|
||||||
|
border-top-color:#FFE761;
|
||||||
|
border-left-color:#FFE761;
|
||||||
|
border-bottom-color:#FFE761;
|
||||||
|
cursor:move;
|
||||||
|
opacity:1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug code {
|
||||||
|
display:block;
|
||||||
|
font-family:Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space:pre;
|
||||||
|
overflow:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugOdd {
|
||||||
|
background-color:#f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent {
|
||||||
|
display:none;
|
||||||
|
position:fixed;
|
||||||
|
margin:0;
|
||||||
|
top:0;
|
||||||
|
right:200px;
|
||||||
|
bottom:0;
|
||||||
|
left:0px;
|
||||||
|
background-color:#eee;
|
||||||
|
color:#666;
|
||||||
|
z-index:100000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent > div {
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugPanelTitle {
|
||||||
|
position:absolute;
|
||||||
|
background-color:#ffc;
|
||||||
|
color:#666;
|
||||||
|
padding-left:20px;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
left:0;
|
||||||
|
height:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugPanelTitle code {
|
||||||
|
display:inline;
|
||||||
|
font-size:inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugPanelContent {
|
||||||
|
position:absolute;
|
||||||
|
top:50px;
|
||||||
|
right:0;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
height:auto;
|
||||||
|
padding:5px 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugPanelContent .djdt-loader {
|
||||||
|
display:block;
|
||||||
|
margin:80px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugPanelContent .djdt-scroll {
|
||||||
|
height:100%;
|
||||||
|
overflow:auto;
|
||||||
|
display:block;
|
||||||
|
padding:0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug h3 {
|
||||||
|
font-size:24px;
|
||||||
|
font-weight:normal;
|
||||||
|
line-height:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug h4 {
|
||||||
|
font-size:20px;
|
||||||
|
font-weight:bold;
|
||||||
|
margin-top:0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent table {
|
||||||
|
border:1px solid #ccc;
|
||||||
|
border-collapse:collapse;
|
||||||
|
width:100%;
|
||||||
|
background-color:#fff;
|
||||||
|
display:table;
|
||||||
|
margin-top:0.8em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent tbody td,
|
||||||
|
#djDebug .djdt-panelContent tbody th {
|
||||||
|
vertical-align:top;
|
||||||
|
padding:2px 3px;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent tbody td.djdt-time {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent thead th {
|
||||||
|
padding:1px 6px 1px 3px;
|
||||||
|
text-align:left;
|
||||||
|
font-weight:bold;
|
||||||
|
font-size:14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent tbody th {
|
||||||
|
width:12em;
|
||||||
|
text-align:right;
|
||||||
|
color:#666;
|
||||||
|
padding-right:.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djTemplateContext {
|
||||||
|
background-color:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#djDebug .djdt-panelContent p a:hover, #djDebug .djdt-panelContent dd a:hover {
|
||||||
|
color:#111;
|
||||||
|
background-color:#ffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent p {
|
||||||
|
padding:0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent p, #djDebug .djdt-panelContent table, #djDebug .djdt-panelContent ol, #djDebug .djdt-panelContent ul, #djDebug .djdt-panelContent dl {
|
||||||
|
margin:5px 0 15px;
|
||||||
|
background-color:#fff;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent table {
|
||||||
|
clear:both;
|
||||||
|
border:0;
|
||||||
|
padding:0;
|
||||||
|
margin:0;
|
||||||
|
border-collapse:collapse;
|
||||||
|
border-spacing:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent table a {
|
||||||
|
color:#000;
|
||||||
|
padding:2px 4px;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent table a:hover {
|
||||||
|
background-color:#ffc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent table th {
|
||||||
|
background-color:#333;
|
||||||
|
font-weight:bold;
|
||||||
|
color:#fff;
|
||||||
|
padding:3px 7px 3px;
|
||||||
|
text-align:left;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent table td {
|
||||||
|
padding:5px 10px;
|
||||||
|
font-size:14px;
|
||||||
|
background-color:#fff;
|
||||||
|
color:#000;
|
||||||
|
vertical-align:top;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent table tr.djDebugOdd td {
|
||||||
|
background-color:#eee;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent .djDebugClose {
|
||||||
|
display:block;
|
||||||
|
position:absolute;
|
||||||
|
top:4px;
|
||||||
|
right:15px;
|
||||||
|
height:40px;
|
||||||
|
width:40px;
|
||||||
|
background: no-repeat center center;
|
||||||
|
background-image: url(../img/close.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent .djDebugClose:hover {
|
||||||
|
background-image: url(../img/close_hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent .djDebugClose.djDebugBack {
|
||||||
|
background-image: url(../img/back.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent .djDebugClose.djDebugBack:hover {
|
||||||
|
background-image: url(../img/back_hover.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent dt, #djDebug .djdt-panelContent dd {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent dt {
|
||||||
|
margin-top:0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent dd {
|
||||||
|
margin-left:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug a.toggleTemplate {
|
||||||
|
padding:4px;
|
||||||
|
background-color:#bbb;
|
||||||
|
-webkit-border-radius:3px;
|
||||||
|
-moz-border-radius:3px;
|
||||||
|
border-radius:3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug a.toggleTemplate:hover {
|
||||||
|
padding:4px;
|
||||||
|
background-color:#444;
|
||||||
|
color:#ffe761;
|
||||||
|
-webkit-border-radius:3px;
|
||||||
|
-moz-border-radius:3px;
|
||||||
|
border-radius:3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#djDebug .djDebugSqlWrap {
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugCollapsed {
|
||||||
|
display: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugUncollapsed {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djUnselected {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#djDebug tr.djHiddenByDefault {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#djDebug tr.djSelected {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugSql {
|
||||||
|
word-break:break-word;
|
||||||
|
z-index:100000002;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djSQLDetailsDiv tbody th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djSqlExplain td {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug span.djDebugLineChart {
|
||||||
|
background-color:#777;
|
||||||
|
height:3px;
|
||||||
|
position:absolute;
|
||||||
|
bottom:0;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
display:block;
|
||||||
|
z-index:1000000001;
|
||||||
|
}
|
||||||
|
#djDebug span.djDebugLineChartWarning {
|
||||||
|
background-color:#900;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .highlight { color:#000; }
|
||||||
|
#djDebug .highlight .err { color:#000; } /* Error */
|
||||||
|
#djDebug .highlight .g { color:#000; } /* Generic */
|
||||||
|
#djDebug .highlight .k { color:#000; font-weight:bold } /* Keyword */
|
||||||
|
#djDebug .highlight .o { color:#000; } /* Operator */
|
||||||
|
#djDebug .highlight .n { color:#000; } /* Name */
|
||||||
|
#djDebug .highlight .mi { color:#000; font-weight:bold } /* Literal.Number.Integer */
|
||||||
|
#djDebug .highlight .l { color:#000; } /* Literal */
|
||||||
|
#djDebug .highlight .x { color:#000; } /* Other */
|
||||||
|
#djDebug .highlight .p { color:#000; } /* Punctuation */
|
||||||
|
#djDebug .highlight .m { color:#000; font-weight:bold } /* Literal.Number */
|
||||||
|
#djDebug .highlight .s { color:#333 } /* Literal.String */
|
||||||
|
#djDebug .highlight .w { color:#888888 } /* Text.Whitespace */
|
||||||
|
#djDebug .highlight .il { color:#000; font-weight:bold } /* Literal.Number.Integer.Long */
|
||||||
|
#djDebug .highlight .na { color:#333 } /* Name.Attribute */
|
||||||
|
#djDebug .highlight .nt { color:#000; font-weight:bold } /* Name.Tag */
|
||||||
|
#djDebug .highlight .nv { color:#333 } /* Name.Variable */
|
||||||
|
#djDebug .highlight .s2 { color:#333 } /* Literal.String.Double */
|
||||||
|
#djDebug .highlight .cp { color:#333 } /* Comment.Preproc */
|
||||||
|
|
||||||
|
#djDebug .djdt-timeline {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
#djDebug .djDebugTimeline {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
#djDebug div.djDebugLineChart {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
#djDebug div.djDebugLineChart strong {
|
||||||
|
text-indent: -10000em;
|
||||||
|
display: block;
|
||||||
|
font-weight: normal;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-color:#ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug div.djDebugLineChartWarning strong {
|
||||||
|
background-color:#900;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djDebugInTransaction div.djDebugLineChart strong {
|
||||||
|
background-color: #d3ff82;
|
||||||
|
}
|
||||||
|
#djDebug .djDebugStartTransaction div.djDebugLineChart strong {
|
||||||
|
border-left: 1px solid #94b24d;
|
||||||
|
}
|
||||||
|
#djDebug .djDebugEndTransaction div.djDebugLineChart strong {
|
||||||
|
border-right: 1px solid #94b24d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent ul.djdt-stats {
|
||||||
|
position: relative;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent ul.djdt-stats li {
|
||||||
|
width: 30%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent ul.djdt-stats li strong.djdt-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent ul.djdt-stats li span.djdt-color {
|
||||||
|
height: 12px;
|
||||||
|
width: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent ul.djdt-stats li span.djdt-info {
|
||||||
|
display: block;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-panelContent thead th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#djDebug .djDebugRowWarning .djdt-time {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#djdebug .djdt-panelContent table .djdt-toggle {
|
||||||
|
width: 14px;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-panelContent table .djdt-actions {
|
||||||
|
min-width: 70px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#djdebug .djdt-panelContent table .djdt-color {
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
#djdebug .djdt-panelContent table .djdt-color span {
|
||||||
|
width: 3px;
|
||||||
|
height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#djDebug .djToggleSwitch {
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #999;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #777;
|
||||||
|
display: inline-block;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFF', endColorstr='#DCDCDC'); /* for IE */
|
||||||
|
background: -webkit-gradient(linear, left top, left bottom, from(#FFF), to(#DCDCDC)); /* for webkit browsers */
|
||||||
|
background:-moz-linear-gradient(center top , #FFFFFF 0pt, #DCDCDC 100%) repeat scroll 0 0 transparent;
|
||||||
|
}
|
||||||
|
#djDebug .djNoToggleSwitch {
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djSQLDetailsDiv {
|
||||||
|
margin-top:0.8em;
|
||||||
|
}
|
||||||
|
#djDebug pre {
|
||||||
|
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||||
|
white-space: -pre-wrap; /* Opera 4-6 */
|
||||||
|
white-space: -o-pre-wrap; /* Opera 7 */
|
||||||
|
white-space: pre-wrap; /* CSS-3 */
|
||||||
|
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||||
|
color: #555;
|
||||||
|
border:1px solid #ccc;
|
||||||
|
border-collapse:collapse;
|
||||||
|
background-color:#fff;
|
||||||
|
display:block;
|
||||||
|
overflow: auto;
|
||||||
|
padding:2px 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-family:Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-stack span {
|
||||||
|
color: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-stack span.djdt-path,
|
||||||
|
#djDebug .djdt-stack pre.djdt-locals,
|
||||||
|
#djDebug .djdt-stack pre.djdt-locals span {
|
||||||
|
color: #777;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-stack span.djdt-code {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-stack pre.djdt-locals {
|
||||||
|
margin: 0 27px 27px 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#djDebug .djdt-width-20 {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-width-60 {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
#djDebug .djdt-highlighted {
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
.djdt-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
After Width: | Height: | Size: 404 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 613 B |
After Width: | Height: | Size: 498 B |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 882 B |
After Width: | Height: | Size: 436 B |
|
@ -0,0 +1 @@
|
||||||
|
document.getElementById('redirect_to').focus();
|
|
@ -0,0 +1,324 @@
|
||||||
|
(function () {
|
||||||
|
var $$ = {
|
||||||
|
on: function(root, eventName, selector, fn) {
|
||||||
|
root.addEventListener(eventName, function(event) {
|
||||||
|
var target = event.target.closest(selector);
|
||||||
|
if (root.contains(target)) {
|
||||||
|
fn.call(target, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
show: function(element) {
|
||||||
|
element.style.display = 'block';
|
||||||
|
},
|
||||||
|
hide: function(element) {
|
||||||
|
element.style.display = 'none';
|
||||||
|
},
|
||||||
|
toggle: function(element, value) {
|
||||||
|
if (value) {
|
||||||
|
$$.show(element);
|
||||||
|
} else {
|
||||||
|
$$.hide(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visible: function(element) {
|
||||||
|
style = getComputedStyle(element);
|
||||||
|
return style.display !== 'none';
|
||||||
|
},
|
||||||
|
executeScripts: function(root) {
|
||||||
|
root.querySelectorAll('script').forEach(function(e) {
|
||||||
|
var clone = document.createElement('script');
|
||||||
|
clone.src = e.src;
|
||||||
|
root.appendChild(clone);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var onKeyDown = function(event) {
|
||||||
|
if (event.keyCode == 27) {
|
||||||
|
djdt.hide_one_level();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ajax = function(url, init) {
|
||||||
|
init = Object.assign({credentials: 'same-origin'}, init);
|
||||||
|
return fetch(url, init).then(function(response) {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.text();
|
||||||
|
} else {
|
||||||
|
var win = document.querySelector('#djDebugWindow');
|
||||||
|
win.innerHTML = '<div class="djDebugPanelTitle"><a class="djDebugClose djDebugBack" href=""></a><h3>'+response.status+': '+response.statusText+'</h3></div>';
|
||||||
|
$$.show(win);
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var djdt = {
|
||||||
|
handleDragged: false,
|
||||||
|
events: {
|
||||||
|
ready: []
|
||||||
|
},
|
||||||
|
isReady: false,
|
||||||
|
init: function() {
|
||||||
|
var djDebug = document.querySelector('#djDebug');
|
||||||
|
$$.show(djDebug);
|
||||||
|
$$.on(djDebug.querySelector('#djDebugPanelList'), 'click', 'li a', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this.className) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var current = djDebug.querySelector('#' + this.className);
|
||||||
|
if ($$.visible(current)) {
|
||||||
|
djdt.hide_panels();
|
||||||
|
} else {
|
||||||
|
djdt.hide_panels();
|
||||||
|
|
||||||
|
$$.show(current);
|
||||||
|
this.parentElement.classList.add('djdt-active');
|
||||||
|
|
||||||
|
var inner = current.querySelector('.djDebugPanelContent .djdt-scroll'),
|
||||||
|
store_id = djDebug.dataset.storeId;
|
||||||
|
if (store_id && inner.children.length === 0) {
|
||||||
|
var url = djDebug.dataset.renderPanelUrl;
|
||||||
|
var url_params = new URLSearchParams();
|
||||||
|
url_params.append('store_id', store_id);
|
||||||
|
url_params.append('panel_id', this.className);
|
||||||
|
url += '?' + url_params.toString();
|
||||||
|
ajax(url).then(function(body) {
|
||||||
|
inner.previousElementSibling.remove(); // Remove AJAX loader
|
||||||
|
inner.innerHTML = body;
|
||||||
|
$$.executeScripts(inner);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$$.on(djDebug, 'click', 'a.djDebugClose', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
djdt.hide_one_level();
|
||||||
|
});
|
||||||
|
$$.on(djDebug, 'click', '.djDebugPanelButton input[type=checkbox]', function() {
|
||||||
|
djdt.cookie.set(this.dataset.cookie, this.checked ? 'on' : 'off', {
|
||||||
|
path: '/',
|
||||||
|
expires: 10
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Used by the SQL and template panels
|
||||||
|
$$.on(djDebug, 'click', '.remoteCall', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var name = this.tagName.toLowerCase();
|
||||||
|
var ajax_data = {};
|
||||||
|
|
||||||
|
if (name == 'button') {
|
||||||
|
var form = this.closest('form');
|
||||||
|
ajax_data.url = this.getAttribute('formaction');
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
ajax_data.body = new FormData(form);
|
||||||
|
ajax_data.method = form.getAttribute('method') || 'POST';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == 'a') {
|
||||||
|
ajax_data.url = this.getAttribute('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
ajax(ajax_data.url, ajax_data).then(function(body) {
|
||||||
|
var win = djDebug.querySelector('#djDebugWindow');
|
||||||
|
win.innerHTML = body;
|
||||||
|
$$.executeScripts(win);
|
||||||
|
$$.show(win);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Used by the cache, profiling and SQL panels
|
||||||
|
$$.on(djDebug, 'click', 'a.djToggleSwitch', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var self = this;
|
||||||
|
var id = this.dataset.toggleId;
|
||||||
|
var open_me = this.textContent == this.dataset.toggleOpen;
|
||||||
|
if (id === '' || !id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var name = this.dataset.toggleName;
|
||||||
|
var container = this.closest('.djDebugPanelContent').querySelector('#' + name + '_' + id);
|
||||||
|
container.querySelectorAll('.djDebugCollapsed').forEach(function(e) {
|
||||||
|
$$.toggle(e, open_me);
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.djDebugUncollapsed').forEach(function(e) {
|
||||||
|
$$.toggle(e, !open_me);
|
||||||
|
});
|
||||||
|
this.closest('.djDebugPanelContent').querySelectorAll('.djToggleDetails_' + id).forEach(function(e) {
|
||||||
|
if (open_me) {
|
||||||
|
e.classList.add('djSelected');
|
||||||
|
e.classList.remove('djUnselected');
|
||||||
|
self.textContent = self.dataset.toggleClose;
|
||||||
|
} else {
|
||||||
|
e.classList.remove('djSelected');
|
||||||
|
e.classList.add('djUnselected');
|
||||||
|
self.textContent = self.dataset.toggleOpen;
|
||||||
|
}
|
||||||
|
var switch_ = e.querySelector('.djToggleSwitch')
|
||||||
|
if (switch_) switch_.textContent = self.textContent;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
djDebug.querySelector('#djHideToolBarButton').addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
djdt.hide_toolbar(true);
|
||||||
|
});
|
||||||
|
djDebug.querySelector('#djShowToolBarButton').addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!djdt.handleDragged) {
|
||||||
|
djdt.show_toolbar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var startPageY, baseY;
|
||||||
|
var handle = document.querySelector('#djDebugToolbarHandle');
|
||||||
|
var onHandleMove = function(event) {
|
||||||
|
// Chrome can send spurious mousemove events, so don't do anything unless the
|
||||||
|
// cursor really moved. Otherwise, it will be impossible to expand the toolbar
|
||||||
|
// due to djdt.handleDragged being set to true.
|
||||||
|
if (djdt.handleDragged || event.pageY != startPageY) {
|
||||||
|
var top = baseY + event.pageY;
|
||||||
|
|
||||||
|
if (top < 0) {
|
||||||
|
top = 0;
|
||||||
|
} else if (top + handle.offsetHeight > window.innerHeight) {
|
||||||
|
top = window.innerHeight - handle.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.style.top = top + 'px';
|
||||||
|
djdt.handleDragged = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
djDebug.querySelector('#djShowToolBarButton').addEventListener('mousedown', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
startPageY = event.pageY;
|
||||||
|
baseY = handle.offsetTop - startPageY;
|
||||||
|
document.addEventListener('mousemove', onHandleMove);
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', function (event) {
|
||||||
|
document.removeEventListener('mousemove', onHandleMove);
|
||||||
|
if (djdt.handleDragged) {
|
||||||
|
event.preventDefault();
|
||||||
|
djdt.cookie.set('djdttop', handle.offsetTop, {
|
||||||
|
path: '/',
|
||||||
|
expires: 10
|
||||||
|
});
|
||||||
|
setTimeout(function () {
|
||||||
|
djdt.handleDragged = false;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (djdt.cookie.get('djdt') == 'hide') {
|
||||||
|
djdt.hide_toolbar(false);
|
||||||
|
} else {
|
||||||
|
djdt.show_toolbar();
|
||||||
|
}
|
||||||
|
djdt.isReady = true;
|
||||||
|
djdt.events.ready.forEach(function(callback) {
|
||||||
|
callback(djdt);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hide_panels: function() {
|
||||||
|
$$.hide(djDebug.querySelector('#djDebugWindow'));
|
||||||
|
djDebug.querySelectorAll('.djdt-panelContent').forEach(function(e) {
|
||||||
|
$$.hide(e);
|
||||||
|
});
|
||||||
|
djDebug.querySelectorAll('#djDebugToolbar li').forEach(function(e) {
|
||||||
|
e.classList.remove('djdt-active');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hide_toolbar: function(setCookie) {
|
||||||
|
djdt.hide_panels();
|
||||||
|
$$.hide(djDebug.querySelector('#djDebugToolbar'));
|
||||||
|
|
||||||
|
var handle = document.querySelector('#djDebugToolbarHandle');
|
||||||
|
$$.show(handle);
|
||||||
|
// set handle position
|
||||||
|
var handleTop = djdt.cookie.get('djdttop');
|
||||||
|
if (handleTop) {
|
||||||
|
handleTop = Math.min(handleTop, window.innerHeight - handle.offsetHeight);
|
||||||
|
handle.style.top = handleTop + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
if (setCookie) {
|
||||||
|
djdt.cookie.set('djdt', 'hide', {
|
||||||
|
path: '/',
|
||||||
|
expires: 10
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hide_one_level: function(skipDebugWindow) {
|
||||||
|
if ($$.visible(djDebug.querySelector('#djDebugWindow'))) {
|
||||||
|
$$.hide(djDebug.querySelector('#djDebugWindow'));
|
||||||
|
} else if (djDebug.querySelector('#djDebugToolbar li.djdt-active')) {
|
||||||
|
djdt.hide_panels();
|
||||||
|
} else {
|
||||||
|
djdt.hide_toolbar(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
show_toolbar: function() {
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
$$.hide(djDebug.querySelector('#djDebugToolbarHandle'));
|
||||||
|
$$.show(djDebug.querySelector('#djDebugToolbar'));
|
||||||
|
djdt.cookie.set('djdt', 'show', {
|
||||||
|
path: '/',
|
||||||
|
expires: 10
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ready: function(callback){
|
||||||
|
if (djdt.isReady) {
|
||||||
|
callback(djdt);
|
||||||
|
} else {
|
||||||
|
djdt.events.ready.push(callback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cookie: {
|
||||||
|
get: function(key){
|
||||||
|
if (document.cookie.indexOf(key) === -1) return null;
|
||||||
|
|
||||||
|
var cookieArray = document.cookie.split('; '),
|
||||||
|
cookies = {};
|
||||||
|
|
||||||
|
cookieArray.forEach(function(e){
|
||||||
|
var parts = e.split('=');
|
||||||
|
cookies[ parts[0] ] = parts[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
return cookies[ key ];
|
||||||
|
},
|
||||||
|
set: function(key, value, options){
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
if (typeof options.expires === 'number') {
|
||||||
|
var days = options.expires, t = options.expires = new Date();
|
||||||
|
t.setDate(t.getDate() + days);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = [
|
||||||
|
encodeURIComponent(key) + '=' + String(value),
|
||||||
|
options.expires ? '; expires=' + options.expires.toUTCString() : '',
|
||||||
|
options.path ? '; path=' + options.path : '',
|
||||||
|
options.domain ? '; domain=' + options.domain : '',
|
||||||
|
options.secure ? '; secure' : ''
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.djdt = {
|
||||||
|
show_toolbar: djdt.show_toolbar,
|
||||||
|
hide_toolbar: djdt.hide_toolbar,
|
||||||
|
init: djdt.init,
|
||||||
|
close: djdt.hide_one_level,
|
||||||
|
cookie: djdt.cookie,
|
||||||
|
};
|
||||||
|
document.addEventListener('DOMContentLoaded', djdt.init);
|
||||||
|
})();
|
|
@ -0,0 +1,52 @@
|
||||||
|
(function () {
|
||||||
|
// Browser timing remains hidden unless we can successfully access the performance object
|
||||||
|
var perf = window.performance || window.msPerformance ||
|
||||||
|
window.webkitPerformance || window.mozPerformance;
|
||||||
|
if (!perf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var rowCount = 0,
|
||||||
|
timingOffset = perf.timing.navigationStart,
|
||||||
|
timingEnd = perf.timing.loadEventEnd,
|
||||||
|
totalTime = timingEnd - timingOffset;
|
||||||
|
function getLeft(stat) {
|
||||||
|
return ((perf.timing[stat] - timingOffset) / (totalTime)) * 100.0;
|
||||||
|
}
|
||||||
|
function getCSSWidth(stat, endStat) {
|
||||||
|
var width = ((perf.timing[endStat] - perf.timing[stat]) / (totalTime)) * 100.0;
|
||||||
|
// Calculate relative percent (same as sql panel logic)
|
||||||
|
width = 100.0 * width / (100.0 - getLeft(stat));
|
||||||
|
return (width < 1) ? "2px" : width + "%";
|
||||||
|
}
|
||||||
|
function addRow(stat, endStat) {
|
||||||
|
rowCount++;
|
||||||
|
var row = document.createElement('tr');
|
||||||
|
row.className = (rowCount % 2) ? 'djDebugOdd' : 'djDebugEven';
|
||||||
|
if (endStat) {
|
||||||
|
// Render a start through end bar
|
||||||
|
row.innerHTML = '<td>' + stat.replace('Start', '') + '</td>' +
|
||||||
|
'<td class="djdt-timeline"><div class="djDebugTimeline"><div class="djDebugLineChart"><strong> </strong></div></div></td>' +
|
||||||
|
'<td>' + (perf.timing[stat] - timingOffset) + ' (+' + (perf.timing[endStat] - perf.timing[stat]) + ')</td>';
|
||||||
|
row.querySelector('strong').style.width = getCSSWidth(stat, endStat);
|
||||||
|
} else {
|
||||||
|
// Render a point in time
|
||||||
|
row.innerHTML = '<td>' + stat + '</td>' +
|
||||||
|
'<td class="djdt-timeline"><div class="djDebugTimeline"><div class="djDebugLineChart"><strong> </strong></div></div></td>' +
|
||||||
|
'<td>' + (perf.timing[stat] - timingOffset) + '</td>';
|
||||||
|
row.querySelector('strong').style.width = '2px';
|
||||||
|
}
|
||||||
|
row.querySelector('.djDebugLineChart').style.left = getLeft(stat) + '%';
|
||||||
|
document.querySelector('#djDebugBrowserTimingTableBody').appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param)
|
||||||
|
addRow('domainLookupStart', 'domainLookupEnd');
|
||||||
|
addRow('connectStart', 'connectEnd');
|
||||||
|
addRow('requestStart', 'responseEnd'); // There is no requestEnd
|
||||||
|
addRow('responseStart', 'responseEnd');
|
||||||
|
addRow('domLoading', 'domComplete'); // Spans the events below
|
||||||
|
addRow('domInteractive');
|
||||||
|
addRow('domContentLoadedEventStart', 'domContentLoadedEventEnd');
|
||||||
|
addRow('loadEventStart', 'loadEventEnd');
|
||||||
|
document.querySelector('#djDebugBrowserTiming').classList.remove('djdt-hidden');
|
||||||
|
})();
|
|
@ -13,7 +13,7 @@
|
||||||
"note": note_id
|
"note": note_id
|
||||||
}
|
}
|
||||||
).done(function(){
|
).done(function(){
|
||||||
$("#alias_table").load(location.href+ " #alias_table");
|
$("#alias_table").load(location.pathname+ " #alias_table");
|
||||||
addMsg("Alias ajouté","success");
|
addMsg("Alias ajouté","success");
|
||||||
})
|
})
|
||||||
.fail(function(xhr, textStatus, error){
|
.fail(function(xhr, textStatus, error){
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
})
|
})
|
||||||
.done(function(){
|
.done(function(){
|
||||||
addMsg('Alias supprimé','success');
|
addMsg('Alias supprimé','success');
|
||||||
$("#alias_table").load(location.href + " #alias_table");
|
$("#alias_table").load(location.pathname + " #alias_table");
|
||||||
})
|
})
|
||||||
.fail(function(xhr,textStatus, error){
|
.fail(function(xhr,textStatus, error){
|
||||||
errMsg(xhr.responseJSON);
|
errMsg(xhr.responseJSON);
|
||||||
|
|
|
@ -95,6 +95,8 @@ function li(id, text, extra_css) {
|
||||||
* @param note The concerned note.
|
* @param note The concerned note.
|
||||||
*/
|
*/
|
||||||
function displayStyle(note) {
|
function displayStyle(note) {
|
||||||
|
if (!note)
|
||||||
|
return "";
|
||||||
let balance = note.balance;
|
let balance = note.balance;
|
||||||
var css = "";
|
var css = "";
|
||||||
if (balance < -5000)
|
if (balance < -5000)
|
||||||
|
@ -130,7 +132,6 @@ function displayNote(note, alias, user_note_field = null, profile_pic_field = nu
|
||||||
if (profile_pic_field != null) {
|
if (profile_pic_field != null) {
|
||||||
$("#" + profile_pic_field).attr('src', img);
|
$("#" + profile_pic_field).attr('src', img);
|
||||||
$("#" + profile_pic_field).click(function () {
|
$("#" + profile_pic_field).click(function () {
|
||||||
console.log(note);
|
|
||||||
if (note.resourcetype === "NoteUser") {
|
if (note.resourcetype === "NoteUser") {
|
||||||
document.location.href = "/accounts/user/" + note.user;
|
document.location.href = "/accounts/user/" + note.user;
|
||||||
} else if (note.resourcetype === "NoteClub") {
|
} else if (note.resourcetype === "NoteClub") {
|
||||||
|
@ -275,7 +276,6 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
|
||||||
field.attr('data-original-title', aliases_matched_html).tooltip('show');
|
field.attr('data-original-title', aliases_matched_html).tooltip('show');
|
||||||
|
|
||||||
consumers.results.forEach(function (consumer) {
|
consumers.results.forEach(function (consumer) {
|
||||||
let note = consumer.note;
|
|
||||||
let consumer_obj = $("#" + alias_prefix + "_" + consumer.id);
|
let consumer_obj = $("#" + alias_prefix + "_" + consumer.id);
|
||||||
consumer_obj.hover(function () {
|
consumer_obj.hover(function () {
|
||||||
displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
|
displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field)
|
||||||
|
@ -283,8 +283,8 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
|
||||||
consumer_obj.click(function () {
|
consumer_obj.click(function () {
|
||||||
var disp = null;
|
var disp = null;
|
||||||
notes_display.forEach(function (d) {
|
notes_display.forEach(function (d) {
|
||||||
// We compare the note ids
|
// We compare the alias ids
|
||||||
if (d.id === note.id) {
|
if (d.id === consumer.id) {
|
||||||
d.quantity += 1;
|
d.quantity += 1;
|
||||||
disp = d;
|
disp = d;
|
||||||
}
|
}
|
||||||
|
@ -294,7 +294,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
|
||||||
disp = {
|
disp = {
|
||||||
name: consumer.name,
|
name: consumer.name,
|
||||||
id: consumer.id,
|
id: consumer.id,
|
||||||
note: note,
|
note: consumer.note,
|
||||||
quantity: 1
|
quantity: 1
|
||||||
};
|
};
|
||||||
notes_display.push(disp);
|
notes_display.push(disp);
|
||||||
|
@ -343,7 +343,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
|
||||||
// When a validate button is clicked, we switch the validation status
|
// When a validate button is clicked, we switch the validation status
|
||||||
function de_validate(id, validated) {
|
function de_validate(id, validated) {
|
||||||
let invalidity_reason = $("#invalidity_reason_" + id).val();
|
let invalidity_reason = $("#invalidity_reason_" + id).val();
|
||||||
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
|
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳</strong>");
|
||||||
|
|
||||||
// Perform a PATCH request to the API in order to update the transaction
|
// Perform a PATCH request to the API in order to update the transaction
|
||||||
// If the user has insufficient rights, an error message will appear
|
// If the user has insufficient rights, an error message will appear
|
||||||
|
@ -369,7 +369,7 @@ function de_validate(id, validated) {
|
||||||
},
|
},
|
||||||
error: function (err) {
|
error: function (err) {
|
||||||
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
|
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
|
||||||
"de cette transaction : " + err.responseText, "danger");
|
"de cette transaction : " + JSON.parse(err.responseText)["detail"], "danger", 10000);
|
||||||
|
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
// error if this method doesn't exist. Please define it.
|
// error if this method doesn't exist. Please define it.
|
||||||
|
|
|
@ -156,7 +156,7 @@ function reset() {
|
||||||
function consumeAll() {
|
function consumeAll() {
|
||||||
notes_display.forEach(function(note_display) {
|
notes_display.forEach(function(note_display) {
|
||||||
buttons.forEach(function(button) {
|
buttons.forEach(function(button) {
|
||||||
consume(note_display.note.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
|
consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
|
||||||
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
|
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -164,7 +164,7 @@ function consumeAll() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new transaction from a button through the API.
|
* Create a new transaction from a button through the API.
|
||||||
* @param source The note that paid the item (type: int)
|
* @param source The note that paid the item (type: note)
|
||||||
* @param source_alias The alias used for the source (type: str)
|
* @param source_alias The alias used for the source (type: str)
|
||||||
* @param dest The note that sold the item (type: int)
|
* @param dest The note that sold the item (type: int)
|
||||||
* @param quantity The quantity sold (type: int)
|
* @param quantity The quantity sold (type: int)
|
||||||
|
@ -184,12 +184,24 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"polymorphic_ctype": type,
|
"polymorphic_ctype": type,
|
||||||
"resourcetype": "RecurrentTransaction",
|
"resourcetype": "RecurrentTransaction",
|
||||||
"source": source,
|
"source": source.id,
|
||||||
"source_alias": source_alias,
|
"source_alias": source_alias,
|
||||||
"destination": dest,
|
"destination": dest,
|
||||||
"category": category,
|
"category": category,
|
||||||
"template": template
|
"template": template
|
||||||
}, reset).fail(function (e) {
|
})
|
||||||
|
.done(function () {
|
||||||
|
if (!isNaN(source.balance)) {
|
||||||
|
let newBalance = source.balance - quantity * amount;
|
||||||
|
if (newBalance <= -5000)
|
||||||
|
addMsg("Attention, la note émettrice " + source_alias + " passe en négatif sévère.",
|
||||||
|
"danger", 10000);
|
||||||
|
else if (newBalance < 0)
|
||||||
|
addMsg("Attention, la note émettrice " + source_alias + " passe en négatif.",
|
||||||
|
"warning", 10000);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
}).fail(function (e) {
|
||||||
$.post("/api/note/transaction/transaction/",
|
$.post("/api/note/transaction/transaction/",
|
||||||
{
|
{
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
"csrfmiddlewaretoken": CSRF_TOKEN,
|
||||||
|
@ -207,10 +219,10 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
|
||||||
"template": template
|
"template": template
|
||||||
}).done(function() {
|
}).done(function() {
|
||||||
reset();
|
reset();
|
||||||
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger");
|
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000);
|
||||||
}).fail(function () {
|
}).fail(function () {
|
||||||
reset();
|
reset();
|
||||||
errMsg(e.responseJSON);
|
errMsg(e.responseJSON, 10000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,23 +7,39 @@ function refreshHistory() {
|
||||||
$("#history").load("/note/transfer/ #history");
|
$("#history").load("/note/transfer/ #history");
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset(refresh=true) {
|
||||||
sources_notes_display.length = 0;
|
sources_notes_display.length = 0;
|
||||||
sources.length = 0;
|
sources.length = 0;
|
||||||
dests_notes_display.length = 0;
|
dests_notes_display.length = 0;
|
||||||
dests.length = 0;
|
dests.length = 0;
|
||||||
$("#source_note_list").html("");
|
$("#source_note_list").html("");
|
||||||
$("#dest_note_list").html("");
|
$("#dest_note_list").html("");
|
||||||
$("#amount").val("");
|
let source_field = $("#source_note");
|
||||||
$("#reason").val("");
|
source_field.val("");
|
||||||
|
source_field.trigger("keyup");
|
||||||
|
source_field.removeClass('is-invalid');
|
||||||
|
let dest_field = $("#dest_note");
|
||||||
|
dest_field.val("");
|
||||||
|
dest_field.trigger("keyup");
|
||||||
|
dest_field.removeClass('is-invalid');
|
||||||
|
let amount_field = $("#amount");
|
||||||
|
amount_field.val("");
|
||||||
|
amount_field.removeClass('is-invalid');
|
||||||
|
$("#amount-required").html("");
|
||||||
|
let reason_field = $("#reason");
|
||||||
|
reason_field.val("");
|
||||||
|
reason_field.removeClass('is-invalid');
|
||||||
|
$("#reason-required").html("");
|
||||||
$("#last_name").val("");
|
$("#last_name").val("");
|
||||||
$("#first_name").val("");
|
$("#first_name").val("");
|
||||||
$("#bank").val("");
|
$("#bank").val("");
|
||||||
$("#user_note").val("");
|
$("#user_note").val("");
|
||||||
$("#profile_pic").attr("src", "/media/pic/default.png");
|
$("#profile_pic").attr("src", "/media/pic/default.png");
|
||||||
|
if (refresh) {
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
refreshHistory();
|
refreshHistory();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
/**
|
/**
|
||||||
|
@ -71,17 +87,10 @@ $(document).ready(function() {
|
||||||
let source = $("#source_note");
|
let source = $("#source_note");
|
||||||
let dest = $("#dest_note");
|
let dest = $("#dest_note");
|
||||||
|
|
||||||
$("#type_gift").click(function() {
|
|
||||||
$("#special_transaction_div").addClass('d-none');
|
|
||||||
source.attr('disabled', true);
|
|
||||||
source.val(username);
|
|
||||||
source.tooltip('hide');
|
|
||||||
$("#source_note_list").addClass('d-none');
|
|
||||||
dest.attr('disabled', false);
|
|
||||||
$("#dest_note_list").removeClass('d-none');
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#type_transfer").click(function() {
|
$("#type_transfer").click(function() {
|
||||||
|
$("#source_me_div").removeClass('d-none');
|
||||||
|
$("#source_note").removeClass('is-invalid');
|
||||||
|
$("#dest_note").removeClass('is-invalid');
|
||||||
$("#special_transaction_div").addClass('d-none');
|
$("#special_transaction_div").addClass('d-none');
|
||||||
source.attr('disabled', false);
|
source.attr('disabled', false);
|
||||||
$("#source_note_list").removeClass('d-none');
|
$("#source_note_list").removeClass('d-none');
|
||||||
|
@ -90,6 +99,9 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_credit").click(function() {
|
$("#type_credit").click(function() {
|
||||||
|
$("#source_me_div").addClass('d-none');
|
||||||
|
$("#source_note").removeClass('is-invalid');
|
||||||
|
$("#dest_note").removeClass('is-invalid');
|
||||||
$("#special_transaction_div").removeClass('d-none');
|
$("#special_transaction_div").removeClass('d-none');
|
||||||
$("#source_note_list").addClass('d-none');
|
$("#source_note_list").addClass('d-none');
|
||||||
$("#dest_note_list").removeClass('d-none');
|
$("#dest_note_list").removeClass('d-none');
|
||||||
|
@ -107,6 +119,9 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_debit").click(function() {
|
$("#type_debit").click(function() {
|
||||||
|
$("#source_me_div").addClass('d-none');
|
||||||
|
$("#source_note").removeClass('is-invalid');
|
||||||
|
$("#dest_note").removeClass('is-invalid');
|
||||||
$("#special_transaction_div").removeClass('d-none');
|
$("#special_transaction_div").removeClass('d-none');
|
||||||
$("#source_note_list").removeClass('d-none');
|
$("#source_note_list").removeClass('d-none');
|
||||||
$("#dest_note_list").addClass('d-none');
|
$("#dest_note_list").addClass('d-none');
|
||||||
|
@ -131,13 +146,11 @@ $(document).ready(function() {
|
||||||
dest.val(type);
|
dest.val(type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
|
// Ensure we begin in transfer mode. Removing these lines may cause problems when reloading.
|
||||||
let type_gift = $("#type_gift"); // Default mode
|
let type_transfer = $("#type_transfer"); // Default mode
|
||||||
type_gift.removeAttr('checked');
|
type_transfer.removeAttr('checked');
|
||||||
$("#type_transfer").removeAttr('checked');
|
|
||||||
$("#type_credit").removeAttr('checked');
|
$("#type_credit").removeAttr('checked');
|
||||||
$("#type_debit").removeAttr('checked');
|
$("#type_debit").removeAttr('checked');
|
||||||
$("label[for='type_gift']").attr('class', 'btn btn-sm btn-outline-primary');
|
|
||||||
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
|
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
|
||||||
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
|
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
|
||||||
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
|
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
|
||||||
|
@ -145,90 +158,120 @@ $(document).ready(function() {
|
||||||
if (location.hash)
|
if (location.hash)
|
||||||
$("#type_" + location.hash.substr(1)).click();
|
$("#type_" + location.hash.substr(1)).click();
|
||||||
else
|
else
|
||||||
type_gift.click();
|
type_transfer.click();
|
||||||
location.hash = "";
|
location.hash = "";
|
||||||
|
|
||||||
|
$("#source_me").click(function() {
|
||||||
|
// Shortcut to set the current user as the only emitter
|
||||||
|
sources_notes_display.length = 0;
|
||||||
|
sources.length = 0;
|
||||||
|
$("#source_note_list").html("");
|
||||||
|
|
||||||
|
let source_note = $("#source_note");
|
||||||
|
source_note.focus();
|
||||||
|
source_note.val("");
|
||||||
|
let event = jQuery.Event("keyup");
|
||||||
|
event.originalEvent = {charCode: 97};
|
||||||
|
source_note.trigger(event);
|
||||||
|
source_note.val(username);
|
||||||
|
event = jQuery.Event("keyup");
|
||||||
|
event.originalEvent = {charCode: 97};
|
||||||
|
source_note.trigger(event);
|
||||||
|
let fill_note = function() {
|
||||||
|
if (sources.length === 0) {
|
||||||
|
setTimeout(fill_note, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event = jQuery.Event("keypress");
|
||||||
|
event.originalEvent = {charCode: 13};
|
||||||
|
source_note.trigger(event);
|
||||||
|
|
||||||
|
source_note.tooltip('hide');
|
||||||
|
source_note.val('');
|
||||||
|
$("#dest_note").focus();
|
||||||
|
};
|
||||||
|
fill_note();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn_transfer").click(function() {
|
$("#btn_transfer").click(function() {
|
||||||
if ($("#type_gift").is(':checked')) {
|
let error = false;
|
||||||
dests_notes_display.forEach(function (dest) {
|
|
||||||
|
let amount_field = $("#amount");
|
||||||
|
amount_field.removeClass('is-invalid');
|
||||||
|
$("#amount-required").html("");
|
||||||
|
|
||||||
|
let reason_field = $("#reason");
|
||||||
|
reason_field.removeClass('is-invalid');
|
||||||
|
$("#reason-required").html("");
|
||||||
|
|
||||||
|
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
|
||||||
|
amount_field.addClass('is-invalid');
|
||||||
|
$("#amount-required").html("<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>");
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reason_field.val()) {
|
||||||
|
reason_field.addClass('is-invalid');
|
||||||
|
$("#reason-required").html("<strong>Ce champ est requis.</strong>");
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let amount = 100 * amount_field.val();
|
||||||
|
let reason = reason_field.val();
|
||||||
|
|
||||||
|
if ($("#type_transfer").is(':checked')) {
|
||||||
|
// We copy the arrays to ensure that transactions are well-processed even if the form is reset
|
||||||
|
[...sources_notes_display].forEach(function (source) {
|
||||||
|
[...dests_notes_display].forEach(function (dest) {
|
||||||
$.post("/api/note/transaction/transaction/",
|
$.post("/api/note/transaction/transaction/",
|
||||||
{
|
{
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
"csrfmiddlewaretoken": CSRF_TOKEN,
|
||||||
"quantity": dest.quantity,
|
"quantity": source.quantity * dest.quantity,
|
||||||
"amount": 100 * $("#amount").val(),
|
"amount": amount,
|
||||||
"reason": $("#reason").val(),
|
"reason": reason,
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
||||||
"resourcetype": "Transaction",
|
"resourcetype": "Transaction",
|
||||||
"source": user_id,
|
"source": source.note.id,
|
||||||
|
"source_alias": source.name,
|
||||||
"destination": dest.note.id,
|
"destination": dest.note.id,
|
||||||
"destination_alias": dest.name
|
"destination_alias": dest.name
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
|
if (!isNaN(source.note.balance)) {
|
||||||
|
let newBalance = source.note.balance - source.quantity * dest.quantity * amount;
|
||||||
|
if (newBalance <= -5000) {
|
||||||
addMsg("Le transfert de "
|
addMsg("Le transfert de "
|
||||||
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
|
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
|
||||||
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
|
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
|
||||||
|
"mais la note émettrice passe en négatif sévère.", "danger", 10000);
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (newBalance < 0) {
|
||||||
|
addMsg("Le transfert de "
|
||||||
|
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note "
|
||||||
|
+ source.name + " vers la note " + dest.name + " a été fait avec succès, " +
|
||||||
|
"mais la note émettrice passe en négatif.", "warning", 10000);
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addMsg("Le transfert de "
|
||||||
|
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
|
||||||
|
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
}).fail(function () { // do it again but valid = false
|
}).fail(function () { // do it again but valid = false
|
||||||
$.post("/api/note/transaction/transaction/",
|
|
||||||
{
|
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
|
||||||
"quantity": dest.quantity,
|
|
||||||
"amount": 100 * $("#amount").val(),
|
|
||||||
"reason": $("#reason").val(),
|
|
||||||
"valid": false,
|
|
||||||
"invalidity_reason": "Solde insuffisant",
|
|
||||||
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
|
||||||
"resourcetype": "Transaction",
|
|
||||||
"source": user_id,
|
|
||||||
"destination": dest.note.id,
|
|
||||||
"destination_alias": dest.name
|
|
||||||
}).done(function () {
|
|
||||||
addMsg("Le transfert de "
|
|
||||||
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
|
|
||||||
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger");
|
|
||||||
|
|
||||||
reset();
|
|
||||||
}).fail(function (err) {
|
|
||||||
addMsg("Le transfert de "
|
|
||||||
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
|
|
||||||
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
|
|
||||||
|
|
||||||
reset();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if ($("#type_transfer").is(':checked')) {
|
|
||||||
sources_notes_display.forEach(function (source) {
|
|
||||||
dests_notes_display.forEach(function (dest) {
|
|
||||||
$.post("/api/note/transaction/transaction/",
|
$.post("/api/note/transaction/transaction/",
|
||||||
{
|
{
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
"csrfmiddlewaretoken": CSRF_TOKEN,
|
||||||
"quantity": source.quantity * dest.quantity,
|
"quantity": source.quantity * dest.quantity,
|
||||||
"amount": 100 * $("#amount").val(),
|
"amount": amount,
|
||||||
"reason": $("#reason").val(),
|
"reason": reason,
|
||||||
"valid": true,
|
|
||||||
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
|
||||||
"resourcetype": "Transaction",
|
|
||||||
"source": source.note.id,
|
|
||||||
"source_alias": source.name,
|
|
||||||
"destination": dest.note.id,
|
|
||||||
"destination_alias": dest.name
|
|
||||||
}).done(function () {
|
|
||||||
addMsg("Le transfert de "
|
|
||||||
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
|
|
||||||
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
|
|
||||||
|
|
||||||
reset();
|
|
||||||
}).fail(function (err) { // do it again but valid = false
|
|
||||||
$.post("/api/note/transaction/transaction/",
|
|
||||||
{
|
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
|
||||||
"quantity": source.quantity * dest.quantity,
|
|
||||||
"amount": 100 * $("#amount").val(),
|
|
||||||
"reason": $("#reason").val(),
|
|
||||||
"valid": false,
|
"valid": false,
|
||||||
"invalidity_reason": "Solde insuffisant",
|
"invalidity_reason": "Solde insuffisant",
|
||||||
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
|
||||||
|
@ -239,16 +282,12 @@ $("#btn_transfer").click(function() {
|
||||||
"destination_alias": dest.name
|
"destination_alias": dest.name
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg("Le transfert de "
|
addMsg("Le transfert de "
|
||||||
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
|
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
|
||||||
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger");
|
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
|
||||||
|
|
||||||
reset();
|
|
||||||
}).fail(function (err) {
|
}).fail(function (err) {
|
||||||
addMsg("Le transfert de "
|
addMsg("Le transfert de "
|
||||||
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
|
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
|
||||||
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
|
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
|
||||||
|
|
||||||
reset();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -256,20 +295,30 @@ $("#btn_transfer").click(function() {
|
||||||
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
|
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
|
||||||
let special_note = $("#credit_type").val();
|
let special_note = $("#credit_type").val();
|
||||||
let user_note;
|
let user_note;
|
||||||
let given_reason = $("#reason").val();
|
let given_reason = reason;
|
||||||
let source, dest, reason;
|
let source_id, dest_id;
|
||||||
if ($("#type_credit").is(':checked')) {
|
if ($("#type_credit").is(':checked')) {
|
||||||
|
if (!dests_notes_display.length) {
|
||||||
|
$("#dest_note").addClass('is-invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
user_note = dests_notes_display[0].note.id;
|
user_note = dests_notes_display[0].note.id;
|
||||||
source = special_note;
|
source_id = special_note;
|
||||||
dest = user_note;
|
dest_id = user_note;
|
||||||
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
|
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
|
||||||
if (given_reason.length > 0)
|
if (given_reason.length > 0)
|
||||||
reason += " (" + given_reason + ")";
|
reason += " (" + given_reason + ")";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (!sources_notes_display.length) {
|
||||||
|
$("#source_note").addClass('is-invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
user_note = sources_notes_display[0].note.id;
|
user_note = sources_notes_display[0].note.id;
|
||||||
source = user_note;
|
source_id = user_note;
|
||||||
dest = special_note;
|
dest_id = special_note;
|
||||||
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
|
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
|
||||||
if (given_reason.length > 0)
|
if (given_reason.length > 0)
|
||||||
reason += " (" + given_reason + ")";
|
reason += " (" + given_reason + ")";
|
||||||
|
@ -278,24 +327,24 @@ $("#btn_transfer").click(function() {
|
||||||
{
|
{
|
||||||
"csrfmiddlewaretoken": CSRF_TOKEN,
|
"csrfmiddlewaretoken": CSRF_TOKEN,
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"amount": 100 * $("#amount").val(),
|
"amount": amount,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
|
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
|
||||||
"resourcetype": "SpecialTransaction",
|
"resourcetype": "SpecialTransaction",
|
||||||
"source": source,
|
"source": source_id,
|
||||||
"source_alias": source.name,
|
"source_alias": sources_notes_display[0].name,
|
||||||
"destination": dest,
|
"destination": dest_id,
|
||||||
"destination_alias": dest.name,
|
"destination_alias": dests_notes_display[0].name,
|
||||||
"last_name": $("#last_name").val(),
|
"last_name": $("#last_name").val(),
|
||||||
"first_name": $("#first_name").val(),
|
"first_name": $("#first_name").val(),
|
||||||
"bank": $("#bank").val()
|
"bank": $("#bank").val()
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg("Le crédit/retrait a bien été effectué !", "success");
|
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
|
||||||
reset();
|
reset();
|
||||||
}).fail(function (err) {
|
}).fail(function (err) {
|
||||||
addMsg("Le crédit/retrait a échoué : " + err.responseText, "danger");
|
addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"],
|
||||||
reset();
|
"danger", 10000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
})
|
})
|
||||||
.done(function() {
|
.done(function() {
|
||||||
addMsg('Invité supprimé','success');
|
addMsg('Invité supprimé','success');
|
||||||
$("#guests_table").load(location.href + " #guests_table");
|
$("#guests_table").load(location.pathname + " #guests_table");
|
||||||
})
|
})
|
||||||
.fail(function(xhr, textStatus, error) {
|
.fail(function(xhr, textStatus, error) {
|
||||||
errMsg(xhr.responseJSON);
|
errMsg(xhr.responseJSON);
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
if ((pattern === old_pattern || pattern === "") && !force)
|
if ((pattern === old_pattern || pattern === "") && !force)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
|
$("#entry_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% block extracss %}{% endblock %}
|
{% block extracss %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex w-100 h-100 flex-column">
|
<body class="d-flex w-100 h-100 flex-column">
|
||||||
|
{% if debug %}<div style="background:#ffeeba;text-align:center">Mode DEBUG activé.</div>{% endif %}
|
||||||
<main class="mb-auto">
|
<main class="mb-auto">
|
||||||
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
|
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
|
||||||
<a class="navbar-brand" href="/">{{ request.site.name }}</a>
|
<a class="navbar-brand" href="/">{{ request.site.name }}</a>
|
||||||
|
@ -98,7 +99,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a>
|
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "auth.user"|model_list|length >= 2 %}
|
{% if "auth.user"|model_list_length >= 2 %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a>
|
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -108,13 +109,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a>
|
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "member.change_profile_registration_valid"|has_perm:user %}
|
|
||||||
<li class="nav-item active">
|
|
||||||
<a class="nav-link" href="{% url 'registration:future_user_list' %}">
|
|
||||||
<i class="fas fa-user-plus"></i> {% trans "Registrations" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if "activity.activity"|not_empty_model_list %}
|
{% if "activity.activity"|not_empty_model_list %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a>
|
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a>
|
||||||
|
@ -130,12 +124,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a>
|
<a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
|
<a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% if user.is_staff %}
|
{% endif %}
|
||||||
|
{% if user.is_staff and ""|has_perm:user %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Administration' %}</a>
|
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Admin' %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -178,6 +174,17 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
|
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
|
||||||
|
<div class="alert alert-warning alert-dismissible">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
Attention : la Note Kfet 2020 est en phase de beta. Des fonctionnalités pourront être rajoutées d'ici à la version
|
||||||
|
finale, et des bugs peuvent survenir. Pour tout problème, merci d'envoyer un mail à l'adresse
|
||||||
|
<a href="mailto:notekfet2020@lists.crans.org">
|
||||||
|
notekfet2020@lists.crans.org</a>,
|
||||||
|
ou bien levez une issue sur le dépôt <a href="https://gitlab.crans.org/bde/nk20/-/issues">Gitlab</a>,
|
||||||
|
ou encore posez un commentaire sur le <a href="https://pad.crans.org/p/todoNK20">pad</a>.<br><br>
|
||||||
|
|
||||||
|
Certaines données ont été anonymisées afin de limiter les fuites de données, et peuvent ne pas correspondre avec vos données réelles.
|
||||||
|
</div>
|
||||||
<div id="messages"></div>
|
<div id="messages"></div>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Default content...</p>
|
<p>Default content...</p>
|
||||||
|
|
|
@ -4,9 +4,6 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center mb-4">
|
<div class="row justify-content-center mb-4">
|
||||||
<div class="col-md-10 text-center">
|
<div class="col-md-10 text-center">
|
||||||
<h4>
|
|
||||||
{% trans "search clubs" %}
|
|
||||||
</h4>
|
|
||||||
<input class="form-control mx-auto w-25" type="text" id="search_field"/>
|
<input class="form-control mx-auto w-25" type="text" id="search_field"/>
|
||||||
<hr>
|
<hr>
|
||||||
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
|
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
|
||||||
|
@ -36,7 +33,7 @@
|
||||||
|
|
||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
let pattern = searchbar_obj.val();
|
let pattern = searchbar_obj.val();
|
||||||
$("#club_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #club_table", init);
|
$("#club_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #club_table", init);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchbar_obj.keyup(function() {
|
searchbar_obj.keyup(function() {
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
{% extends "member/noteowner_detail.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block profile_info %}
|
||||||
|
{% include "member/club_info.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block profile_content %}
|
||||||
|
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label" for="only_active">
|
||||||
|
<input type="checkbox" class="checkboxinput form-check-input" id="only_active"
|
||||||
|
{% if only_active %}checked{% endif %}>
|
||||||
|
{% trans "Display only active memberships" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="div_id_roles" class="form-group">
|
||||||
|
<label for="id_roles" class="col-form-label">{% trans "Filter roles:" %}</label>
|
||||||
|
<div class="">
|
||||||
|
<select name="roles" class="selectmultiple form-control" id="roles" multiple="">
|
||||||
|
{% for role in applicable_roles %}
|
||||||
|
<option value="{{ role.id }}" selected>{{ role.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div id="memberships_table">
|
||||||
|
{% if table.data %}
|
||||||
|
{% render_table table %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans "There is no membership found with this pattern." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function () {
|
||||||
|
let searchbar_obj = $("#searchbar");
|
||||||
|
let only_active_obj = $("#only_active");
|
||||||
|
let roles_obj = $("#roles");
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
let pattern = searchbar_obj.val();
|
||||||
|
|
||||||
|
let roles = [];
|
||||||
|
$("#roles option:selected").each(function() {
|
||||||
|
roles.push($(this).val());
|
||||||
|
});
|
||||||
|
let roles_str = roles.join(',');
|
||||||
|
|
||||||
|
$("#memberships_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20")
|
||||||
|
+ "&only_active=" + (only_active_obj.is(':checked') ? '1' : '0')
|
||||||
|
+ "&roles=" + roles_str + " #memberships_table");
|
||||||
|
}
|
||||||
|
|
||||||
|
searchbar_obj.keyup(reloadTable);
|
||||||
|
only_active_obj.change(reloadTable);
|
||||||
|
roles_obj.change(reloadTable);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -1,10 +1,23 @@
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% if managers.data %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
|
<a class="font-weight-bold">
|
||||||
|
<i class="fa fa-users"></i> {% trans "Club managers" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% render_table managers %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if member_list.data %}
|
{% if member_list.data %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="btn btn-link stretched-link font-weight-bold">
|
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
|
||||||
<i class="fa fa-users"></i> {% trans "Member of the Club" %}
|
<i class="fa fa-users"></i> {% trans "Club members" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table member_list %}
|
{% render_table member_list %}
|
||||||
|
@ -16,7 +29,7 @@
|
||||||
{% if history_list.data %}
|
{% if history_list.data %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="btn btn-link stretched-link font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="btn btn-link stretched-link font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-users"></i> {% trans "View my memberships" %}
|
<i class="fa fa-users"></i> {% trans "View my memberships" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="btn btn-link stretched-link collapsed font-weight-bold"
|
<a class="collapsed font-weight-bold"
|
||||||
data-toggle="collapse" data-target="#historyListCollapse"
|
data-toggle="collapse" data-target="#historyListCollapse"
|
||||||
aria-expanded="true" aria-controls="historyListCollapse">
|
aria-expanded="true" aria-controls="historyListCollapse">
|
||||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load perms %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
|
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
|
||||||
|
@ -18,6 +19,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{% if "member.change_profile_registration_valid"|has_perm:user %}
|
||||||
|
<a class="btn btn-block btn-secondary" href="{% url 'registration:future_user_list' %}">
|
||||||
|
<i class="fas fa-user-plus"></i> {% trans "Registrations" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
@ -34,7 +42,7 @@
|
||||||
if (pattern === old_pattern || pattern === "")
|
if (pattern === old_pattern || pattern === "")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
|
$("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchbar_obj.keyup(function() {
|
searchbar_obj.keyup(function() {
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text">€</span>
|
<span class="input-group-text">€</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="amount-required" class="invalid-feedback"></p>
|
||||||
</div>
|
</div>
|
|
@ -68,15 +68,15 @@
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<p class="card-text font-weight-bold">
|
<p class="card-text font-weight-bold">
|
||||||
{% trans "Most used buttons" %}
|
{% trans "Highlighted buttons" %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-nowrap" style="overflow:auto hidden">
|
<div class="card-body text-nowrap" style="overflow:auto hidden">
|
||||||
<div class="d-inline-flex flex-wrap justify-content-center" id="most_used">
|
<div class="d-inline-flex flex-wrap justify-content-center" id="highlighted">
|
||||||
{% for button in most_used %}
|
{% for button in highlighted %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
<button class="btn btn-outline-dark rounded-0 flex-fill"
|
<button class="btn btn-outline-dark rounded-0 flex-fill"
|
||||||
id="most_used_button{{ button.id }}" name="button" value="{{ button.name }}">
|
id="highlighted_button{{ button.id }}" name="button" value="{{ button.name }}">
|
||||||
{{ button.name }} ({{ button.amount | pretty_money }})
|
{{ button.name }} ({{ button.amount | pretty_money }})
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Regroup buttons under categories #}
|
{# Regroup buttons under categories #}
|
||||||
{% regroup transaction_templates by category as categories %}
|
{# {% regroup transaction_templates by category as categories %} #}
|
||||||
|
|
||||||
<div class="card border-primary text-center shadow mb-4">
|
<div class="card border-primary text-center shadow mb-4">
|
||||||
{# Tabs for button categories #}
|
{# Tabs for button categories #}
|
||||||
|
@ -94,8 +94,8 @@
|
||||||
<ul class="nav nav-tabs nav-fill card-header-tabs">
|
<ul class="nav nav-tabs nav-fill card-header-tabs">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.grouper|slugify }}">
|
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.name|slugify }}">
|
||||||
{{ category.grouper }}
|
{{ category.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -106,9 +106,9 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<div class="tab-pane" id="{{ category.grouper|slugify }}">
|
<div class="tab-pane" id="{{ category.name|slugify }}">
|
||||||
<div class="d-inline-flex flex-wrap justify-content-center">
|
<div class="d-inline-flex flex-wrap justify-content-center">
|
||||||
{% for button in category.list %}
|
{% for button in category.templates_filtered %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
<button class="btn btn-outline-dark rounded-0 flex-fill"
|
<button class="btn btn-outline-dark rounded-0 flex-fill"
|
||||||
id="button{{ button.id }}" name="button" value="{{ button.name }}">
|
id="button{{ button.id }}" name="button" value="{{ button.name }}">
|
||||||
|
@ -157,24 +157,26 @@
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script type="text/javascript" src="{% static "js/consos.js" %}"></script>
|
<script type="text/javascript" src="{% static "js/consos.js" %}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% for button in most_used %}
|
{% for button in highlighted %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
$("#most_used_button{{ button.id }}").click(function() {
|
$("#highlighted_button{{ button.id }}").click(function() {
|
||||||
addConso({{ button.destination.id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for button in transaction_templates %}
|
{% for category in categories %}
|
||||||
|
{% for button in category.templates_filtered %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
$("#button{{ button.id }}").click(function() {
|
$("#button{{ button.id }}").click(function() {
|
||||||
addConso({{ button.destination.id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -10,11 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
||||||
<label for="type_gift" class="btn btn-sm btn-outline-primary active">
|
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
|
||||||
<input type="radio" name="transaction_type" id="type_gift" checked>
|
|
||||||
{% trans "Gift" %}
|
|
||||||
</label>
|
|
||||||
<label for="type_transfer" class="btn btn-sm btn-outline-primary">
|
|
||||||
<input type="radio" name="transaction_type" id="type_transfer">
|
<input type="radio" name="transaction_type" id="type_transfer">
|
||||||
{% trans "Transfer" %}
|
{% trans "Transfer" %}
|
||||||
</label>
|
</label>
|
||||||
|
@ -59,6 +55,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
</ul>
|
</ul>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<input class="form-control mx-auto d-block" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" />
|
<input class="form-control mx-auto d-block" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" />
|
||||||
|
<div id="source_me_div">
|
||||||
|
<hr>
|
||||||
|
<span class="form-control mx-auto d-block btn btn-secondary" id="source_me">
|
||||||
|
{% trans "I am the emitter" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -100,7 +102,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label for="reason">{% trans "Reason" %} :</label>
|
<label for="reason">{% trans "Reason" %} :</label>
|
||||||
<input class="form-control mx-auto d-block" type="text" id="reason" required />
|
<input class="form-control mx-auto d-block" type="text" id="reason" />
|
||||||
|
<p id="reason-required" class="invalid-feedback"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -160,7 +163,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
|
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
|
||||||
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
|
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
|
||||||
user_id = {{ user.note.pk }};
|
user_id = {{ user.note.pk }};
|
||||||
username = "{{ user.username }}";
|
username = "{{ user.username|escapejs }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/transfer.js"></script>
|
<script src="/static/js/transfer.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -5,18 +5,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center mb-4">
|
<div class="row justify-content-center mb-4">
|
||||||
<div class="col-md-10 text-center">
|
<div class="col-md-10 text-center">
|
||||||
<h4>
|
|
||||||
{% trans "Search button" %}
|
|
||||||
</h4>
|
|
||||||
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}">
|
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}">
|
||||||
<div class="form-group">
|
|
||||||
<div id="div_active_only" class="form-check">
|
|
||||||
<label for="active_only" class="form-check-label">
|
|
||||||
<input type="checkbox" name="active_only" class="checkboxinput form-check-input" checked="" id="active_only">
|
|
||||||
{% trans "Display visible buttons only" %}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
<hr>
|
||||||
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a>
|
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,45 +25,25 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script>
|
<script type="text/javascript">
|
||||||
/* fonction appelée à la fin du timer */
|
$(document).ready(function() {
|
||||||
function getInfo() {
|
let searchbar_obj = $("#search_field");
|
||||||
var asked = $("#search_field").val();
|
var timer_on = false;
|
||||||
/* on ne fait la requête que si on a au moins un caractère pour chercher */
|
var timer;
|
||||||
if (asked.length >= 1) {
|
|
||||||
$.getJSON("/api/note/transaction/template/?format=json&search=" + asked + ($("#active_only").is(":checked") ? "&display=true" : ""), function(buttons) {
|
function reloadTable() {
|
||||||
console.log(buttons);
|
let pattern = searchbar_obj.val();
|
||||||
let selected_id = buttons.results.map((a => "#row-" + a.id));
|
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table");
|
||||||
console.log(".table-row " + selected_id.join());
|
|
||||||
$(".table-row " + selected_id.join()).removeClass('d-none');
|
|
||||||
$(".table-row").not(selected_id.join()).addClass('d-none');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ($("#active_only").is(":checked")) {
|
|
||||||
$('.table-success').removeClass('d-none');
|
|
||||||
$('.table-danger').addClass('d-none');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// show everything
|
|
||||||
$('table tr').removeClass('d-none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var timer;
|
searchbar_obj.keyup(function() {
|
||||||
var timer_on;
|
if (timer_on)
|
||||||
/* Fontion appelée quand le texte change (délenche le timer) */
|
|
||||||
function search_field_moved() {
|
|
||||||
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
|
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = setTimeout(getInfo, 300);
|
|
||||||
}
|
|
||||||
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
|
|
||||||
timer = setTimeout(getInfo, 300);
|
|
||||||
timer_on = true;
|
timer_on = true;
|
||||||
}
|
setTimeout(reloadTable, 0);
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// on click of button "delete" , call the API
|
// on click of button "delete" , call the API
|
||||||
function delete_button(button_id) {
|
function delete_button(button_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -84,18 +53,11 @@
|
||||||
})
|
})
|
||||||
.done(function() {
|
.done(function() {
|
||||||
addMsg('{% trans "button successfully deleted "%}','success');
|
addMsg('{% trans "button successfully deleted "%}','success');
|
||||||
$("#buttons_table").load("{% url 'note:template_list' %} #buttons_table");
|
$("#buttons_table").load(location.pathname + "?search=" + $("#search_field").val().replace(" ", "%20") + " #buttons_table");
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
$("#search_field").keyup(search_field_moved);
|
|
||||||
$("#active_only").change(search_field_moved);
|
|
||||||
|
|
||||||
search_field_moved();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,15 +15,15 @@
|
||||||
{% regroup active_memberships by roles as memberships_per_role %}
|
{% regroup active_memberships by roles as memberships_per_role %}
|
||||||
{% for role in roles %}
|
{% for role in roles %}
|
||||||
<li class="{% if not role.clubs %}no-club{% endif %}">
|
<li class="{% if not role.clubs %}no-club{% endif %}">
|
||||||
{{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %}
|
{{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %} {% if role.for_club %}(<em>Pour le club {{ role.for_club }} uniquement</em>){% endif %}
|
||||||
{% if role.clubs %}
|
{% if role.clubs %}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
{% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }}
|
{% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for permission in role.permissions.permissions.all %}
|
{% for permission in role.permissions.all %}
|
||||||
<li data-toggle="tooltip" title="{% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.type }} {{ permission.model }}{% if permission.permanent %}, {% trans "permanent" %}{% endif %})</li>
|
<li data-toggle="tooltip" title="{% trans "Mask:" %} {{ permission.mask }}, {% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.get_type_display }} {{ permission.model }}{% if permission.permanent %}, {% trans "permanent" %}{% endif %})</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<em>{% trans "No associated permission" %}</em>
|
<em>{% trans "No associated permission" %}</em>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
if (pattern === old_pattern || pattern === "")
|
if (pattern === old_pattern || pattern === "")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
|
$("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
|
||||||
|
|
||||||
$(".table-row").click(function() {
|
$(".table-row").click(function() {
|
||||||
window.document.location = $(this).data("href");
|
window.document.location = $(this).data("href");
|
||||||
|
|
|
@ -10,12 +10,22 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p class="errornote">
|
<p class="errornote">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed with username=request.user.username %}
|
||||||
You are authenticated as {{ username }}, but are not authorized to
|
You are authenticated as {{ username }}, but are not authorized to
|
||||||
access this page. Would you like to login to a different account?
|
access this page. Would you like to login to a different account,
|
||||||
|
or with a higher permission mask?
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if request.resolver_match.view_name == 'admin:login' %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You must be logged with a staff account with the higher mask to access Django Admin.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
|
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
|
||||||
{{ form | crispy }}
|
{{ form | crispy }}
|
||||||
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
|
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
|
||||||
|
|
|
@ -55,5 +55,11 @@
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<h2>{% trans "Closed remittances" %}</h2>
|
<h2>{% trans "Closed remittances" %}</h2>
|
||||||
|
{% if closed_remittances.data %}
|
||||||
{% render_table closed_remittances %}
|
{% render_table closed_remittances %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans "There is no closed remittance yet." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{% if teams.data %}
|
{% if teams.data %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="btn btn-link stretched-link font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
{% if memberships.data %}
|
{% if memberships.data %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="btn btn-link stretched-link font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Members" %}
|
<i class="fa fa-bus"></i> {% trans "Members" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|