mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-03 22:32:51 +02:00
Compare commits
100 Commits
9930c48253
...
svg_icons
Author | SHA1 | Date | |
---|---|---|---|
466cbd9878 | |||
0bd447b608 | |||
3f3c93d928 | |||
340c90f5d3 | |||
a05dfcbf3d
|
|||
ba3c0fb18d
|
|||
ab69963ea1 | |||
654c01631a
|
|||
d94cc2a7ad
|
|||
69bb38297f
|
|||
9628560d64
|
|||
df3bb71357
|
|||
2a216fd994
|
|||
8dd2619013
|
|||
62431a4910
|
|||
946bc1e497 | |||
d4896bfd76
|
|||
23f46cc598
|
|||
d1a9f21b56 | |||
d809b2595a
|
|||
97803ac983 | |||
b951c4aa05 | |||
69b3d2ac9c
|
|||
f29054558a
|
|||
11dd8adbb7 | |||
d437f2bdbd
|
|||
ac8453b04c
|
|||
6b4d18f4b3 | |||
668cfa71a7 | |||
161db0b00b
|
|||
8638c16b34
|
|||
9583cec3ff
|
|||
1ef25924a0
|
|||
e89383e3f4
|
|||
79a116d9c6
|
|||
aa75ce5c7a
|
|||
a3a9dfc812
|
|||
76531595ad
|
|||
a0b920ac94
|
|||
ab2e580e68
|
|||
0234f19a33
|
|||
1a4b7c83e8
|
|||
4c17e2a92b
|
|||
e68afc7d0a
|
|||
c6e3b54f94
|
|||
7e6a14296a | |||
780f78b385 | |||
4e3c32eb5e
|
|||
ef118c2445
|
|||
600ba15faa
|
|||
944bb127e2
|
|||
f6d042c998
|
|||
bb9a0a2593
|
|||
61feac13c7
|
|||
81e708a7e3
|
|||
3532846c87
|
|||
49551e88f8
|
|||
db936bf75a
|
|||
5828a20383 | |||
cea3138daf | |||
fb98d9cd8b
|
|||
0dd3da5c01
|
|||
af4be98b5b
|
|||
be6059eba6
|
|||
5793b83de7
|
|||
2c02c747f4
|
|||
a78f3b7caa
|
|||
1ee40cb94e
|
|||
bd035744a4
|
|||
7edd622755
|
|||
8fd5b6ee01
|
|||
03411ac9bd
|
|||
d965732b65
|
|||
048266ed61
|
|||
b27341009e
|
|||
da1e15c5e6
|
|||
4b03a78ad6
|
|||
fb6e3c3de0
|
|||
391f3bde8f
|
|||
ad04e45992
|
|||
4e1ba1447a
|
|||
b646f549d6
|
|||
ba9ef0371a
|
|||
881cd88f48
|
|||
b4ed354b73 | |||
e5051ab018
|
|||
bb69627ac5
|
|||
ffaa020310
|
|||
6d2b7054e2
|
|||
d888d5863a
|
|||
dbc7b3444b
|
|||
f25eb1d2c5
|
|||
a2a749e1ca
|
|||
5bf6a5501d
|
|||
9523b5f05f
|
|||
5eb3ffca66 | |||
789ca149af | |||
08ba0b263a | |||
4583958f50 | |||
bab394908d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,6 +47,7 @@ backups/
|
|||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
shell.nix
|
||||||
|
|
||||||
# ansibles customs host
|
# ansibles customs host
|
||||||
ansible/host_vars/*.yaml
|
ansible/host_vars/*.yaml
|
||||||
|
@ -12,7 +12,7 @@ RUN apt-get update && \
|
|||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||||
python3-bs4 python3-setuptools \
|
python3-bs4 python3-setuptools \
|
||||||
uwsgi uwsgi-plugin-python3 \
|
uwsgi uwsgi-plugin-python3 \
|
||||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
|
texlive-xetex gettext libjs-bootstrap4 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Instal PyPI requirements
|
# Instal PyPI requirements
|
||||||
|
@ -23,7 +23,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
|||||||
$ sudo apt update
|
$ sudo apt update
|
||||||
$ sudo apt install --no-install-recommends -y \
|
$ sudo apt install --no-install-recommends -y \
|
||||||
ipython3 python3-setuptools python3-venv python3-dev \
|
ipython3 python3-setuptools python3-venv python3-dev \
|
||||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
|
texlive-xetex gettext libjs-bootstrap4 git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Clonage du dépot** là où vous voulez :
|
2. **Clonage du dépot** là où vous voulez :
|
||||||
@ -115,7 +115,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
|||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||||
python3-bs4 python3-setuptools python3-docutils \
|
python3-bs4 python3-setuptools python3-docutils \
|
||||||
memcached uwsgi uwsgi-plugin-python3 \
|
memcached uwsgi uwsgi-plugin-python3 \
|
||||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
|
texlive-xetex gettext libjs-bootstrap4 \
|
||||||
nginx python3-venv git acl
|
nginx python3-venv git acl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
note:
|
|
||||||
server_name: note-beta.crans.org
|
|
||||||
git_branch: beta
|
|
||||||
cron_enabled: false
|
|
||||||
email: notekfet2020@lists.crans.org
|
|
@ -2,5 +2,6 @@
|
|||||||
note:
|
note:
|
||||||
server_name: note-dev.crans.org
|
server_name: note-dev.crans.org
|
||||||
git_branch: beta
|
git_branch: beta
|
||||||
|
serve_static: false
|
||||||
cron_enabled: false
|
cron_enabled: false
|
||||||
email: notekfet2020@lists.crans.org
|
email: notekfet2020@lists.crans.org
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
note:
|
note:
|
||||||
server_name: note.crans.org
|
server_name: note.crans.org
|
||||||
git_branch: master
|
git_branch: master
|
||||||
|
serve_static: true
|
||||||
cron_enabled: true
|
cron_enabled: true
|
||||||
email: notekfet2020@lists.crans.org
|
email: notekfet2020@lists.crans.org
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
[dev]
|
[dev]
|
||||||
bde-note-dev.adh.crans.org
|
bde-note-dev.adh.crans.org
|
||||||
bde-nk20-beta.adh.crans.org
|
|
||||||
|
|
||||||
[prod]
|
[prod]
|
||||||
bde-note.adh.crans.org
|
bde-note.adh.crans.org
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
- ipython3
|
- ipython3
|
||||||
|
|
||||||
# Front-end dependencies
|
# Front-end dependencies
|
||||||
- fonts-font-awesome
|
|
||||||
- libjs-bootstrap4
|
- libjs-bootstrap4
|
||||||
|
|
||||||
# Python dependencies
|
# Python dependencies
|
||||||
|
@ -41,6 +41,7 @@ server {
|
|||||||
# max upload size
|
# max upload size
|
||||||
client_max_body_size 75M; # adjust to taste
|
client_max_body_size 75M; # adjust to taste
|
||||||
|
|
||||||
|
{% if note.serve_static %}
|
||||||
# Django media
|
# Django media
|
||||||
location /media {
|
location /media {
|
||||||
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
||||||
@ -50,6 +51,7 @@ server {
|
|||||||
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
location /doc {
|
location /doc {
|
||||||
alias /var/www/documentation; # The documentation of the project
|
alias /var/www/documentation; # The documentation of the project
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,12 @@ class ActivityForm(forms.ModelForm):
|
|||||||
shuffle(clubs)
|
shuffle(clubs)
|
||||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||||
|
|
||||||
|
def clean_organizer(self):
|
||||||
|
organizer = self.cleaned_data['organizer']
|
||||||
|
if not organizer.note.is_active:
|
||||||
|
self.add_error('organiser', _('The note of this club is inactive.'))
|
||||||
|
return organizer
|
||||||
|
|
||||||
def clean_date_end(self):
|
def clean_date_end(self):
|
||||||
date_end = self.cleaned_data["date_end"]
|
date_end = self.cleaned_data["date_end"]
|
||||||
date_start = self.cleaned_data["date_start"]
|
date_start = self.cleaned_data["date_start"]
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
@ -52,7 +54,7 @@ class GuestTable(tables.Table):
|
|||||||
def render_entry(self, record):
|
def render_entry(self, record):
|
||||||
if record.has_entry:
|
if record.has_entry:
|
||||||
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
|
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
|
||||||
return format_html('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
||||||
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
||||||
|
|
||||||
|
|
||||||
@ -91,7 +93,7 @@ class EntryTable(tables.Table):
|
|||||||
if hasattr(record, 'username'):
|
if hasattr(record, 'username'):
|
||||||
username = record.username
|
username = record.username
|
||||||
if username != value:
|
if username != value:
|
||||||
return format_html(value + " <em>aka.</em> " + username)
|
return mark_safe(escape(value) + " <em>aka.</em> " + escape(username))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def render_balance(self, value):
|
def render_balance(self, value):
|
||||||
|
@ -63,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
refreshBalance();
|
refreshBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
alias_obj.keyup(reloadTable);
|
alias_obj.keyup(function(event) {
|
||||||
|
let code = event.originalEvent.keyCode
|
||||||
|
if (65 <= code <= 122 || code === 13) {
|
||||||
|
debounce(reloadTable)()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(init);
|
$(document).ready(init);
|
||||||
|
|
||||||
|
@ -34,7 +34,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
|
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
|
||||||
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i>
|
<svg class="bi bi-calendar-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 8.5V10H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V11H6a.5.5 0 0 1 0-1h1.5V8.5a.5.5 0 0 1 1 0z"/>
|
||||||
|
</svg>
|
||||||
{% trans 'New activity' %}
|
{% trans 'New activity' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,8 +66,8 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
ordering = ('-date_start',)
|
ordering = ('-date_start',)
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset().distinct()
|
return super().get_queryset(**kwargs).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)
|
||||||
@ -78,9 +78,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
prefix='upcoming-',
|
prefix='upcoming-',
|
||||||
)
|
)
|
||||||
|
|
||||||
started_activities = Activity.objects\
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
|
||||||
.filter(open=True, valid=True).all()
|
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -145,7 +143,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
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, Activity, "view"))\
|
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.get(pk=self.kwargs["pk"])
|
.filter(pk=self.kwargs["pk"]).first()
|
||||||
form.fields["inviter"].initial = self.request.user.note
|
form.fields["inviter"].initial = self.request.user.note
|
||||||
return form
|
return form
|
||||||
|
|
||||||
@ -192,7 +190,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
||||||
.filter(activity=activity)\
|
.filter(activity=activity)\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
|
||||||
.order_by('last_name', 'first_name').distinct()
|
.order_by('last_name', 'first_name')
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
@ -206,7 +204,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
return guest_qs
|
return guest_qs.distinct()
|
||||||
|
|
||||||
def get_invited_note(self, activity):
|
def get_invited_note(self, activity):
|
||||||
"""
|
"""
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher, mask_hash
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +49,18 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
|||||||
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)
|
||||||
|
|
||||||
|
def safe_summary(self, encoded):
|
||||||
|
# Displayed information in Django Admin.
|
||||||
|
if '|' in encoded:
|
||||||
|
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||||
|
return OrderedDict([
|
||||||
|
(_('algorithm'), 'custom_nk15'),
|
||||||
|
(_('iterations'), '1'),
|
||||||
|
(_('salt'), mask_hash(salt)),
|
||||||
|
(_('hash'), mask_hash(db_hashed_pass)),
|
||||||
|
])
|
||||||
|
return super().safe_summary(encoded)
|
||||||
|
|
||||||
|
|
||||||
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||||
"""
|
"""
|
||||||
|
@ -19,8 +19,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
|||||||
membership_fee_paid=500,
|
membership_fee_paid=500,
|
||||||
membership_fee_unpaid=500,
|
membership_fee_unpaid=500,
|
||||||
membership_duration=396,
|
membership_duration=396,
|
||||||
membership_start="2020-08-01",
|
membership_start="2021-08-01",
|
||||||
membership_end="2021-09-30",
|
membership_end="2022-09-30",
|
||||||
)
|
)
|
||||||
Club.objects.get_or_create(
|
Club.objects.get_or_create(
|
||||||
id=2,
|
id=2,
|
||||||
@ -31,8 +31,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
|||||||
membership_fee_paid=3500,
|
membership_fee_paid=3500,
|
||||||
membership_fee_unpaid=3500,
|
membership_fee_unpaid=3500,
|
||||||
membership_duration=396,
|
membership_duration=396,
|
||||||
membership_start="2020-08-01",
|
membership_start="2021-08-01",
|
||||||
membership_end="2021-09-30",
|
membership_end="2022-09-30",
|
||||||
)
|
)
|
||||||
|
|
||||||
NoteClub.objects.get_or_create(
|
NoteClub.objects.get_or_create(
|
||||||
|
@ -57,7 +57,7 @@ class Profile(models.Model):
|
|||||||
('A1', _("Mathematics (A1)")),
|
('A1', _("Mathematics (A1)")),
|
||||||
('A2', _("Physics (A2)")),
|
('A2', _("Physics (A2)")),
|
||||||
("A'2", _("Applied physics (A'2)")),
|
("A'2", _("Applied physics (A'2)")),
|
||||||
('A''2', _("Chemistry (A''2)")),
|
("A''2", _("Chemistry (A''2)")),
|
||||||
('A3', _("Biology (A3)")),
|
('A3', _("Biology (A3)")),
|
||||||
('B1234', _("SAPHIRE (B1234)")),
|
('B1234', _("SAPHIRE (B1234)")),
|
||||||
('B1', _("Mechanics (B1)")),
|
('B1', _("Mechanics (B1)")),
|
||||||
@ -74,7 +74,7 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
promotion = models.PositiveSmallIntegerField(
|
promotion = models.PositiveSmallIntegerField(
|
||||||
null=True,
|
null=True,
|
||||||
default=datetime.date.today().year,
|
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
|
||||||
verbose_name=_("promotion"),
|
verbose_name=_("promotion"),
|
||||||
help_text=_("Year of entry to the school (None if not ENS student)"),
|
help_text=_("Year of entry to the school (None if not ENS student)"),
|
||||||
)
|
)
|
||||||
@ -413,6 +413,12 @@ class Membership(models.Model):
|
|||||||
"""
|
"""
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
"""
|
"""
|
||||||
|
# Ensure that club membership dates are valid
|
||||||
|
old_membership_start = self.club.membership_start
|
||||||
|
self.club.update_membership_dates()
|
||||||
|
if self.club.membership_start != old_membership_start:
|
||||||
|
self.club.save()
|
||||||
|
|
||||||
created = not self.pk
|
created = not self.pk
|
||||||
if not created:
|
if not created:
|
||||||
for role in self.roles.all():
|
for role in self.roles.all():
|
||||||
|
@ -31,7 +31,8 @@ class ClubTable(tables.Table):
|
|||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': 'table-row',
|
'class': 'table-row',
|
||||||
'id': lambda record: "row-" + str(record.pk),
|
'id': lambda record: "row-" + str(record.pk),
|
||||||
'data-href': lambda record: record.pk
|
'data-href': lambda record: record.pk,
|
||||||
|
'style': 'cursor:pointer',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +75,8 @@ class UserTable(tables.Table):
|
|||||||
model = User
|
model = User
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': 'table-row',
|
'class': 'table-row',
|
||||||
'data-href': lambda record: record.pk
|
'data-href': lambda record: record.pk,
|
||||||
|
'style': 'cursor:pointer',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
{% if user_object %}
|
{% if user_object %}
|
||||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
|
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
|
||||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Update Profile' %}
|
||||||
</a>
|
</a>
|
||||||
{% url 'member:user_detail' user_object.pk as user_profile_url %}
|
{% url 'member:user_detail' user_object.pk as user_profile_url %}
|
||||||
{% if request.path_info != user_profile_url %}
|
{% if request.path_info != user_profile_url %}
|
||||||
@ -59,7 +62,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% if ".change_"|has_perm:club %}
|
{% if ".change_"|has_perm:club %}
|
||||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
|
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
|
||||||
data-turbolinks="false">
|
data-turbolinks="false">
|
||||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Update Profile' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% url 'member:club_detail' club.pk as club_detail_url %}
|
{% url 'member:club_detail' club.pk as club_detail_url %}
|
||||||
|
@ -10,7 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-users"></i> {% trans "Club managers" %}
|
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Club managers" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table managers %}
|
{% render_table managers %}
|
||||||
@ -23,7 +26,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
|
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
|
||||||
<i class="fa fa-users"></i> {% trans "Club members" %}
|
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
|
||||||
|
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Club members" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table member_list %}
|
{% render_table member_list %}
|
||||||
@ -37,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
|
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
|
||||||
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
||||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Transaction history" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="history_list">
|
<div id="history_list">
|
||||||
|
@ -47,7 +47,9 @@
|
|||||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
||||||
<i class="fa fa-edit"></i>
|
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||||
|
</svg>
|
||||||
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
|
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
<a class="badge badge-secondary" href="{% url 'password_change' %}">
|
<a class="badge badge-secondary" href="{% url 'password_change' %}">
|
||||||
<i class="fa fa-lock"></i>
|
<svg class="bi bi-lock" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
{% trans 'Change password' %}
|
{% trans 'Change password' %}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
@ -20,7 +22,9 @@
|
|||||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
||||||
<i class="fa fa-edit"></i>
|
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||||
|
</svg>
|
||||||
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
|
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
@ -39,20 +43,23 @@
|
|||||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||||
|
|
||||||
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
|
||||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
|
||||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
||||||
|
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% if user_object.pk == user.pk %}
|
{% if user_object.pk == user.pk %}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
<svg class="bi bi-cogs" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'API token' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -18,7 +18,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card bg-light mb-3">
|
<div class="card bg-light mb-3">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-users"></i> {% trans "View my memberships" %}
|
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "View my memberships" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table club_list %}
|
{% render_table club_list %}
|
||||||
@ -29,7 +32,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<a class="stretched-link font-weight-bold text-decoration-none"
|
<a class="stretched-link font-weight-bold text-decoration-none"
|
||||||
{% if "note.view_note"|has_perm:user_object.note %}
|
{% if "note.view_note"|has_perm:user_object.note %}
|
||||||
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
|
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
|
||||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Transaction history" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="history_list">
|
<div id="history_list">
|
||||||
|
@ -7,7 +7,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if can_manage_registrations %}
|
{% if can_manage_registrations %}
|
||||||
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
||||||
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
|
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Registrations" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -133,23 +134,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
try:
|
||||||
|
re.compile(alias)
|
||||||
|
valid_regex = True
|
||||||
|
except (re.error, TypeError):
|
||||||
|
valid_regex = False
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.prefetch_related('note')
|
queryset = queryset.prefetch_related('note')
|
||||||
|
|
||||||
if alias:
|
if alias:
|
||||||
# We match first an alias if it is matched without normalization,
|
# We match first an alias if it is matched without normalization,
|
||||||
# then if the normalized pattern matches a normalized alias.
|
# then if the normalized pattern matches a normalized alias.
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
name__iregex="^" + alias
|
**{f'name{suffix}': alias_prefix + alias}
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||||
),
|
),
|
||||||
all=True).union(
|
all=True).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + alias.lower())
|
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
|
@ -222,6 +222,13 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make transfer when pressing Enter on the amount section
|
||||||
|
$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => {
|
||||||
|
if (event.originalEvent.charCode === 13) {
|
||||||
|
$('#btn_transfer').click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
$('#btn_transfer').click(function () {
|
$('#btn_transfer').click(function () {
|
||||||
if (LOCK) { return }
|
if (LOCK) { return }
|
||||||
|
|
||||||
@ -348,14 +355,14 @@ $('#btn_transfer').click(function () {
|
|||||||
destination_alias: dest.name
|
destination_alias: dest.name
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000)
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, gettext('insufficient funds')]), 'danger', 10000)
|
||||||
reset()
|
reset()
|
||||||
}).fail(function (err) {
|
}).fail(function (err) {
|
||||||
const errObj = JSON.parse(err.responseText)
|
const errObj = JSON.parse(err.responseText)
|
||||||
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
||||||
if (!error) { error = err.responseText }
|
if (!error) { error = err.responseText }
|
||||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger')
|
||||||
LOCK = false
|
LOCK = false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -129,7 +129,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{# Mode switch #}
|
{# Mode switch #}
|
||||||
<div class="card-footer border-primary">
|
<div class="card-footer border-primary">
|
||||||
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
|
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
|
||||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Edit" %}
|
||||||
</a>
|
</a>
|
||||||
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
|
||||||
<label for="single_conso" class="btn btn-sm btn-outline-primary active">
|
<label for="single_conso" class="btn btn-sm btn-outline-primary active">
|
||||||
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
{# bandeau transfert/crédit/débit/activité #}
|
{# bandeau transfert/crédit/débit/activité #}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
|
<div class="btn-group btn-block">
|
||||||
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
||||||
<label for="type_transfer" 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_transfer">
|
<input type="radio" name="transaction_type" id="type_transfer">
|
||||||
@ -25,6 +26,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
{% trans "Debit" %}
|
{% trans "Debit" %}
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Add shortcuts for opened activites if necessary #}
|
||||||
{% for activity in activities_open %}
|
{% for activity in activities_open %}
|
||||||
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Entries" %} {{ activity.name }}
|
{% trans "Entries" %} {{ activity.name }}
|
||||||
|
@ -53,7 +53,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
|||||||
# Add a shortcut for entry page for open activities
|
# Add a shortcut for entry page for open activities
|
||||||
if "activity" in settings.INSTALLED_APPS:
|
if "activity" in settings.INSTALLED_APPS:
|
||||||
from activity.models import Activity
|
from activity.models import Activity
|
||||||
activities_open = Activity.objects.filter(open=True).filter(
|
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||||
context["activities_open"] = [a for a in activities_open
|
context["activities_open"] = [a for a in activities_open
|
||||||
if PermissionBackend.check_perm(self.request,
|
if PermissionBackend.check_perm(self.request,
|
||||||
|
@ -159,6 +159,10 @@ class PermissionBackend(ModelBackend):
|
|||||||
primary key, the result is not memoized. Moreover, the right could change
|
primary key, the result is not memoized. Moreover, the right could change
|
||||||
(e.g. for a transaction, the balance of the user could change)
|
(e.g. for a transaction, the balance of the user could change)
|
||||||
"""
|
"""
|
||||||
|
# Requested by a shell
|
||||||
|
if request is None:
|
||||||
|
return False
|
||||||
|
|
||||||
user_obj = request.user
|
user_obj = request.user
|
||||||
sess = request.session
|
sess = request.session
|
||||||
|
|
||||||
|
@ -111,12 +111,12 @@
|
|||||||
"note",
|
"note",
|
||||||
"alias"
|
"alias"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", [\"OR\", {\"note__noteuser__user__memberships__club__name\": \"Kfet\", \"note__noteuser__user__memberships__date_start__lte\": [\"today\"], \"note__noteuser__user__memberships__date_end__gte\": [\"today\"]}, {\"note__noteclub__isnull\": false}], {\"note__is_active\": true}]",
|
"query": "[\"AND\", [\"OR\", {\"note__noteuser__user__memberships__club__name\": \"BDE\", \"note__noteuser__user__memberships__date_start__lte\": [\"today\"], \"note__noteuser__user__memberships__date_end__gte\": [\"today\"]}, {\"note__noteclub__isnull\": false}], {\"note__is_active\": true}]",
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir les aliases des notes des clubs et des adhérents du club Kfet"
|
"description": "Voir les aliases des notes des clubs et des adhérents du club BDE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -627,7 +627,7 @@
|
|||||||
"type": "view",
|
"type": "view",
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": true,
|
||||||
"description": "Voir les personnes qu'on a invitées"
|
"description": "Voir les personnes qu'on a invitées"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2883,6 +2883,7 @@
|
|||||||
3,
|
3,
|
||||||
4,
|
4,
|
||||||
5,
|
5,
|
||||||
|
6,
|
||||||
7,
|
7,
|
||||||
8,
|
8,
|
||||||
9,
|
9,
|
||||||
@ -2890,6 +2891,10 @@
|
|||||||
11,
|
11,
|
||||||
12,
|
12,
|
||||||
13,
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
22,
|
22,
|
||||||
48,
|
48,
|
||||||
52,
|
52,
|
||||||
@ -2907,11 +2912,6 @@
|
|||||||
"for_club": 2,
|
"for_club": 2,
|
||||||
"name": "Adh\u00e9rent Kfet",
|
"name": "Adh\u00e9rent Kfet",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
6,
|
|
||||||
14,
|
|
||||||
15,
|
|
||||||
16,
|
|
||||||
17,
|
|
||||||
22,
|
22,
|
||||||
34,
|
34,
|
||||||
36,
|
36,
|
||||||
@ -3048,6 +3048,7 @@
|
|||||||
31,
|
31,
|
||||||
32,
|
32,
|
||||||
33,
|
33,
|
||||||
|
43,
|
||||||
51,
|
51,
|
||||||
53,
|
53,
|
||||||
54,
|
54,
|
||||||
@ -3304,6 +3305,7 @@
|
|||||||
30,
|
30,
|
||||||
31,
|
31,
|
||||||
70,
|
70,
|
||||||
|
72,
|
||||||
143,
|
143,
|
||||||
166,
|
166,
|
||||||
167,
|
167,
|
||||||
@ -3511,6 +3513,8 @@
|
|||||||
56,
|
56,
|
||||||
57,
|
57,
|
||||||
58,
|
58,
|
||||||
|
70,
|
||||||
|
72,
|
||||||
135,
|
135,
|
||||||
137,
|
137,
|
||||||
143,
|
143,
|
||||||
|
@ -61,6 +61,12 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
# If the field wasn't modified, no need to check the permissions
|
# If the field wasn't modified, no need to check the permissions
|
||||||
if old_value == new_value:
|
if old_value == new_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if app_label == 'auth' and model_name == 'user' and field.name == 'password' and request.user.is_anonymous:
|
||||||
|
# We must ignore password changes from anonymous users since it can be done by people that forgot
|
||||||
|
# their password. We trust password change form.
|
||||||
|
continue
|
||||||
|
|
||||||
if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
|
if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
|
||||||
instance):
|
instance):
|
||||||
raise PermissionDenied(
|
raise PermissionDenied(
|
||||||
|
@ -46,7 +46,8 @@ class SignUpForm(UserCreationForm):
|
|||||||
|
|
||||||
class DeclareSogeAccountOpenedForm(forms.Form):
|
class DeclareSogeAccountOpenedForm(forms.Form):
|
||||||
soge_account = forms.BooleanField(
|
soge_account = forms.BooleanField(
|
||||||
label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."),
|
label=_("I declare that I opened or I will open soon a bank account in the Société générale with the BDE "
|
||||||
|
"partnership."),
|
||||||
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
|
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
|
||||||
"account, you will have to pay the BDE membership."),
|
"account, you will have to pay the BDE membership."),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -85,6 +85,9 @@ class UserCreateView(CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
# Direct access to validation menu if we have the right to validate it
|
||||||
|
if PermissionBackend.check_perm(self.request, 'auth.view_user', self.object):
|
||||||
|
return reverse_lazy('registration:future_user_detail', args=(self.object.pk,))
|
||||||
return reverse_lazy('registration:email_validation_sent')
|
return reverse_lazy('registration:email_validation_sent')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from django.db import transaction
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from note.api.serializers import SpecialTransactionSerializer
|
from note.api.serializers import SpecialTransactionSerializer
|
||||||
|
|
||||||
@ -68,6 +68,14 @@ class SogeCreditSerializer(serializers.ModelSerializer):
|
|||||||
The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API.
|
The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, **kwargs):
|
||||||
|
# Update soge transactions after creating a credit
|
||||||
|
instance = super().save(**kwargs)
|
||||||
|
instance.update_transactions()
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SogeCredit
|
model = SogeCredit
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
@ -4,11 +4,12 @@
|
|||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Submit
|
from crispy_forms.layout import Submit
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.inputs import AmountInput
|
from note_kfet.inputs import AmountInput, Autocomplete
|
||||||
|
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
|
||||||
|
|
||||||
|
|
||||||
class InvoiceForm(forms.ModelForm):
|
class InvoiceForm(forms.ModelForm):
|
||||||
@ -161,3 +162,19 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = SpecialTransactionProxy
|
model = SpecialTransactionProxy
|
||||||
fields = ('remittance', )
|
fields = ('remittance', )
|
||||||
|
|
||||||
|
|
||||||
|
class SogeCreditForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SogeCredit
|
||||||
|
fields = ('user', )
|
||||||
|
widgets = {
|
||||||
|
"user": Autocomplete(
|
||||||
|
User,
|
||||||
|
attrs={
|
||||||
|
'api_url': '/api/user/',
|
||||||
|
'name_field': 'username',
|
||||||
|
'placeholder': 'Nom ...',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import datetime
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -11,6 +12,7 @@ from django.db.models import Q
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from member.models import Club, Membership
|
||||||
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
|
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
|
||||||
|
|
||||||
|
|
||||||
@ -286,6 +288,7 @@ class SogeCredit(models.Model):
|
|||||||
transactions = models.ManyToManyField(
|
transactions = models.ManyToManyField(
|
||||||
MembershipTransaction,
|
MembershipTransaction,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
|
blank=True,
|
||||||
verbose_name=_("membership transactions"),
|
verbose_name=_("membership transactions"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -302,8 +305,55 @@ class SogeCredit(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
return self.credit_transaction.total if self.valid \
|
if self.valid:
|
||||||
else sum(transaction.total for transaction in self.transactions.all())
|
return self.credit_transaction.total
|
||||||
|
amount = sum(transaction.total for transaction in self.transactions.all())
|
||||||
|
if 'wei' in settings.INSTALLED_APPS:
|
||||||
|
from wei.models import WEIMembership
|
||||||
|
if not WEIMembership.objects.filter(club__weiclub__year=datetime.date.today().year, user=self.user)\
|
||||||
|
.exists():
|
||||||
|
# 80 € for people that don't go to WEI
|
||||||
|
amount += 8000
|
||||||
|
return amount
|
||||||
|
|
||||||
|
def update_transactions(self):
|
||||||
|
"""
|
||||||
|
The Sogé credit may be created after the user already paid its memberships.
|
||||||
|
We query transactions and update the credit, if it is unvalid.
|
||||||
|
"""
|
||||||
|
if self.valid or not self.pk:
|
||||||
|
return
|
||||||
|
|
||||||
|
bde = Club.objects.get(name="BDE")
|
||||||
|
kfet = Club.objects.get(name="Kfet")
|
||||||
|
bde_qs = Membership.objects.filter(user=self.user, club=bde, date_start__gte=bde.membership_start)
|
||||||
|
kfet_qs = Membership.objects.filter(user=self.user, club=kfet, date_start__gte=kfet.membership_start)
|
||||||
|
|
||||||
|
if bde_qs.exists():
|
||||||
|
m = bde_qs.get()
|
||||||
|
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
||||||
|
if m.transaction not in self.transactions.all():
|
||||||
|
self.transactions.add(m.transaction)
|
||||||
|
|
||||||
|
if kfet_qs.exists():
|
||||||
|
m = kfet_qs.get()
|
||||||
|
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
||||||
|
if m.transaction not in self.transactions.all():
|
||||||
|
self.transactions.add(m.transaction)
|
||||||
|
|
||||||
|
if 'wei' in settings.INSTALLED_APPS:
|
||||||
|
from wei.models import WEIClub
|
||||||
|
wei = WEIClub.objects.order_by('-year').first()
|
||||||
|
wei_qs = Membership.objects.filter(user=self.user, club=wei, date_start__gte=wei.membership_start)
|
||||||
|
if wei_qs.exists():
|
||||||
|
m = wei_qs.get()
|
||||||
|
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
||||||
|
if m.transaction not in self.transactions.all():
|
||||||
|
self.transactions.add(m.transaction)
|
||||||
|
|
||||||
|
for tr in self.transactions.all():
|
||||||
|
tr.valid = False
|
||||||
|
tr.save()
|
||||||
|
|
||||||
def invalidate(self):
|
def invalidate(self):
|
||||||
"""
|
"""
|
||||||
@ -365,7 +415,8 @@ class SogeCredit(models.Model):
|
|||||||
self.credit_transaction.amount = self.amount
|
self.credit_transaction.amount = self.amount
|
||||||
self.credit_transaction._force_save = True
|
self.credit_transaction._force_save = True
|
||||||
self.credit_transaction.save()
|
self.credit_transaction.save()
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -392,6 +443,7 @@ class SogeCredit(models.Model):
|
|||||||
# was opened after the validation of the account.
|
# was opened after the validation of the account.
|
||||||
self.credit_transaction.valid = False
|
self.credit_transaction.valid = False
|
||||||
self.credit_transaction.reason += " (invalide)"
|
self.credit_transaction.reason += " (invalide)"
|
||||||
|
self.credit_transaction._force_save = True
|
||||||
self.credit_transaction.save()
|
self.credit_transaction.save()
|
||||||
super().delete(**kwargs)
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -27,7 +28,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="input-group">
|
||||||
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
|
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button id="add_sogecredit" class="btn btn-success" data-toggle="modal" data-target="#add-sogecredit-modal">{% trans "Add" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label for="invalid_only" class="form-check-label">
|
<label for="invalid_only" class="form-check-label">
|
||||||
<input id="invalid_only" name="invalid_only" type="checkbox" class="checkboxinput form-check-input" checked>
|
<input id="invalid_only" name="invalid_only" type="checkbox" class="checkboxinput form-check-input" checked>
|
||||||
@ -47,11 +53,32 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Popup to add new Soge credits manually if needed #}
|
||||||
|
<div class="modal fade" id="add-sogecredit-modal" tabindex="-1" role="dialog" aria-labelledby="addSogeCredit"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="lockNote">{% trans "Add credit from the Société générale" %}</h5>
|
||||||
|
<button type="button" class="close btn-modal" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary btn-modal" data-dismiss="modal">{% trans "Close" %}</button>
|
||||||
|
<button type="button" class="btn btn-success btn-modal" data-dismiss="modal" onclick="addSogeCredit()">{% trans "Add" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function () {
|
|
||||||
let old_pattern = null;
|
let old_pattern = null;
|
||||||
let searchbar_obj = $("#searchbar");
|
let searchbar_obj = $("#searchbar");
|
||||||
let invalid_only_obj = $("#invalid_only");
|
let invalid_only_obj = $("#invalid_only");
|
||||||
@ -69,6 +96,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
|
|
||||||
searchbar_obj.keyup(reloadTable);
|
searchbar_obj.keyup(reloadTable);
|
||||||
invalid_only_obj.change(reloadTable);
|
invalid_only_obj.change(reloadTable);
|
||||||
});
|
|
||||||
|
function addSogeCredit() {
|
||||||
|
let user_pk = $('#id_user_pk').val()
|
||||||
|
if (!user_pk)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.post('/api/treasury/soge_credit/?format=json', {
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN,
|
||||||
|
user: user_pk,
|
||||||
|
}).done(function() {
|
||||||
|
addMsg("{% trans "Credit successfully registered" %}", 'success', 10000)
|
||||||
|
reloadTable()
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
errMsg(xhr.responseJSON, 30000)
|
||||||
|
reloadTable()
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -25,7 +25,8 @@ from note_kfet.settings.base import BASE_DIR
|
|||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
|
||||||
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
|
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, \
|
||||||
|
LinkTransactionToRemittanceForm, SogeCreditForm
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
|
||||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
|
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
|
||||||
|
|
||||||
@ -433,6 +434,11 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
|||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['form'] = SogeCreditForm(self.request.POST or None)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
|
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
|
||||||
"""
|
"""
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .registration import WEIForm, WEIRegistrationForm, WEIMembershipForm, BusForm, BusTeamForm
|
from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, WEIMembershipForm, BusForm, BusTeamForm
|
||||||
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
|
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'WEIForm', 'WEIRegistrationForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
||||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||||
]
|
]
|
||||||
|
@ -6,7 +6,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
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, NoteUser
|
||||||
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
||||||
|
|
||||||
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
||||||
@ -27,6 +27,15 @@ class WEIForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class WEIRegistrationForm(forms.ModelForm):
|
class WEIRegistrationForm(forms.ModelForm):
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
if 'user' in cleaned_data:
|
||||||
|
if not NoteUser.objects.filter(user=cleaned_data['user']).exists():
|
||||||
|
self.add_error('user', _("The selected user is not validated. Please validate its account first"))
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
exclude = ('wei', )
|
exclude = ('wei', )
|
||||||
@ -39,8 +48,7 @@ class WEIRegistrationForm(forms.ModelForm):
|
|||||||
'placeholder': 'Nom ...',
|
'placeholder': 'Nom ...',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"birth_date": DatePickerInput(options={'defaultDate': '2000-01-01',
|
"birth_date": DatePickerInput(options={'minDate': '1900-01-01',
|
||||||
'minDate': '1900-01-01',
|
|
||||||
'maxDate': '2100-01-01'}),
|
'maxDate': '2100-01-01'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +117,8 @@ class WEIMembershipForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]:
|
if 'team' in cleaned_data and cleaned_data["team"] is not None \
|
||||||
|
and cleaned_data["team"].bus != cleaned_data["bus"]:
|
||||||
self.add_error('bus', _("This team doesn't belong to the given bus."))
|
self.add_error('bus', _("This team doesn't belong to the given bus."))
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
@ -135,6 +144,20 @@ class WEIMembershipForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WEIMembership1AForm(WEIMembershipForm):
|
||||||
|
"""
|
||||||
|
Used to confirm registrations of first year members without choosing a bus now.
|
||||||
|
"""
|
||||||
|
roles = None
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
return super(forms.ModelForm, self).clean()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WEIMembership
|
||||||
|
fields = ('credit_type', 'credit_amount', 'last_name', 'first_name', 'bank',)
|
||||||
|
|
||||||
|
|
||||||
class BusForm(forms.ModelForm):
|
class BusForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bus
|
model = Bus
|
||||||
|
@ -50,15 +50,19 @@ class WEIBusInformation:
|
|||||||
self.bus.information = d
|
self.bus.information = d
|
||||||
self.bus.save()
|
self.bus.save()
|
||||||
|
|
||||||
def free_seats(self, surveys: List["WEISurvey"] = None):
|
def free_seats(self, surveys: List["WEISurvey"] = None, quotas=None):
|
||||||
|
if not quotas:
|
||||||
size = self.bus.size
|
size = self.bus.size
|
||||||
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
||||||
|
quotas = {self.bus: size - already_occupied}
|
||||||
|
|
||||||
|
quota = quotas[self.bus]
|
||||||
valid_surveys = sum(1 for survey in surveys if survey.information.valid
|
valid_surveys = sum(1 for survey in surveys if survey.information.valid
|
||||||
and survey.information.get_selected_bus() == self.bus) if surveys else 0
|
and survey.information.get_selected_bus() == self.bus) if surveys else 0
|
||||||
return size - already_occupied - valid_surveys
|
return quota - valid_surveys
|
||||||
|
|
||||||
def has_free_seats(self, surveys=None):
|
def has_free_seats(self, surveys=None, quotas=None):
|
||||||
return self.free_seats(surveys) > 0
|
return self.free_seats(surveys, quotas) > 0
|
||||||
|
|
||||||
|
|
||||||
class WEISurveyAlgorithm:
|
class WEISurveyAlgorithm:
|
||||||
@ -86,14 +90,20 @@ class WEISurveyAlgorithm:
|
|||||||
"""
|
"""
|
||||||
Queryset of all first year registrations
|
Queryset of all first year registrations
|
||||||
"""
|
"""
|
||||||
return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True)
|
if not hasattr(cls, '_registrations'):
|
||||||
|
cls._registrations = WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(),
|
||||||
|
first_year=True).all()
|
||||||
|
|
||||||
|
return cls._registrations
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_buses(cls) -> QuerySet:
|
def get_buses(cls) -> QuerySet:
|
||||||
"""
|
"""
|
||||||
Queryset of all buses of the associated wei.
|
Queryset of all buses of the associated wei.
|
||||||
"""
|
"""
|
||||||
return Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0)
|
if not hasattr(cls, '_buses'):
|
||||||
|
cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all()
|
||||||
|
return cls._buses
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_bus_information(cls, bus):
|
def get_bus_information(cls, bus):
|
||||||
@ -135,7 +145,10 @@ class WEISurvey:
|
|||||||
"""
|
"""
|
||||||
The WEI associated to this kind of survey.
|
The WEI associated to this kind of survey.
|
||||||
"""
|
"""
|
||||||
return WEIClub.objects.get(year=cls.get_year())
|
if not hasattr(cls, '_wei'):
|
||||||
|
cls._wei = WEIClub.objects.get(year=cls.get_year())
|
||||||
|
|
||||||
|
return cls._wei
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_survey_information_class(cls):
|
def get_survey_information_class(cls):
|
||||||
@ -210,3 +223,15 @@ class WEISurvey:
|
|||||||
self.information.selected_bus_pk = None
|
self.information.selected_bus_pk = None
|
||||||
self.information.selected_bus_name = None
|
self.information.selected_bus_name = None
|
||||||
self.information.valid = False
|
self.information.valid = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_cache(cls):
|
||||||
|
"""
|
||||||
|
Clear stored information.
|
||||||
|
"""
|
||||||
|
if hasattr(cls, '_wei'):
|
||||||
|
del cls._wei
|
||||||
|
if hasattr(cls.get_algorithm_class(), '_registrations'):
|
||||||
|
del cls.get_algorithm_class()._registrations
|
||||||
|
if hasattr(cls.get_algorithm_class(), '_buses'):
|
||||||
|
del cls.get_algorithm_class()._buses
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from functools import lru_cache
|
||||||
from random import Random
|
from random import Random
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
|
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
|
||||||
|
from ...models import WEIMembership
|
||||||
|
|
||||||
WORDS = [
|
WORDS = [
|
||||||
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
|
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
|
||||||
@ -40,19 +44,31 @@ class WEISurveyForm2021(forms.Form):
|
|||||||
if not information.seed:
|
if not information.seed:
|
||||||
information.seed = int(1000 * time.time())
|
information.seed = int(1000 * time.time())
|
||||||
information.save(registration)
|
information.save(registration)
|
||||||
|
registration._force_save = True
|
||||||
registration.save()
|
registration.save()
|
||||||
|
|
||||||
rng = Random(information.seed)
|
|
||||||
|
|
||||||
words = []
|
|
||||||
for _ignored in range(information.step + 1):
|
|
||||||
# Generate N times words
|
|
||||||
words = [rng.choice(WORDS) for _ignored2 in range(10)]
|
|
||||||
words = [(w, w) for w in words]
|
|
||||||
if self.data:
|
if self.data:
|
||||||
self.fields["word"].choices = [(w, w) for w in WORDS]
|
self.fields["word"].choices = [(w, w) for w in WORDS]
|
||||||
if self.is_valid():
|
if self.is_valid():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
rng = Random((information.step + 1) * information.seed)
|
||||||
|
|
||||||
|
words = None
|
||||||
|
|
||||||
|
buses = WEISurveyAlgorithm2021.get_buses()
|
||||||
|
informations = {bus: WEIBusInformation2021(bus) for bus in buses}
|
||||||
|
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
|
||||||
|
average_score = sum(scores) / len(scores)
|
||||||
|
|
||||||
|
preferred_words = {bus: [word for word in WORDS
|
||||||
|
if informations[bus].scores[word] >= average_score]
|
||||||
|
for bus in buses}
|
||||||
|
while words is None or len(set(words)) != len(words):
|
||||||
|
# Ensure that there is no the same word 2 times
|
||||||
|
words = [rng.choice(words) for _ignored2, words in preferred_words.items()]
|
||||||
|
rng.shuffle(words)
|
||||||
|
words = [(w, w) for w in words]
|
||||||
self.fields["word"].choices = words
|
self.fields["word"].choices = words
|
||||||
|
|
||||||
|
|
||||||
@ -123,20 +139,41 @@ class WEISurvey2021(WEISurvey):
|
|||||||
"""
|
"""
|
||||||
return self.information.step == 20
|
return self.information.step == 20
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@lru_cache()
|
||||||
|
def word_mean(cls, word):
|
||||||
|
"""
|
||||||
|
Calculate the mid-score given by all buses.
|
||||||
|
"""
|
||||||
|
buses = cls.get_algorithm_class().get_buses()
|
||||||
|
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
def score(self, bus):
|
def score(self, bus):
|
||||||
if not self.is_complete():
|
if not self.is_complete():
|
||||||
raise ValueError("Survey is not ended, can't calculate score")
|
raise ValueError("Survey is not ended, can't calculate score")
|
||||||
bus_info = self.get_algorithm_class().get_bus_information(bus)
|
|
||||||
return sum(bus_info.scores[getattr(self.information, 'word' + str(i))] for i in range(1, 21)) / 20
|
|
||||||
|
|
||||||
|
bus_info = self.get_algorithm_class().get_bus_information(bus)
|
||||||
|
# Score is the given score by the bus subtracted to the mid-score of the buses.
|
||||||
|
s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))]
|
||||||
|
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20
|
||||||
|
return s
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
def scores_per_bus(self):
|
def scores_per_bus(self):
|
||||||
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
|
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
def ordered_buses(self):
|
def ordered_buses(self):
|
||||||
values = list(self.scores_per_bus().items())
|
values = list(self.scores_per_bus().items())
|
||||||
values.sort(key=lambda item: -item[1])
|
values.sort(key=lambda item: -item[1])
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_cache(cls):
|
||||||
|
cls.word_mean.cache_clear()
|
||||||
|
return super().clear_cache()
|
||||||
|
|
||||||
|
|
||||||
class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
||||||
"""
|
"""
|
||||||
@ -152,18 +189,72 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
|||||||
def get_bus_information_class(cls):
|
def get_bus_information_class(cls):
|
||||||
return WEIBusInformation2021
|
return WEIBusInformation2021
|
||||||
|
|
||||||
def run_algorithm(self):
|
def run_algorithm(self, display_tqdm=False):
|
||||||
"""
|
"""
|
||||||
Gale-Shapley algorithm implementation.
|
Gale-Shapley algorithm implementation.
|
||||||
We modify it to allow buses to have multiple "weddings".
|
We modify it to allow buses to have multiple "weddings".
|
||||||
"""
|
"""
|
||||||
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
|
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
|
||||||
free_surveys = [s for s in surveys if not s.information.valid] # Remaining surveys
|
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
|
||||||
|
# Don't manage hardcoded people
|
||||||
|
surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
|
||||||
|
|
||||||
|
# Reset previous algorithm run
|
||||||
|
for survey in surveys:
|
||||||
|
survey.free()
|
||||||
|
survey.save()
|
||||||
|
|
||||||
|
non_men = [s for s in surveys if s.registration.gender != 'male']
|
||||||
|
men = [s for s in surveys if s.registration.gender == 'male']
|
||||||
|
|
||||||
|
quotas = {}
|
||||||
|
registrations = self.get_registrations()
|
||||||
|
non_men_total = registrations.filter(~Q(gender='male')).count()
|
||||||
|
for bus in self.get_buses():
|
||||||
|
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
||||||
|
# Remove hardcoded people
|
||||||
|
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
|
||||||
|
registration__information_json__icontains="hardcoded").count()
|
||||||
|
quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
|
||||||
|
|
||||||
|
tqdm_obj = None
|
||||||
|
if display_tqdm:
|
||||||
|
from tqdm import tqdm
|
||||||
|
tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
|
||||||
|
|
||||||
|
# Repartition for non men people first
|
||||||
|
self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
|
||||||
|
|
||||||
|
quotas = {}
|
||||||
|
for bus in self.get_buses():
|
||||||
|
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
||||||
|
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
|
||||||
|
# Remove hardcoded people
|
||||||
|
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
|
||||||
|
registration__information_json__icontains="hardcoded").count()
|
||||||
|
quotas[bus] = free_seats
|
||||||
|
|
||||||
|
if display_tqdm:
|
||||||
|
tqdm_obj.close()
|
||||||
|
|
||||||
|
from tqdm import tqdm
|
||||||
|
tqdm_obj = tqdm(total=len(men), desc="Hommes")
|
||||||
|
|
||||||
|
self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
|
||||||
|
|
||||||
|
if display_tqdm:
|
||||||
|
tqdm_obj.close()
|
||||||
|
|
||||||
|
# Clear cache information after running algorithm
|
||||||
|
WEISurvey2021.clear_cache()
|
||||||
|
|
||||||
|
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
|
||||||
|
free_surveys = surveys.copy() # Remaining surveys
|
||||||
while free_surveys: # Some students are not affected
|
while free_surveys: # Some students are not affected
|
||||||
survey = free_surveys[0]
|
survey = free_surveys[0]
|
||||||
buses = survey.ordered_buses() # Preferences of the student
|
buses = survey.ordered_buses() # Preferences of the student
|
||||||
for bus, _ignored in buses:
|
for bus, current_score in buses:
|
||||||
if self.get_bus_information(bus).has_free_seats(surveys):
|
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
|
||||||
# Selected bus has free places. Put student in the bus
|
# Selected bus has free places. Put student in the bus
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
survey.save()
|
survey.save()
|
||||||
@ -171,7 +262,6 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Current bus has not enough places. Remove the least preferred student from the bus if existing
|
# Current bus has not enough places. Remove the least preferred student from the bus if existing
|
||||||
current_score = survey.score(bus)
|
|
||||||
least_preferred_survey = None
|
least_preferred_survey = None
|
||||||
least_score = -1
|
least_score = -1
|
||||||
# Find the least student in the bus that has a lower score than the current student
|
# Find the least student in the bus that has a lower score than the current student
|
||||||
@ -193,6 +283,11 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
|||||||
free_surveys.append(least_preferred_survey)
|
free_surveys.append(least_preferred_survey)
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
survey.save()
|
survey.save()
|
||||||
|
free_surveys.remove(survey)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"User {survey.registration.user} has no free seat")
|
raise ValueError(f"User {survey.registration.user} has no free seat")
|
||||||
|
|
||||||
|
if tqdm_obj is not None:
|
||||||
|
tqdm_obj.n = len(surveys) - len(free_surveys)
|
||||||
|
tqdm_obj.refresh()
|
||||||
|
50
apps/wei/management/commands/import_scores.py
Normal file
50
apps/wei/management/commands/import_scores.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from ...forms import CurrentSurvey
|
||||||
|
from ...forms.surveys.wei2021 import WORDS # WARNING: this is specific to 2021
|
||||||
|
from ...models import Bus
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
This script is used to load scores for buses from a CSV file.
|
||||||
|
"""
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('file', nargs='?', type=argparse.FileType('r'), default=sys.stdin, help='Input CSV file')
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
file = options['file']
|
||||||
|
head = file.readline().replace('\n', '')
|
||||||
|
bus_names = head.split(';')
|
||||||
|
bus_names = [name for name in bus_names if name]
|
||||||
|
buses = []
|
||||||
|
for name in bus_names:
|
||||||
|
qs = Bus.objects.filter(name__iexact=name)
|
||||||
|
if not qs.exists():
|
||||||
|
raise ValueError(f"Bus '{name}' does not exist")
|
||||||
|
buses.append(qs.get())
|
||||||
|
|
||||||
|
informations = {bus: CurrentSurvey.get_algorithm_class().get_bus_information(bus) for bus in buses}
|
||||||
|
|
||||||
|
for line in file:
|
||||||
|
elem = line.split(';')
|
||||||
|
word = elem[0]
|
||||||
|
if word not in WORDS:
|
||||||
|
raise ValueError(f"Word {word} is not used")
|
||||||
|
|
||||||
|
for i, bus in enumerate(buses):
|
||||||
|
info = informations[bus]
|
||||||
|
info.scores[word] = float(elem[i + 1].replace(',', '.'))
|
||||||
|
|
||||||
|
for bus, info in informations.items():
|
||||||
|
info.save()
|
||||||
|
bus.save()
|
||||||
|
if options['verbosity'] > 0:
|
||||||
|
self.stdout.write(f"Bus {bus.name} saved!")
|
@ -24,17 +24,31 @@ class Command(BaseCommand):
|
|||||||
sid = transaction.savepoint()
|
sid = transaction.savepoint()
|
||||||
|
|
||||||
algorithm = CurrentSurvey.get_algorithm_class()()
|
algorithm = CurrentSurvey.get_algorithm_class()()
|
||||||
algorithm.run_algorithm()
|
|
||||||
|
try:
|
||||||
|
from tqdm import tqdm
|
||||||
|
del tqdm
|
||||||
|
display_tqdm = True
|
||||||
|
except ImportError:
|
||||||
|
display_tqdm = False
|
||||||
|
|
||||||
|
algorithm.run_algorithm(display_tqdm=display_tqdm)
|
||||||
|
|
||||||
output = options['output']
|
output = options['output']
|
||||||
registrations = algorithm.get_registrations()
|
registrations = algorithm.get_registrations()
|
||||||
per_bus = {bus: [r for r in registrations if r.information['selected_bus_pk'] == bus.pk]
|
per_bus = {bus: [r for r in registrations if 'selected_bus_pk' in r.information
|
||||||
|
and r.information['selected_bus_pk'] == bus.pk]
|
||||||
for bus in algorithm.get_buses()}
|
for bus in algorithm.get_buses()}
|
||||||
for bus, members in per_bus.items():
|
for bus, members in per_bus.items():
|
||||||
output.write(bus.name + "\n")
|
output.write(bus.name + "\n")
|
||||||
output.write("=" * len(bus.name) + "\n")
|
output.write("=" * len(bus.name) + "\n")
|
||||||
|
_order = -1
|
||||||
for r in members:
|
for r in members:
|
||||||
output.write(r.user.username + "\n")
|
survey = CurrentSurvey(r)
|
||||||
|
for _order, (b, _score) in enumerate(survey.ordered_buses()):
|
||||||
|
if b == bus:
|
||||||
|
break
|
||||||
|
output.write(f"{r.user.username} ({_order + 1})\n")
|
||||||
output.write("\n")
|
output.write("\n")
|
||||||
|
|
||||||
if not options['doit']:
|
if not options['doit']:
|
||||||
|
@ -7,6 +7,7 @@ from datetime import date
|
|||||||
from django.conf import settings
|
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.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
from member.models import Club, Membership
|
from member.models import Club, Membership
|
||||||
@ -98,6 +99,13 @@ class Bus(models.Model):
|
|||||||
"""
|
"""
|
||||||
self.information_json = json.dumps(information, indent=2)
|
self.information_json = json.dumps(information, indent=2)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggested_first_year(self):
|
||||||
|
registrations = WEIRegistration.objects.filter(Q(membership__isnull=True) | Q(membership__bus__isnull=True),
|
||||||
|
first_year=True, wei=self.wei)
|
||||||
|
registrations = [r for r in registrations if 'selected_bus_pk' in r.information]
|
||||||
|
return sum(1 for r in registrations if r.information['selected_bus_pk'] == self.pk)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@ -364,8 +372,19 @@ class WEIMembership(Membership):
|
|||||||
# to treasurers.
|
# to treasurers.
|
||||||
transaction.refresh_from_db()
|
transaction.refresh_from_db()
|
||||||
from treasury.models import SogeCredit
|
from treasury.models import SogeCredit
|
||||||
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
|
soge_credit, created = SogeCredit.objects.get_or_create(user=self.user)
|
||||||
soge_credit.refresh_from_db()
|
soge_credit.refresh_from_db()
|
||||||
transaction.save()
|
transaction.save()
|
||||||
soge_credit.transactions.add(transaction)
|
soge_credit.transactions.add(transaction)
|
||||||
soge_credit.save()
|
soge_credit.save()
|
||||||
|
|
||||||
|
soge_credit.update_transactions()
|
||||||
|
soge_credit.save()
|
||||||
|
|
||||||
|
if soge_credit.valid and \
|
||||||
|
soge_credit.credit_transaction.total != sum(tr.total for tr in soge_credit.transactions.all()):
|
||||||
|
# The credit is already validated, but we add a new transaction (eg. for the WEI).
|
||||||
|
# Then we invalidate the transaction, update the credit transaction amount
|
||||||
|
# and re-validate the credit.
|
||||||
|
soge_credit.validate(True)
|
||||||
|
soge_credit.save()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.db.models import Q
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -99,9 +100,12 @@ class WEIRegistrationTable(tables.Table):
|
|||||||
|
|
||||||
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
|
url = reverse_lazy('wei:validate_registration', args=(record.pk,))
|
||||||
text = _('Validate')
|
text = _('Validate')
|
||||||
if record.fee > record.user.note.balance:
|
if record.fee > record.user.note.balance and not record.soge_credit:
|
||||||
btn_class = 'btn-secondary'
|
btn_class = 'btn-secondary'
|
||||||
tooltip = _("The user does not have enough money.")
|
tooltip = _("The user does not have enough money.")
|
||||||
|
elif record.first_year:
|
||||||
|
btn_class = 'btn-info'
|
||||||
|
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
|
||||||
else:
|
else:
|
||||||
btn_class = 'btn-success'
|
btn_class = 'btn-success'
|
||||||
tooltip = _("The user has enough money, you can validate the registration.")
|
tooltip = _("The user has enough money, you can validate the registration.")
|
||||||
@ -166,6 +170,35 @@ class WEIMembershipTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WEIRegistration1ATable(tables.Table):
|
||||||
|
user = tables.LinkColumn(
|
||||||
|
'wei:wei_bus_1A',
|
||||||
|
args=[A('pk')],
|
||||||
|
)
|
||||||
|
|
||||||
|
preferred_bus = tables.Column(
|
||||||
|
verbose_name=_('preferred bus').capitalize,
|
||||||
|
accessor='pk',
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_preferred_bus(self, record):
|
||||||
|
information = record.information
|
||||||
|
return information['selected_bus_name'] if 'selected_bus_name' in information else "—"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped table-hover'
|
||||||
|
}
|
||||||
|
model = WEIRegistration
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
fields = ('user', 'user__last_name', 'user__first_name', 'gender',
|
||||||
|
'user__profile__department', 'preferred_bus', 'membership__bus', )
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: '' if 'selected_bus_pk' in record.information else 'bg-danger',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BusTable(tables.Table):
|
class BusTable(tables.Table):
|
||||||
name = tables.LinkColumn(
|
name = tables.LinkColumn(
|
||||||
'wei:manage_bus',
|
'wei:manage_bus',
|
||||||
@ -242,3 +275,66 @@ class BusTeamTable(tables.Table):
|
|||||||
'id': lambda record: "row-" + str(record.pk),
|
'id': lambda record: "row-" + str(record.pk),
|
||||||
'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, ))
|
'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BusRepartitionTable(tables.Table):
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name=_("name").capitalize,
|
||||||
|
accessor='name',
|
||||||
|
)
|
||||||
|
|
||||||
|
suggested_first_year = tables.Column(
|
||||||
|
verbose_name=_("suggested first year").capitalize,
|
||||||
|
accessor='pk',
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_first_year = tables.Column(
|
||||||
|
verbose_name=_("validated first year").capitalize,
|
||||||
|
accessor='pk',
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_staff = tables.Column(
|
||||||
|
verbose_name=_("validated staff").capitalize,
|
||||||
|
accessor='pk',
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
size = tables.Column(
|
||||||
|
verbose_name=_("seat count in the bus").capitalize,
|
||||||
|
accessor='size',
|
||||||
|
)
|
||||||
|
|
||||||
|
free_seats = tables.Column(
|
||||||
|
verbose_name=_("free seats").capitalize,
|
||||||
|
accessor='pk',
|
||||||
|
orderable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_suggested_first_year(self, record):
|
||||||
|
registrations = WEIRegistration.objects.filter(Q(membership__isnull=True) | Q(membership__bus__isnull=True),
|
||||||
|
first_year=True, wei=record.wei)
|
||||||
|
registrations = [r for r in registrations if 'selected_bus_pk' in r.information]
|
||||||
|
return sum(1 for r in registrations if r.information['selected_bus_pk'] == record.pk)
|
||||||
|
|
||||||
|
def render_validated_first_year(self, record):
|
||||||
|
return WEIRegistration.objects.filter(first_year=True, membership__bus=record).count()
|
||||||
|
|
||||||
|
def render_validated_staff(self, record):
|
||||||
|
return WEIRegistration.objects.filter(first_year=False, membership__bus=record).count()
|
||||||
|
|
||||||
|
def render_free_seats(self, record):
|
||||||
|
return record.size - self.render_validated_staff(record) - self.render_validated_first_year(record)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped table-hover'
|
||||||
|
}
|
||||||
|
models = Bus
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
fields = ('name', )
|
||||||
|
row_attrs = {
|
||||||
|
'class': 'table-row',
|
||||||
|
'id': lambda record: "row-" + str(record.pk),
|
||||||
|
}
|
||||||
|
20
apps/wei/templates/wei/1A_list.html
Normal file
20
apps/wei/templates/wei/1A_list.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "wei/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
{% block profile_content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Attribute first year members into buses" %}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
{% render_table bus_repartition_table %}
|
||||||
|
<hr>
|
||||||
|
<a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution!" %}</a>
|
||||||
|
<hr>
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
88
apps/wei/templates/wei/attribute_bus_1A.html
Normal file
88
apps/wei/templates/wei/attribute_bus_1A.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% extends "wei/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block profile_content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Bus attribution" %}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-xl-6">{% trans 'user'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.user }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'last name'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.user.last_name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'first name'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.user.first_name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.get_gender_display }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'department'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.user.profile.get_department_display }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ object.health_issues|default:"—" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'suggested bus'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ survey.information.selected_bus_name }}</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<button class="btn btn-link" data-toggle="collapse" data-target="#raw-survey">{% trans "View raw survey information" %}</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="raw-survey">
|
||||||
|
<dl class="row">
|
||||||
|
{% for key, value in survey.registration.information.items %}
|
||||||
|
<dt class="col-xl-6">{{ key }}</dt>
|
||||||
|
<dd class="col-xl-6">{{ value }}</dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{% for bus, score in survey.ordered_buses %}
|
||||||
|
<button class="btn btn-{% if bus.pk == survey.information.selected_bus_pk %}success{% else %}light{% endif %}" onclick="choose_bus({{ bus.pk }})">
|
||||||
|
{{ bus }} ({{ score|floatformat:2 }}) : {{ bus.memberships.count }}+{{ bus.suggested_first_year }} / {{ bus.size }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<a href="{% url 'wei:wei_1A_list' pk=object.wei.pk %}" class="btn btn-block btn-info">{% trans "Back to main list" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
function choose_bus(bus_id) {
|
||||||
|
let valid_buses = [{% for bus, score in survey.ordered_buses %}{{ bus.pk }}, {% endfor %}];
|
||||||
|
if (valid_buses.indexOf(bus_id) === -1) {
|
||||||
|
console.log("Invalid chosen bus")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/wei/membership/{{ object.membership.id }}/",
|
||||||
|
type: "PATCH",
|
||||||
|
dataType: "json",
|
||||||
|
headers: {
|
||||||
|
"X-CSRFTOKEN": CSRF_TOKEN
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
bus: bus_id,
|
||||||
|
}
|
||||||
|
}).done(function () {
|
||||||
|
window.location = "{% url 'wei:wei_bus_1A_next' pk=object.wei.pk %}"
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -29,7 +29,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Teams" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table teams %}
|
{% render_table teams %}
|
||||||
@ -42,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Members" %}
|
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Members" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table memberships %}
|
{% render_table memberships %}
|
||||||
@ -51,7 +57,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
|
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
|
||||||
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
|
<button class="btn btn-block btn-danger">
|
||||||
|
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "View as PDF" %}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -47,7 +47,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="font-weight-bold">
|
<a class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Teams" %}
|
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Teams" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table memberships %}
|
{% render_table memberships %}
|
||||||
@ -57,7 +60,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
|
|
||||||
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}"
|
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}"
|
||||||
data-turbolinks="false">
|
data-turbolinks="false">
|
||||||
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
|
<button class="btn btn-block btn-danger">
|
||||||
|
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "View as PDF" %}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -48,7 +48,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<span class="font-weight-bold">
|
<span class="font-weight-bold">
|
||||||
<i class="fa fa-bus"></i> {% trans "Buses" %}
|
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Buses" %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% render_table buses %}
|
{% render_table buses %}
|
||||||
@ -60,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card-header position-relative" id="clubListHeading">
|
<div class="card-header position-relative" id="clubListHeading">
|
||||||
<a class="stretched-link font-weight-bold text-decoration-none"
|
<a class="stretched-link font-weight-bold text-decoration-none"
|
||||||
href="{% url "wei:wei_memberships" pk=club.pk %}">
|
href="{% url "wei:wei_memberships" pk=club.pk %}">
|
||||||
<i class="fa fa-users"></i> {% trans "Members of the WEI" %}
|
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
|
||||||
|
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Members of the WEI" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% render_table member_list %}
|
{% render_table member_list %}
|
||||||
@ -72,7 +80,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
|
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
|
||||||
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
||||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Transaction history" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="history_list">
|
<div id="history_list">
|
||||||
@ -86,7 +97,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="stretched-link font-weight-bold text-decoration-none"
|
<a class="stretched-link font-weight-bold text-decoration-none"
|
||||||
href="{% url 'wei:wei_registrations' pk=club.pk %}">
|
href="{% url 'wei:wei_registrations' pk=club.pk %}">
|
||||||
<i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %}
|
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Unvalidated registrations" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="history_list">
|
<div id="history_list">
|
||||||
@ -94,6 +109,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_validate_1a %}
|
||||||
|
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
\usepackage{fontspec}
|
\usepackage{fontspec}
|
||||||
\usepackage[margin=1.5cm]{geometry}
|
\usepackage[margin=1.5cm]{geometry}
|
||||||
|
\usepackage{longtable}
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
\begin{center}
|
\begin{center}
|
||||||
@ -19,7 +20,7 @@
|
|||||||
|
|
||||||
\begin{center}
|
\begin{center}
|
||||||
\footnotesize
|
\footnotesize
|
||||||
\begin{tabular}{ccccccccc}
|
\begin{longtable}{ccccccccc}
|
||||||
\textbf{Nom} & \textbf{Prénom} & \textbf{Date de naissance} & \textbf{Genre} & \textbf{Section}
|
\textbf{Nom} & \textbf{Prénom} & \textbf{Date de naissance} & \textbf{Genre} & \textbf{Section}
|
||||||
& \textbf{Bus} & \textbf{Équipe} & \textbf{Rôles} \\
|
& \textbf{Bus} & \textbf{Équipe} & \textbf{Rôles} \\
|
||||||
{% for membership in memberships %}
|
{% for membership in memberships %}
|
||||||
@ -27,20 +28,20 @@
|
|||||||
& {{ membership.registration.get_gender_display|safe }} & {{ membership.user.profile.section_generated|safe }} & {{ membership.bus.name|safe }}
|
& {{ membership.registration.get_gender_display|safe }} & {{ membership.user.profile.section_generated|safe }} & {{ membership.bus.name|safe }}
|
||||||
& {% if membership.team %}{{ membership.team.name|safe }}{% else %}--{% endif %} & {{ membership.roles.first|safe }} \\
|
& {% if membership.team %}{{ membership.team.name|safe }}{% else %}--{% endif %} & {{ membership.roles.first|safe }} \\
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
\end{tabular}
|
\end{longtable}
|
||||||
\end{center}
|
\end{center}
|
||||||
|
|
||||||
\footnotesize
|
\footnotesize
|
||||||
Section = Année à l'ENS + code du département
|
Section = Année à l'ENS + code du département
|
||||||
|
|
||||||
\begin{center}
|
\begin{center}
|
||||||
\begin{tabular}{ccccccccc}
|
\begin{longtable}{ccccccccc}
|
||||||
\textbf{Code} & A0 & A1 & A2 & A'2 & A''2 & A3 & B1234 & B1 \\
|
\textbf{Code} & A0 & A1 & A2 & A'2 & A''2 & A3 & B1234 & B1 \\
|
||||||
\textbf{Département} & Informatique & Maths & Physique & Physique appliquée & Chimie & Biologie & SAPHIRE & Mécanique \\
|
\textbf{Département} & Informatique & Maths & Physique & Physique appliquée & Chimie & Biologie & SAPHIRE & Mécanique \\
|
||||||
\hline
|
\hline
|
||||||
\textbf{Code} & B2 & B3 & B4 & C & D2 & D3 & E & EXT \\
|
\textbf{Code} & B2 & B3 & B4 & C & D2 & D3 & E & EXT \\
|
||||||
\textbf{Département} & Génie civil & Génie mécanique & EEA & Design & Éco-gestion & Sciences sociales & Anglais & Extérieur
|
\textbf{Département} & Génie civil & Génie mécanique & EEA & Design & Éco-gestion & Sciences sociales & Anglais & Extérieur
|
||||||
\end{tabular}
|
\end{longtable}
|
||||||
\end{center}
|
\end{center}
|
||||||
|
|
||||||
\end{document}
|
\end{document}
|
||||||
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<dd class="col-xl-6">{{ registration.first_year|yesno }}</dd>
|
<dd class="col-xl-6">{{ registration.first_year|yesno }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ registration.gender }}</dd>
|
<dd class="col-xl-6">{{ registration.get_gender_display }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
|
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
|
||||||
|
@ -28,7 +28,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</a>
|
</a>
|
||||||
<hr>
|
<hr>
|
||||||
<a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false">
|
<a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false">
|
||||||
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
|
<button class="btn btn-block btn-danger">
|
||||||
|
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "View as PDF" %}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
from note.models import NoteClub, SpecialTransaction
|
from note.models import NoteClub, SpecialTransaction, NoteUser
|
||||||
from treasury.models import SogeCredit
|
from treasury.models import SogeCredit
|
||||||
|
|
||||||
from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
|
from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
|
||||||
@ -84,6 +84,13 @@ class TestWEIRegistration(TestCase):
|
|||||||
wei=self.wei,
|
wei=self.wei,
|
||||||
description="Test Bus",
|
description="Test Bus",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Setup the bus
|
||||||
|
bus_info = CurrentSurvey.get_algorithm_class().get_bus_information(self.bus)
|
||||||
|
bus_info.scores["Jus de fruit"] = 70
|
||||||
|
bus_info.save()
|
||||||
|
self.bus.save()
|
||||||
|
|
||||||
self.team = BusTeam.objects.create(
|
self.team = BusTeam.objects.create(
|
||||||
name="Test Team",
|
name="Test Team",
|
||||||
bus=self.bus,
|
bus=self.bus,
|
||||||
@ -295,6 +302,7 @@ class TestWEIRegistration(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
user = User.objects.create(username="toto", email="toto@example.com")
|
user = User.objects.create(username="toto", email="toto@example.com")
|
||||||
|
NoteUser.objects.create(user=user)
|
||||||
|
|
||||||
# Try with an invalid form
|
# Try with an invalid form
|
||||||
response = self.client.post(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
response = self.client.post(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||||
@ -361,7 +369,7 @@ class TestWEIRegistration(TestCase):
|
|||||||
last_name="toto",
|
last_name="toto",
|
||||||
bank="Société générale",
|
bank="Société générale",
|
||||||
))
|
))
|
||||||
response = self.client.get(reverse("wei:wei_register_2A_myself", kwargs=dict(wei_pk=self.wei.pk)))
|
response = self.client.get(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk)))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Check that if the WEI is started, we can't register anyone
|
# Check that if the WEI is started, we can't register anyone
|
||||||
@ -377,10 +385,8 @@ class TestWEIRegistration(TestCase):
|
|||||||
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
|
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(reverse("wei:wei_register_1A_myself", kwargs=dict(wei_pk=self.wei.pk)))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
user = User.objects.create(username="toto", email="toto@example.com")
|
user = User.objects.create(username="toto", email="toto@example.com")
|
||||||
|
NoteUser.objects.create(user=user)
|
||||||
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||||
user=user.id,
|
user=user.id,
|
||||||
soge_credit=True,
|
soge_credit=True,
|
||||||
@ -460,6 +466,24 @@ class TestWEIRegistration(TestCase):
|
|||||||
response = self.client.get(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)))
|
response = self.client.get(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)))
|
||||||
self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200)
|
self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_register_myself(self):
|
||||||
|
"""
|
||||||
|
Try to register myself to the WEI, and check redirections.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse('wei:wei_register_1A_myself', args=(self.wei.pk,)))
|
||||||
|
self.assertRedirects(response, reverse('wei:wei_update_registration', args=(self.registration.pk,)))
|
||||||
|
|
||||||
|
response = self.client.get(reverse('wei:wei_register_2A_myself', args=(self.wei.pk,)))
|
||||||
|
self.assertRedirects(response, reverse('wei:wei_update_registration', args=(self.registration.pk,)))
|
||||||
|
|
||||||
|
self.registration.delete()
|
||||||
|
|
||||||
|
response = self.client.get(reverse('wei:wei_register_1A_myself', args=(self.wei.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('wei:wei_register_2A_myself', args=(self.wei.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_wei_survey_ended(self):
|
def test_wei_survey_ended(self):
|
||||||
"""
|
"""
|
||||||
Test display the end page of a survey.
|
Test display the end page of a survey.
|
||||||
@ -761,58 +785,6 @@ class TestDefaultWEISurvey(TestCase):
|
|||||||
self.assertEqual(CurrentSurvey.get_year(), 2021)
|
self.assertEqual(CurrentSurvey.get_year(), 2021)
|
||||||
|
|
||||||
|
|
||||||
class TestWEISurveyAlgorithm(TestCase):
|
|
||||||
"""
|
|
||||||
Run the WEI Algorithm.
|
|
||||||
TODO: Improve this test with some test data once the algorithm will be implemented.
|
|
||||||
"""
|
|
||||||
fixtures = ("initial",)
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.year = timezone.now().year
|
|
||||||
self.wei = WEIClub.objects.create(
|
|
||||||
name="Test WEI",
|
|
||||||
email="gc.wei@example.com",
|
|
||||||
parent_club_id=2,
|
|
||||||
membership_fee_paid=12500,
|
|
||||||
membership_fee_unpaid=5500,
|
|
||||||
membership_start=date(self.year, 1, 1),
|
|
||||||
membership_end=date(self.year, 12, 31),
|
|
||||||
year=self.year,
|
|
||||||
date_start=date.today() + timedelta(days=2),
|
|
||||||
date_end=date(self.year, 12, 31),
|
|
||||||
)
|
|
||||||
NoteClub.objects.create(club=self.wei)
|
|
||||||
self.bus = Bus.objects.create(
|
|
||||||
name="Test Bus",
|
|
||||||
wei=self.wei,
|
|
||||||
description="Test Bus",
|
|
||||||
)
|
|
||||||
self.team = BusTeam.objects.create(
|
|
||||||
name="Test Team",
|
|
||||||
bus=self.bus,
|
|
||||||
color=0xFFFFFF,
|
|
||||||
description="Test Team",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.user = User.objects.create(username="toto")
|
|
||||||
self.registration = WEIRegistration.objects.create(
|
|
||||||
user_id=self.user.id,
|
|
||||||
wei_id=self.wei.id,
|
|
||||||
soge_credit=True,
|
|
||||||
caution_check=True,
|
|
||||||
birth_date=date(2000, 1, 1),
|
|
||||||
gender="nonbinary",
|
|
||||||
clothing_cut="male",
|
|
||||||
clothing_size="XL",
|
|
||||||
health_issues="I am a bot",
|
|
||||||
emergency_contact_name="Pikachu",
|
|
||||||
emergency_contact_phone="+33123456789",
|
|
||||||
first_year=True,
|
|
||||||
)
|
|
||||||
CurrentSurvey(self.registration).save()
|
|
||||||
|
|
||||||
|
|
||||||
class TestWeiAPI(TestAPI):
|
class TestWeiAPI(TestAPI):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
@ -3,12 +3,11 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\
|
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
|
||||||
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView,\
|
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \
|
||||||
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\
|
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
|
||||||
WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIDeleteRegistrationView,\
|
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
|
||||||
WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
|
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
|
||||||
|
|
||||||
|
|
||||||
app_name = 'wei'
|
app_name = 'wei'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -24,6 +23,7 @@ urlpatterns = [
|
|||||||
name="wei_memberships_bus_pdf"),
|
name="wei_memberships_bus_pdf"),
|
||||||
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
|
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
|
||||||
name="wei_memberships_team_pdf"),
|
name="wei_memberships_team_pdf"),
|
||||||
|
path('bus-1A/list/<int:pk>/', WEI1AListView.as_view(), name="wei_1A_list"),
|
||||||
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
|
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
|
||||||
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
|
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
|
||||||
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
|
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
|
||||||
@ -40,4 +40,6 @@ urlpatterns = [
|
|||||||
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
|
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
|
||||||
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
|
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
|
||||||
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
|
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
|
||||||
|
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
|
||||||
|
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
|
||||||
]
|
]
|
||||||
|
@ -7,14 +7,14 @@ import subprocess
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
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.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q, Count
|
from django.db.models import Q, Count
|
||||||
from django.db.models.functions.text import Lower
|
from django.db.models.functions.text import Lower
|
||||||
from django.forms import HiddenInput
|
from django.http import HttpResponse, Http404
|
||||||
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
|
||||||
@ -32,8 +32,10 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
|||||||
|
|
||||||
from .forms.registration import WEIChooseBusForm
|
from .forms.registration import WEIChooseBusForm
|
||||||
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
|
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
|
||||||
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembershipForm, CurrentSurvey
|
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
|
||||||
from .tables import WEITable, WEIRegistrationTable, BusTable, BusTeamTable, WEIMembershipTable
|
WEIMembershipForm, CurrentSurvey
|
||||||
|
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
|
||||||
|
WEIRegistration1ATable, WEIMembershipTable
|
||||||
|
|
||||||
|
|
||||||
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
|
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
|
||||||
@ -132,7 +134,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
wei=club
|
wei=club
|
||||||
)
|
)
|
||||||
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
|
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
|
||||||
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('pre-registration-page', 1))
|
||||||
context['pre_registrations'] = pre_registrations_table
|
context['pre_registrations'] = pre_registrations_table
|
||||||
|
|
||||||
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
||||||
@ -190,6 +192,10 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
|
|
||||||
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
||||||
|
|
||||||
|
qs = WEIMembership.objects.filter(club=club, registration__first_year=True, bus__isnull=True)
|
||||||
|
context["can_validate_1a"] = PermissionBackend.check_perm(
|
||||||
|
self.request, "wei.change_weimembership_bus", qs.first()) if qs.exists() else False
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -487,9 +493,16 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
def get_sample_object(self):
|
def get_sample_object(self):
|
||||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||||
|
if "myself" in self.request.path:
|
||||||
|
user = self.request.user
|
||||||
|
else:
|
||||||
|
# To avoid unique validation issues, we use an account that can't join the WEI.
|
||||||
|
# In development mode, the note account may not exist, we use a random user (may fail)
|
||||||
|
user = User.objects.get(username="note") \
|
||||||
|
if User.objects.filter(username="note").exists() else User.objects.first()
|
||||||
return WEIRegistration(
|
return WEIRegistration(
|
||||||
wei=wei,
|
wei=wei,
|
||||||
user=self.request.user,
|
user=user,
|
||||||
first_year=True,
|
first_year=True,
|
||||||
birth_date="1970-01-01",
|
birth_date="1970-01-01",
|
||||||
gender="No",
|
gender="No",
|
||||||
@ -503,6 +516,11 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
# We can't register someone once the WEI is started and before the membership start date
|
# We can't register someone once the WEI is started and before the membership start date
|
||||||
if today >= wei.date_start or today < wei.membership_start:
|
if today >= wei.date_start or today < wei.membership_start:
|
||||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||||
|
# Don't register twice
|
||||||
|
if 'myself' in self.request.path and not self.request.user.is_anonymous \
|
||||||
|
and WEIRegistration.objects.filter(wei=wei, user=self.request.user).exists():
|
||||||
|
obj = WEIRegistration.objects.get(wei=wei, user=self.request.user)
|
||||||
|
return redirect(reverse_lazy('wei:wei_update_registration', args=(obj.pk,)))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -538,6 +556,12 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
" participated to a WEI."))
|
" participated to a WEI."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
if 'treasury' in settings.INSTALLED_APPS:
|
||||||
|
from treasury.models import SogeCredit
|
||||||
|
form.instance.soge_credit = \
|
||||||
|
form.instance.soge_credit \
|
||||||
|
or SogeCredit.objects.filter(user=form.instance.user, credit_transaction__valid=False).exists()
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -555,9 +579,16 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
def get_sample_object(self):
|
def get_sample_object(self):
|
||||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||||
|
if "myself" in self.request.path:
|
||||||
|
user = self.request.user
|
||||||
|
else:
|
||||||
|
# To avoid unique validation issues, we use an account that can't join the WEI.
|
||||||
|
# In development mode, the note account may not exist, we use a random user (may fail)
|
||||||
|
user = User.objects.get(username="note") \
|
||||||
|
if User.objects.filter(username="note").exists() else User.objects.first()
|
||||||
return WEIRegistration(
|
return WEIRegistration(
|
||||||
wei=wei,
|
wei=wei,
|
||||||
user=self.request.user,
|
user=user,
|
||||||
first_year=True,
|
first_year=True,
|
||||||
birth_date="1970-01-01",
|
birth_date="1970-01-01",
|
||||||
gender="No",
|
gender="No",
|
||||||
@ -571,6 +602,11 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
# We can't register someone once the WEI is started and before the membership start date
|
# We can't register someone once the WEI is started and before the membership start date
|
||||||
if today >= wei.date_start or today < wei.membership_start:
|
if today >= wei.date_start or today < wei.membership_start:
|
||||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||||
|
# Don't register twice
|
||||||
|
if 'myself' in self.request.path and not self.request.user.is_anonymous \
|
||||||
|
and WEIRegistration.objects.filter(wei=wei, user=self.request.user).exists():
|
||||||
|
obj = WEIRegistration.objects.get(wei=wei, user=self.request.user)
|
||||||
|
return redirect(reverse_lazy('wei:wei_update_registration', args=(obj.pk,)))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -627,6 +663,12 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
form.instance.information = information
|
form.instance.information = information
|
||||||
form.instance.save()
|
form.instance.save()
|
||||||
|
|
||||||
|
if 'treasury' in settings.INSTALLED_APPS:
|
||||||
|
from treasury.models import SogeCredit
|
||||||
|
form.instance.soge_credit = \
|
||||||
|
form.instance.soge_credit \
|
||||||
|
or SogeCredit.objects.filter(user=form.instance.user, credit_transaction__valid=False).exists()
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -655,26 +697,19 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
context["club"] = self.object.wei
|
context["club"] = self.object.wei
|
||||||
|
|
||||||
if self.object.is_validated:
|
if self.object.is_validated:
|
||||||
membership_form = WEIMembershipForm(instance=self.object.membership,
|
membership_form = self.get_membership_form(instance=self.object.membership,
|
||||||
data=self.request.POST if self.request.POST else None)
|
data=self.request.POST)
|
||||||
for field_name, field in membership_form.fields.items():
|
|
||||||
if not PermissionBackend.check_perm(
|
|
||||||
self.request, "wei.change_membership_" + field_name, self.object.membership):
|
|
||||||
field.widget = HiddenInput()
|
|
||||||
del membership_form.fields["credit_type"]
|
|
||||||
del membership_form.fields["credit_amount"]
|
|
||||||
del membership_form.fields["first_name"]
|
|
||||||
del membership_form.fields["last_name"]
|
|
||||||
del membership_form.fields["bank"]
|
|
||||||
context["membership_form"] = membership_form
|
context["membership_form"] = membership_form
|
||||||
elif not self.object.first_year and PermissionBackend.check_perm(
|
elif not self.object.first_year and PermissionBackend.check_perm(
|
||||||
self.request, "wei.change_weiregistration_information_json", self.object):
|
self.request, "wei.change_weiregistration_information_json", self.object):
|
||||||
|
information = self.object.information
|
||||||
|
d = dict(
|
||||||
|
bus=Bus.objects.filter(pk__in=information["preferred_bus_pk"]).all(),
|
||||||
|
team=BusTeam.objects.filter(pk__in=information["preferred_team_pk"]).all(),
|
||||||
|
roles=WEIRole.objects.filter(pk__in=information["preferred_roles_pk"]).all(),
|
||||||
|
) if 'preferred_bus_pk' in information else dict()
|
||||||
choose_bus_form = WEIChooseBusForm(
|
choose_bus_form = WEIChooseBusForm(
|
||||||
self.request.POST if self.request.POST else dict(
|
self.request.POST if self.request.POST else d
|
||||||
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
|
||||||
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
|
|
||||||
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
|
||||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
|
||||||
@ -690,15 +725,29 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
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.fields["user"].disabled = True
|
form.fields["user"].disabled = True
|
||||||
if not self.object.first_year:
|
# The auto-json-format may cause issues with the default field remove
|
||||||
|
if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object):
|
||||||
del form.fields["information_json"]
|
del form.fields["information_json"]
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def get_membership_form(self, data=None, instance=None):
|
||||||
|
membership_form = WEIMembershipForm(data if data else None, instance=instance)
|
||||||
|
del membership_form.fields["credit_type"]
|
||||||
|
del membership_form.fields["credit_amount"]
|
||||||
|
del membership_form.fields["first_name"]
|
||||||
|
del membership_form.fields["last_name"]
|
||||||
|
del membership_form.fields["bank"]
|
||||||
|
for field_name, _field in list(membership_form.fields.items()):
|
||||||
|
if not PermissionBackend.check_perm(
|
||||||
|
self.request, "wei.change_weimembership_" + field_name, self.object.membership):
|
||||||
|
del membership_form.fields[field_name]
|
||||||
|
return membership_form
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
# If the membership is already validated, then we update the bus and the team (and the roles)
|
# If the membership is already validated, then we update the bus and the team (and the roles)
|
||||||
if form.instance.is_validated:
|
if form.instance.is_validated:
|
||||||
membership_form = WEIMembershipForm(self.request.POST, instance=form.instance.membership)
|
membership_form = self.get_membership_form(self.request.POST, form.instance.membership)
|
||||||
if not membership_form.is_valid():
|
if not membership_form.is_valid():
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
membership_form.save()
|
membership_form.save()
|
||||||
@ -772,7 +821,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
Validate WEI Registration
|
Validate WEI Registration
|
||||||
"""
|
"""
|
||||||
model = WEIMembership
|
model = WEIMembership
|
||||||
form_class = WEIMembershipForm
|
|
||||||
extra_context = {"title": _("Validate WEI registration")}
|
extra_context = {"title": _("Validate WEI registration")}
|
||||||
|
|
||||||
def get_sample_object(self):
|
def get_sample_object(self):
|
||||||
@ -828,6 +876,12 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||||
|
if registration.first_year and 'sleected_bus_pk' not in registration.information:
|
||||||
|
return WEIMembership1AForm
|
||||||
|
return WEIMembershipForm
|
||||||
|
|
||||||
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)
|
||||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||||
@ -843,6 +897,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
form.fields["bank"].disabled = True
|
form.fields["bank"].disabled = True
|
||||||
form.fields["bank"].initial = "Société générale"
|
form.fields["bank"].initial = "Société générale"
|
||||||
|
|
||||||
|
if 'bus' in form.fields:
|
||||||
|
# For 2A+ and hardcoded 1A
|
||||||
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
|
form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk)
|
||||||
if registration.first_year:
|
if registration.first_year:
|
||||||
# Use the results of the survey to fill initial data
|
# Use the results of the survey to fill initial data
|
||||||
@ -950,12 +1006,11 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
membership.roles.set(WEIRole.objects.filter(name="1A").all())
|
membership.roles.set(WEIRole.objects.filter(name="1A").all())
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
ret = super().form_valid(form)
|
membership.save()
|
||||||
|
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
|
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
|
||||||
|
|
||||||
return ret
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
self.object.refresh_from_db()
|
self.object.refresh_from_db()
|
||||||
@ -1122,3 +1177,65 @@ class MemberListRenderView(LoginRequiredMixin, View):
|
|||||||
shutil.rmtree(tmp_dir)
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
|
||||||
|
model = WEIRegistration
|
||||||
|
template_name = "wei/1A_list.html"
|
||||||
|
table_class = WEIRegistration1ATable
|
||||||
|
extra_context = {"title": _("Attribute buses to first year members")}
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self, filter_permissions=True, **kwargs):
|
||||||
|
qs = super().get_queryset(filter_permissions, **kwargs)
|
||||||
|
qs = qs.filter(first_year=True, membership__isnull=False)
|
||||||
|
qs = qs.order_by('-membership__bus')
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['club'] = self.club
|
||||||
|
context['bus_repartition_table'] = BusRepartitionTable(
|
||||||
|
Bus.objects.filter(wei=self.club, size__gt=0)
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Bus, "view"))
|
||||||
|
.all())
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class WEIAttributeBus1AView(ProtectQuerysetMixin, DetailView):
|
||||||
|
model = WEIRegistration
|
||||||
|
template_name = "wei/attribute_bus_1A.html"
|
||||||
|
extra_context = {"title": _("Attribute bus")}
|
||||||
|
|
||||||
|
def get_queryset(self, filter_permissions=True, **kwargs):
|
||||||
|
qs = super().get_queryset(filter_permissions, **kwargs)
|
||||||
|
qs = qs.filter(first_year=True)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_object()
|
||||||
|
if 'selected_bus_pk' not in obj.information:
|
||||||
|
return redirect(reverse_lazy('wei:wei_survey', args=(obj.pk,)))
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['club'] = self.object.wei
|
||||||
|
context['survey'] = CurrentSurvey(self.object)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
wei = WEIClub.objects.filter(pk=self.kwargs['pk'])
|
||||||
|
if not wei.exists():
|
||||||
|
raise Http404
|
||||||
|
wei = wei.get()
|
||||||
|
qs = WEIRegistration.objects.filter(wei=wei, membership__isnull=False, membership__bus__isnull=True)
|
||||||
|
qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works...
|
||||||
|
if qs.exists():
|
||||||
|
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, ))
|
||||||
|
return reverse_lazy('wei:wei_1A_list', args=(wei.pk, ))
|
||||||
|
@ -23,7 +23,7 @@ Sur un Ubuntu/Debian :
|
|||||||
$ sudo apt update
|
$ sudo apt update
|
||||||
$ sudo apt install --no-install-recommends -y \
|
$ sudo apt install --no-install-recommends -y \
|
||||||
python3-setuptools python3-venv python3-dev \
|
python3-setuptools python3-venv python3-dev \
|
||||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
|
texlive-xetex gettext libjs-bootstrap4 git
|
||||||
|
|
||||||
Pour Arch Linux :
|
Pour Arch Linux :
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ plus propre. On peut donc installer tout ce dont on a besoin, depuis buster-back
|
|||||||
$ sudo apt update
|
$ sudo apt update
|
||||||
$ sudo apt install -t buster-backports --no-install-recommends \
|
$ sudo apt install -t buster-backports --no-install-recommends \
|
||||||
gettext git ipython3 \ # Dépendances basiques
|
gettext git ipython3 \ # Dépendances basiques
|
||||||
fonts-font-awesome libjs-bootstrap4 \ # Pour l'affichage web
|
libjs-bootstrap4 \ # Pour l'affichage web
|
||||||
python3-bs4 python3-django python3-django-crispy-forms python3-django-extensions \
|
python3-bs4 python3-django python3-django-crispy-forms python3-django-extensions \
|
||||||
python3-django-filters python3-django-oauth-toolkit python3-django-polymorphic \
|
python3-django-filters python3-django-oauth-toolkit python3-django-polymorphic \
|
||||||
python3-djangorestframework python3-memcache python3-phonenumbers \
|
python3-djangorestframework python3-memcache python3-phonenumbers \
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -75,7 +75,7 @@ class LoginByIPMiddleware(object):
|
|||||||
else:
|
else:
|
||||||
ip = request.META.get('REMOTE_ADDR')
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
|
||||||
qs = User.objects.filter(password=f"ipbased${ip}")
|
qs = User.objects.filter(password__iregex=f"ipbased\\$.*\\^{ip}\\$.*")
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
login(request, qs.get())
|
login(request, qs.get())
|
||||||
session = request.session
|
session = request.session
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function pretty_money (value) {
|
function pretty_money (value) {
|
||||||
if (value % 100 === 0) { return (value < 0 ? '- ' : '') + Math.round(Math.abs(value) / 100) + ' €' } else {
|
if (value % 100 === 0) { return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + ' €' } else {
|
||||||
return (value < 0 ? '- ' : '') + Math.round(Math.abs(value) / 100) + '.' +
|
return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + '.' +
|
||||||
(Math.abs(value) % 100 < 10 ? '0' : '') + (Math.abs(value) % 100) + ' €'
|
(Math.abs(value) % 100 < 10 ? '0' : '') + (Math.abs(value) % 100) + ' €'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,7 +96,11 @@ function displayStyle (note) {
|
|||||||
if (!note) { return '' }
|
if (!note) { return '' }
|
||||||
const balance = note.balance
|
const balance = note.balance
|
||||||
var css = ''
|
var css = ''
|
||||||
if (balance < -5000) { css += ' text-danger bg-dark' } else if (balance < -1000) { css += ' text-danger' } else if (balance < 0) { css += ' text-warning' } else if (!note.email_confirmed) { css += ' text-white bg-primary' } else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += 'text-white bg-info' }
|
if (balance < -5000) { css += ' text-danger bg-dark' }
|
||||||
|
else if (balance < -1000) { css += ' text-danger' }
|
||||||
|
else if (balance < 0) { css += ' text-warning' }
|
||||||
|
if (!note.email_confirmed) { css += ' bg-primary' }
|
||||||
|
else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += ' bg-info' }
|
||||||
return css
|
return css
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,11 +381,11 @@ function de_validate (id, validated, resourcetype) {
|
|||||||
* @param callback Function to call
|
* @param callback Function to call
|
||||||
* @param wait Debounced milliseconds
|
* @param wait Debounced milliseconds
|
||||||
*/
|
*/
|
||||||
function debounce (callback, wait) {
|
let debounce_timeout
|
||||||
let timeout
|
function debounce (callback, wait=500) {
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
const context = this
|
const context = this
|
||||||
clearTimeout(timeout)
|
clearTimeout(debounce_timeout)
|
||||||
timeout = setTimeout(() => callback.apply(context, args), wait)
|
debounce_timeout = setTimeout(() => callback.apply(context, args), wait)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}">
|
<meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
{# Bootstrap, Font Awesome and custom CSS #}
|
{# Load CSS #}
|
||||||
<link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}">
|
<link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}">
|
||||||
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
|
|
||||||
<link rel="stylesheet" href="{% static "css/custom.css" %}">
|
<link rel="stylesheet" href="{% static "css/custom.css" %}">
|
||||||
|
|
||||||
{# JQuery, Bootstrap and Turbolinks JavaScript #}
|
{# JQuery, Bootstrap and Turbolinks JavaScript #}
|
||||||
@ -64,54 +63,101 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% if "note.transactiontemplate"|not_empty_model_list %}
|
{% if "note.transactiontemplate"|not_empty_model_list %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'note:consos' as url %}
|
{% url 'note:consos' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-mug" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 2a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v1h.5A1.5 1.5 0 0 1 16 4.5v7a1.5 1.5 0 0 1-1.5 1.5h-.55a2.5 2.5 0 0 1-2.45 2h-8A2.5 2.5 0 0 1 1 12.5V2zm13 10h.5a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5H14v8z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Consumptions' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.is_authenticated and user|is_member:"Kfet" %}
|
{% if user.is_authenticated and user|is_member:"Kfet" %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'note:transfer' as url %}
|
{% url 'note:transfer' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %} </a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-exchange" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 11.5a.5.5 0 0 0 .5.5h11.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 11H1.5a.5.5 0 0 0-.5.5zm14-7a.5.5 0 0 1-.5.5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H14.5a.5.5 0 0 1 .5.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% 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">
|
<li class="nav-item">
|
||||||
{% url 'member:user_list' as url %}
|
{% url 'member:user_list' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-user"></i> {% trans 'Users' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Users' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "member.club"|not_empty_model_list %}
|
{% if "member.club"|not_empty_model_list %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'member:club_list' as url %}
|
{% url 'member:club_list' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
|
||||||
|
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Clubs' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "activity.activity"|not_empty_model_list %}
|
{% if "activity.activity"|not_empty_model_list %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'activity:activity_list' as url %}
|
{% url 'activity:activity_list' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-calendar" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V5h16V4H0V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Activities' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "treasury.invoice"|not_empty_model_list %}
|
{% if "treasury.invoice"|not_empty_model_list %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'treasury:invoice_list' as url %}
|
{% url 'treasury:invoice_list' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-credit-card"></i> {% trans 'Treasury' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-credit-card" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1H0V4zm0 3v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7H0zm3 2h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Treasury' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if "wei.weiclub"|not_empty_model_list %}
|
{% if "wei.weiclub"|not_empty_model_list %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'wei:current_wei_detail' as url %}
|
{% url 'wei:current_wei_detail' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-bus"></i> {% trans 'WEI' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'WEI' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'permission:rights' as url %}
|
{% url 'permission:rights' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-balance-scale"></i> {% trans 'Rights' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
|
||||||
|
<svg class="bi bi-shield" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 0c-.69 0-1.843.265-2.928.56-1.11.3-2.229.655-2.887.87a1.54 1.54 0 0 0-1.044 1.262c-.596 4.477.787 7.795 2.465 9.99a11.777 11.777 0 0 0 2.517 2.453c.386.273.744.482 1.048.625.28.132.581.24.829.24s.548-.108.829-.24a7.159 7.159 0 0 0 1.048-.625 11.775 11.775 0 0 0 2.517-2.453c1.678-2.195 3.061-5.513 2.465-9.99a1.541 1.541 0 0 0-1.044-1.263 62.467 62.467 0 0 0-2.887-.87C9.843.266 8.69 0 8 0zm0 5a1.5 1.5 0 0 1 .5 2.915l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99A1.5 1.5 0 0 1 8 5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Rights' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_staff and ""|has_perm:user %}
|
{% if request.user.is_staff and ""|has_perm:user %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> {% trans 'Admin' %}</a>
|
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}">
|
||||||
|
<svg class="bi bi-cog" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans 'Admin' %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -119,16 +165,25 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fa fa-user"></i>
|
<svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
</svg>
|
||||||
<span id="user_balance">{{ request.user.username }} ({{ request.user.note.balance | pretty_money }})</span>
|
<span id="user_balance">{{ request.user.username }} ({{ request.user.note.balance | pretty_money }})</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-right"
|
<div class="dropdown-menu dropdown-menu-right"
|
||||||
aria-labelledby="navbarDropdownMenuLink">
|
aria-labelledby="navbarDropdownMenuLink">
|
||||||
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
|
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
|
||||||
<i class="fa fa-user"></i> {% trans "My account" %}
|
<svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "My account" %}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href="{% url 'logout' %}">
|
<a class="dropdown-item" href="{% url 'logout' %}">
|
||||||
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
|
<svg class="bi bi-signout" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||||
|
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Log out" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -136,14 +191,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% if request.path != "/registration/signup/" %}
|
{% if request.path != "/registration/signup/" %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'registration:signup' %}">
|
<a class="nav-link" href="{% url 'registration:signup' %}">
|
||||||
<i class="fa fa-user-plus"></i> {% trans "Sign up" %}
|
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Sign up" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.path != "/accounts/login/" %}
|
{% if request.path != "/accounts/login/" %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'login' %}">
|
<a class="nav-link" href="{% url 'login' %}">
|
||||||
<i class="fa fa-sign-in"></i> {% trans "Log in" %}
|
<svg class="bi bi-login" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/>
|
||||||
|
<path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||||
|
</svg>
|
||||||
|
{% trans "Log in" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -170,8 +233,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% if user.sogecredit and not user.sogecredit.valid %}
|
{% if user.sogecredit and not user.sogecredit.valid %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
You declared that you opened a bank account in the Société générale. The bank did not validate the creation of the account to the BDE,
|
You declared that you opened a bank account in the Société générale. The bank did not validate
|
||||||
so the registration bonus of 80 € is not credited and the membership is not paid yet.
|
the creation of the account to the BDE, so the membership and the WEI are not paid yet.
|
||||||
This verification procedure may last a few days.
|
This verification procedure may last a few days.
|
||||||
Please make sure that you go to the end of the account creation.
|
Please make sure that you go to the end of the account creation.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
@ -193,6 +256,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<span class="text-muted mr-1">
|
<span class="text-muted mr-1">
|
||||||
<a href="mailto:{{ "CONTACT_EMAIL" | getenv }}"
|
<a href="mailto:{{ "CONTACT_EMAIL" | getenv }}"
|
||||||
class="text-muted">{% trans "Contact us" %}</a> —
|
class="text-muted">{% trans "Contact us" %}</a> —
|
||||||
|
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
|
||||||
|
class="text-muted">{% trans "Technical Support" %}</a> —
|
||||||
</span>
|
</span>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<select title="language" name="language"
|
<select title="language" name="language"
|
||||||
|
@ -35,8 +35,9 @@ urlpatterns = [
|
|||||||
path('coffee/', include('django_htcpcp_tea.urls')),
|
path('coffee/', include('django_htcpcp_tea.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# During development, serve media files
|
# During development, serve static and media files
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
if "oauth2_provider" in settings.INSTALLED_APPS:
|
if "oauth2_provider" in settings.INSTALLED_APPS:
|
||||||
|
@ -4,14 +4,14 @@ django-bootstrap-datepicker-plus~=3.0.5
|
|||||||
django-cas-server~=1.2.0
|
django-cas-server~=1.2.0
|
||||||
django-colorfield~=0.3.2
|
django-colorfield~=0.3.2
|
||||||
django-crispy-forms~=1.7.2
|
django-crispy-forms~=1.7.2
|
||||||
django-extensions~=2.1.4
|
django-extensions>=2.1.4
|
||||||
django-filter~=2.1.0
|
django-filter~=2.1
|
||||||
django-htcpcp-tea~=0.3.1
|
django-htcpcp-tea~=0.3.1
|
||||||
django-mailer~=2.0.1
|
django-mailer~=2.0.1
|
||||||
django-oauth-toolkit~=1.3.3
|
django-oauth-toolkit~=1.3.3
|
||||||
django-phonenumber-field~=5.0.0
|
django-phonenumber-field~=5.0.0
|
||||||
django-polymorphic~=2.0.3
|
django-polymorphic>=2.0.3,<3.0.0
|
||||||
djangorestframework~=3.9.0
|
djangorestframework>=3.9.0,<3.13.0
|
||||||
django-rest-polymorphic~=0.1.9
|
django-rest-polymorphic~=0.1.9
|
||||||
django-tables2~=2.3.1
|
django-tables2~=2.3.1
|
||||||
python-memcached~=1.59
|
python-memcached~=1.59
|
||||||
|
Reference in New Issue
Block a user