Merge branch 'wei' into 'master'

[WEI] Algo de répartition

Closes #97 et #98

See merge request bde/nk20!180
This commit is contained in:
ynerant 2021-09-27 12:28:03 +00:00
commit 11dd8adbb7
11 changed files with 260 additions and 101 deletions

View File

@ -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,

View File

@ -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
import datetime
from datetime import date from datetime import date
from django.conf import settings from django.conf import settings
@ -305,8 +305,16 @@ 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): def update_transactions(self):
""" """
@ -323,11 +331,13 @@ class SogeCredit(models.Model):
if bde_qs.exists(): if bde_qs.exists():
m = bde_qs.get() m = bde_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all(): if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction) self.transactions.add(m.transaction)
if kfet_qs.exists(): if kfet_qs.exists():
m = kfet_qs.get() m = kfet_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all(): if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction) self.transactions.add(m.transaction)
@ -337,6 +347,7 @@ class SogeCredit(models.Model):
wei_qs = Membership.objects.filter(user=self.user, club=wei, date_start__gte=wei.membership_start) wei_qs = Membership.objects.filter(user=self.user, club=wei, date_start__gte=wei.membership_start)
if wei_qs.exists(): if wei_qs.exists():
m = wei_qs.get() m = wei_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all(): if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction) self.transactions.add(m.transaction)
@ -432,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)

View File

@ -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

View File

@ -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',
@ -135,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):
""" """
@ -164,19 +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
surveys = [s for s in surveys if s.is_complete()] surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
free_surveys = [s for s in surveys if not s.information.valid] # Remaining 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()
@ -184,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
@ -206,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()

View File

@ -24,7 +24,15 @@ 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()
@ -34,8 +42,13 @@ class Command(BaseCommand):
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']:

View File

@ -95,7 +95,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if can_validate_1a or True %} {% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a> <a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -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}

View File

@ -7,6 +7,7 @@ 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
@ -191,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
@ -551,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):
@ -652,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):
@ -1181,7 +1198,10 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['club'] = self.club context['club'] = self.club
context['bus_repartition_table'] = BusRepartitionTable(Bus.objects.filter(wei=self.club, size__gt=0).all()) 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 return context
@ -1218,4 +1238,4 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works... qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works...
if qs.exists(): if qs.exists():
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, )) return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, ))
return reverse_lazy('wei_1A_list', args=(wei.pk, )) return reverse_lazy('wei:wei_1A_list', args=(wei.pk, ))

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-12 19:30+0200\n" "POT-Creation-Date: 2021-09-13 23:26+0200\n"
"PO-Revision-Date: 2020-11-16 20:02+0000\n" "PO-Revision-Date: 2020-11-16 20:02+0000\n"
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n" "Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -56,7 +56,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301 #: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301
#: apps/permission/models.py:330 #: apps/permission/models.py:330
#: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/registration/templates/registration/future_profile_detail.html:16
#: apps/wei/models.py:66 apps/wei/models.py:123 apps/wei/tables.py:283 #: apps/wei/models.py:67 apps/wei/models.py:131 apps/wei/tables.py:282
#: apps/wei/templates/wei/base.html:26 #: apps/wei/templates/wei/base.html:26
#: apps/wei/templates/wei/weimembership_form.html:14 #: apps/wei/templates/wei/weimembership_form.html:14
msgid "name" msgid "name"
@ -91,7 +91,7 @@ msgstr "types d'activité"
#: apps/activity/models.py:68 #: apps/activity/models.py:68
#: apps/activity/templates/activity/includes/activity_info.html:19 #: apps/activity/templates/activity/includes/activity_info.html:19
#: apps/note/models/transactions.py:81 apps/permission/models.py:110 #: apps/note/models/transactions.py:81 apps/permission/models.py:110
#: apps/permission/models.py:189 apps/wei/models.py:77 apps/wei/models.py:134 #: apps/permission/models.py:189 apps/wei/models.py:78 apps/wei/models.py:142
msgid "description" msgid "description"
msgstr "description" msgstr "description"
@ -112,7 +112,7 @@ msgstr "type"
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:305 #: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:305
#: apps/note/models/notes.py:148 apps/treasury/models.py:285 #: apps/note/models/notes.py:148 apps/treasury/models.py:285
#: apps/wei/models.py:165 apps/wei/templates/wei/attribute_bus_1A.html:13 #: apps/wei/models.py:173 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15 #: apps/wei/templates/wei/survey.html:15
msgid "user" msgid "user"
msgstr "utilisateur" msgstr "utilisateur"
@ -511,7 +511,7 @@ msgstr "rôles"
msgid "fee" msgid "fee"
msgstr "cotisation" msgstr "cotisation"
#: apps/member/apps.py:14 apps/wei/tables.py:227 apps/wei/tables.py:258 #: apps/member/apps.py:14 apps/wei/tables.py:226 apps/wei/tables.py:257
msgid "member" msgid "member"
msgstr "adhérent" msgstr "adhérent"
@ -1913,10 +1913,10 @@ msgstr "Cet email est déjà pris."
#: apps/registration/forms.py:49 #: apps/registration/forms.py:49
msgid "" msgid ""
"I declare that I opened a bank account in the Société générale with the BDE " "I declare that I opened or I will open soon a bank account in the Société générale with the BDE "
"partnership." "partnership."
msgstr "" msgstr ""
"Je déclare avoir ouvert un compte à la société générale avec le partenariat " "Je déclare avoir ouvert ou ouvrir prochainement un compte à la société générale avec le partenariat "
"du BDE." "du BDE."
#: apps/registration/forms.py:50 #: apps/registration/forms.py:50
@ -2508,8 +2508,8 @@ msgstr "Liste des crédits de la Société générale"
msgid "Manage credits from the Société générale" msgid "Manage credits from the Société générale"
msgstr "Gérer les crédits de la Société générale" msgstr "Gérer les crédits de la Société générale"
#: apps/wei/apps.py:10 apps/wei/models.py:49 apps/wei/models.py:50 #: apps/wei/apps.py:10 apps/wei/models.py:50 apps/wei/models.py:51
#: apps/wei/models.py:61 apps/wei/models.py:172 #: apps/wei/models.py:62 apps/wei/models.py:180
#: note_kfet/templates/base.html:103 #: note_kfet/templates/base.html:103
msgid "WEI" msgid "WEI"
msgstr "WEI" msgstr "WEI"
@ -2520,8 +2520,8 @@ msgstr ""
"L'utilisateur sélectionné n'est pas validé. Merci de d'abord valider son " "L'utilisateur sélectionné n'est pas validé. Merci de d'abord valider son "
"compte." "compte."
#: apps/wei/forms/registration.py:59 apps/wei/models.py:118 #: apps/wei/forms/registration.py:59 apps/wei/models.py:126
#: apps/wei/models.py:315 #: apps/wei/models.py:323
msgid "bus" msgid "bus"
msgstr "bus" msgstr "bus"
@ -2547,7 +2547,7 @@ msgstr ""
"bus ou électron libre)" "bus ou électron libre)"
#: apps/wei/forms/registration.py:75 apps/wei/forms/registration.py:85 #: apps/wei/forms/registration.py:75 apps/wei/forms/registration.py:85
#: apps/wei/models.py:153 #: apps/wei/models.py:161
msgid "WEI Roles" msgid "WEI Roles"
msgstr "Rôles au WEI" msgstr "Rôles au WEI"
@ -2563,123 +2563,123 @@ msgstr "Cette équipe n'appartient pas à ce bus."
msgid "Choose a word:" msgid "Choose a word:"
msgstr "Choisissez un mot :" msgstr "Choisissez un mot :"
#: apps/wei/models.py:24 apps/wei/templates/wei/base.html:36 #: apps/wei/models.py:25 apps/wei/templates/wei/base.html:36
msgid "year" msgid "year"
msgstr "année" msgstr "année"
#: apps/wei/models.py:28 apps/wei/templates/wei/base.html:30 #: apps/wei/models.py:29 apps/wei/templates/wei/base.html:30
msgid "date start" msgid "date start"
msgstr "début" msgstr "début"
#: apps/wei/models.py:32 apps/wei/templates/wei/base.html:33 #: apps/wei/models.py:33 apps/wei/templates/wei/base.html:33
msgid "date end" msgid "date end"
msgstr "fin" msgstr "fin"
#: apps/wei/models.py:70 apps/wei/tables.py:306 #: apps/wei/models.py:71 apps/wei/tables.py:305
msgid "seat count in the bus" msgid "seat count in the bus"
msgstr "nombre de sièges dans le bus" msgstr "nombre de sièges dans le bus"
#: apps/wei/models.py:82 #: apps/wei/models.py:83
msgid "survey information" msgid "survey information"
msgstr "informations sur le questionnaire" msgstr "informations sur le questionnaire"
#: apps/wei/models.py:83 #: apps/wei/models.py:84
msgid "Information about the survey for new members, encoded in JSON" msgid "Information about the survey for new members, encoded in JSON"
msgstr "" msgstr ""
"Informations sur le sondage pour les nouveaux membres, encodées en JSON" "Informations sur le sondage pour les nouveaux membres, encodées en JSON"
#: apps/wei/models.py:105 #: apps/wei/models.py:113
msgid "Bus" msgid "Bus"
msgstr "Bus" msgstr "Bus"
#: apps/wei/models.py:106 apps/wei/templates/wei/weiclub_detail.html:51 #: apps/wei/models.py:114 apps/wei/templates/wei/weiclub_detail.html:51
msgid "Buses" msgid "Buses"
msgstr "Bus" msgstr "Bus"
#: apps/wei/models.py:127 #: apps/wei/models.py:135
msgid "color" msgid "color"
msgstr "couleur" msgstr "couleur"
#: apps/wei/models.py:128 #: apps/wei/models.py:136
msgid "The color of the T-Shirt, stored with its number equivalent" msgid "The color of the T-Shirt, stored with its number equivalent"
msgstr "" msgstr ""
"La couleur du T-Shirt, stocké sous la forme de son équivalent numérique" "La couleur du T-Shirt, stocké sous la forme de son équivalent numérique"
#: apps/wei/models.py:142 #: apps/wei/models.py:150
msgid "Bus team" msgid "Bus team"
msgstr "Équipe de bus" msgstr "Équipe de bus"
#: apps/wei/models.py:143 #: apps/wei/models.py:151
msgid "Bus teams" msgid "Bus teams"
msgstr "Équipes de bus" msgstr "Équipes de bus"
#: apps/wei/models.py:152 #: apps/wei/models.py:160
msgid "WEI Role" msgid "WEI Role"
msgstr "Rôle au WEI" msgstr "Rôle au WEI"
#: apps/wei/models.py:177 #: apps/wei/models.py:185
msgid "Credit from Société générale" msgid "Credit from Société générale"
msgstr "Crédit de la Société générale" msgstr "Crédit de la Société générale"
#: apps/wei/models.py:182 #: apps/wei/models.py:190
msgid "Caution check given" msgid "Caution check given"
msgstr "Chèque de caution donné" msgstr "Chèque de caution donné"
#: apps/wei/models.py:186 apps/wei/templates/wei/weimembership_form.html:64 #: apps/wei/models.py:194 apps/wei/templates/wei/weimembership_form.html:64
msgid "birth date" msgid "birth date"
msgstr "date de naissance" msgstr "date de naissance"
#: apps/wei/models.py:192 apps/wei/models.py:202 #: apps/wei/models.py:200 apps/wei/models.py:210
msgid "Male" msgid "Male"
msgstr "Homme" msgstr "Homme"
#: apps/wei/models.py:193 apps/wei/models.py:203 #: apps/wei/models.py:201 apps/wei/models.py:211
msgid "Female" msgid "Female"
msgstr "Femme" msgstr "Femme"
#: apps/wei/models.py:194 #: apps/wei/models.py:202
msgid "Non binary" msgid "Non binary"
msgstr "Non-binaire" msgstr "Non-binaire"
#: apps/wei/models.py:196 apps/wei/templates/wei/attribute_bus_1A.html:22 #: apps/wei/models.py:204 apps/wei/templates/wei/attribute_bus_1A.html:22
#: apps/wei/templates/wei/weimembership_form.html:55 #: apps/wei/templates/wei/weimembership_form.html:55
msgid "gender" msgid "gender"
msgstr "genre" msgstr "genre"
#: apps/wei/models.py:205 apps/wei/templates/wei/weimembership_form.html:58 #: apps/wei/models.py:213 apps/wei/templates/wei/weimembership_form.html:58
msgid "clothing cut" msgid "clothing cut"
msgstr "coupe de vêtement" msgstr "coupe de vêtement"
#: apps/wei/models.py:218 apps/wei/templates/wei/weimembership_form.html:61 #: apps/wei/models.py:226 apps/wei/templates/wei/weimembership_form.html:61
msgid "clothing size" msgid "clothing size"
msgstr "taille de vêtement" msgstr "taille de vêtement"
#: apps/wei/models.py:224 apps/wei/templates/wei/attribute_bus_1A.html:28 #: apps/wei/models.py:232 apps/wei/templates/wei/attribute_bus_1A.html:28
#: apps/wei/templates/wei/weimembership_form.html:67 #: apps/wei/templates/wei/weimembership_form.html:67
msgid "health issues" msgid "health issues"
msgstr "problèmes de santé" msgstr "problèmes de santé"
#: apps/wei/models.py:229 apps/wei/templates/wei/weimembership_form.html:70 #: apps/wei/models.py:237 apps/wei/templates/wei/weimembership_form.html:70
msgid "emergency contact name" msgid "emergency contact name"
msgstr "nom du contact en cas d'urgence" msgstr "nom du contact en cas d'urgence"
#: apps/wei/models.py:234 apps/wei/templates/wei/weimembership_form.html:73 #: apps/wei/models.py:242 apps/wei/templates/wei/weimembership_form.html:73
msgid "emergency contact phone" msgid "emergency contact phone"
msgstr "téléphone du contact en cas d'urgence" msgstr "téléphone du contact en cas d'urgence"
#: apps/wei/models.py:239 apps/wei/templates/wei/weimembership_form.html:52 #: apps/wei/models.py:247 apps/wei/templates/wei/weimembership_form.html:52
msgid "first year" msgid "first year"
msgstr "première année" msgstr "première année"
#: apps/wei/models.py:240 #: apps/wei/models.py:248
msgid "Tells if the user is new in the school." msgid "Tells if the user is new in the school."
msgstr "Indique si l'utilisateur est nouveau dans l'école." msgstr "Indique si l'utilisateur est nouveau dans l'école."
#: apps/wei/models.py:245 #: apps/wei/models.py:253
msgid "registration information" msgid "registration information"
msgstr "informations sur l'inscription" msgstr "informations sur l'inscription"
#: apps/wei/models.py:246 #: apps/wei/models.py:254
msgid "" msgid ""
"Information about the registration (buses for old members, survey for the " "Information about the registration (buses for old members, survey for the "
"new members), encoded in JSON" "new members), encoded in JSON"
@ -2687,27 +2687,27 @@ msgstr ""
"Informations sur l'inscription (bus pour les 2A+, questionnaire pour les " "Informations sur l'inscription (bus pour les 2A+, questionnaire pour les "
"1A), encodées en JSON" "1A), encodées en JSON"
#: apps/wei/models.py:304 #: apps/wei/models.py:312
msgid "WEI User" msgid "WEI User"
msgstr "Participant au WEI" msgstr "Participant au WEI"
#: apps/wei/models.py:305 #: apps/wei/models.py:313
msgid "WEI Users" msgid "WEI Users"
msgstr "Participants au WEI" msgstr "Participants au WEI"
#: apps/wei/models.py:325 #: apps/wei/models.py:333
msgid "team" msgid "team"
msgstr "équipe" msgstr "équipe"
#: apps/wei/models.py:335 #: apps/wei/models.py:343
msgid "WEI registration" msgid "WEI registration"
msgstr "Inscription au WEI" msgstr "Inscription au WEI"
#: apps/wei/models.py:339 #: apps/wei/models.py:347
msgid "WEI membership" msgid "WEI membership"
msgstr "Adhésion au WEI" msgstr "Adhésion au WEI"
#: apps/wei/models.py:340 #: apps/wei/models.py:348
msgid "WEI memberships" msgid "WEI memberships"
msgstr "Adhésions au WEI" msgstr "Adhésions au WEI"
@ -2735,32 +2735,32 @@ msgstr "Année"
msgid "preferred bus" msgid "preferred bus"
msgstr "bus préféré" msgstr "bus préféré"
#: apps/wei/tables.py:211 apps/wei/templates/wei/bus_detail.html:32 #: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:32
#: apps/wei/templates/wei/busteam_detail.html:50 #: apps/wei/templates/wei/busteam_detail.html:50
msgid "Teams" msgid "Teams"
msgstr "Équipes" msgstr "Équipes"
#: apps/wei/tables.py:220 apps/wei/tables.py:261 #: apps/wei/tables.py:219 apps/wei/tables.py:260
msgid "Members count" msgid "Members count"
msgstr "Nombre de membres" msgstr "Nombre de membres"
#: apps/wei/tables.py:227 apps/wei/tables.py:258 #: apps/wei/tables.py:226 apps/wei/tables.py:257
msgid "members" msgid "members"
msgstr "adhérents" msgstr "adhérents"
#: apps/wei/tables.py:288 #: apps/wei/tables.py:287
msgid "suggested first year" msgid "suggested first year"
msgstr "1A suggérés" msgstr "1A suggérés"
#: apps/wei/tables.py:294 #: apps/wei/tables.py:293
msgid "validated first year" msgid "validated first year"
msgstr "1A validés" msgstr "1A validés"
#: apps/wei/tables.py:300 #: apps/wei/tables.py:299
msgid "validated staff" msgid "validated staff"
msgstr "2A+ validés" msgstr "2A+ validés"
#: apps/wei/tables.py:311 #: apps/wei/tables.py:310
msgid "free seats" msgid "free seats"
msgstr "sièges libres" msgstr "sièges libres"
@ -3116,7 +3116,7 @@ msgstr "Valider l'inscription WEI"
msgid "Attribute buses to first year members" msgid "Attribute buses to first year members"
msgstr "Répartir les 1A dans les bus" msgstr "Répartir les 1A dans les bus"
#: apps/wei/views.py:1190 #: apps/wei/views.py:1191
msgid "Attribute bus" msgid "Attribute bus"
msgstr "Attribuer un bus" msgstr "Attribuer un bus"
@ -3252,16 +3252,15 @@ msgstr ""
msgid "" msgid ""
"You declared that you opened a bank account in the Société générale. The " "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, so the " "bank did not validate the creation of the account to the BDE, so the "
"registration bonus of 80 € is not credited and the membership is not paid " "membership and the WEI are not paid yet. This verification procedure may "
"yet. This verification procedure may last a few days. Please make sure that " "last a few days. Please make sure that you go to the end of the account "
"you go to the end of the account creation." "creation."
msgstr "" msgstr ""
"Vous avez déclaré que vous avez ouvert un compte bancaire à la société " "Vous avez déclaré que vous avez ouvert un compte bancaire à la société "
"générale. La banque n'a pas encore validé la création du compte auprès du " "générale. La banque n'a pas encore validé la création du compte auprès du "
"BDE, le bonus d'inscription de 80 € n'a donc pas encore été créditée et " "BDE, l'adhésion et le WEI ne sont donc pas encore payés. Cette procédure de "
"l'adhésion n'est pas encore payée. Cette procédure de vérification peut " "vérification peut durer quelques jours. Merci de vous assurer de bien aller "
"durer quelques jours. Merci de vous assurer de bien aller au bout de vos " "au bout de vos démarches."
"démarches."
#: note_kfet/templates/base.html:195 #: note_kfet/templates/base.html:195
msgid "Contact us" msgid "Contact us"

View File

@ -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
} }

View File

@ -170,8 +170,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 +193,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> &mdash; class="text-muted">{% trans "Contact us" %}</a> &mdash;
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
class="text-muted">{% trans "Technical Support" %}</a> &mdash;
</span> </span>
{% csrf_token %} {% csrf_token %}
<select title="language" name="language" <select title="language" name="language"