From 03c1bb41b60966877684b9dd87b4173c3f32fdfa Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Fri, 18 Jul 2025 23:49:34 +0200 Subject: [PATCH 01/22] First of many --- locale/fr/LC_MESSAGES/django.po | 8 -------- 1 file changed, 8 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index b62c48bc..76ebb02b 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -4095,14 +4095,6 @@ msgstr "La note est indisponible pour le moment" msgid "Thank you for your understanding -- The Respos Info of BDE" msgstr "Merci de votre compréhension -- Les Respos Info du BDE" -#: note_kfet/templates/base_search.html:15 -msgid "Search by attribute such as name..." -msgstr "Chercher par un attribut tel que le nom..." - -#: note_kfet/templates/base_search.html:23 -msgid "There is no results." -msgstr "Il n'y a pas de résultat." - #: note_kfet/templates/cas/logged.html:8 msgid "" "

Log In Successful

You have successfully logged into the Central " From edb6abfff5ca51236844ff73233dc1624b75d5ee Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 19 Jul 2025 16:24:25 +0200 Subject: [PATCH 02/22] Add fee field to WEIRegistration to be able to sort on validation status --- apps/member/apps.py | 11 ++++- apps/member/signals.py | 22 ++++++++++ ...ation_fee_alter_weiclub_fee_soge_credit.py | 23 ++++++++++ apps/wei/models.py | 35 ++++++++++++---- apps/wei/tables.py | 15 ++++--- apps/wei/views.py | 42 +++++++++++++++++-- 6 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 apps/wei/migrations/0016_weiregistration_fee_alter_weiclub_fee_soge_credit.py diff --git a/apps/member/apps.py b/apps/member/apps.py index d5b1f630..84799e6a 100644 --- a/apps/member/apps.py +++ b/apps/member/apps.py @@ -6,7 +6,7 @@ from django.conf import settings from django.db.models.signals import post_save from django.utils.translation import gettext_lazy as _ -from .signals import save_user_profile +from .signals import save_user_profile, update_wei_registration_fee_on_membership_creation, update_wei_registration_fee_on_club_change class MemberConfig(AppConfig): @@ -17,7 +17,16 @@ class MemberConfig(AppConfig): """ Define app internal signals to interact with other apps """ + from .models import Membership, Club post_save.connect( save_user_profile, sender=settings.AUTH_USER_MODEL, ) + post_save.connect( + update_wei_registration_fee_on_membership_creation, + sender=Membership + ) + post_save.connect( + update_wei_registration_fee_on_club_change, + sender=Club + ) diff --git a/apps/member/signals.py b/apps/member/signals.py index 869f9117..e74c37ad 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -13,3 +13,25 @@ def save_user_profile(instance, created, raw, **_kwargs): instance.profile.email_confirmed = True instance.profile.registration_valid = True instance.profile.save() + + +def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs): + if created: + from wei.models import WEIRegistration + if instance.club.id == 1 or instance.club.id == 2: + registrations = WEIRegistration.objects.filter( + user=instance.user, + wei__year=instance.date_start.year, + ) + for r in registrations: + r.save() + + +def update_wei_registration_fee_on_club_change(sender, instance, **kwargs): + from wei.models import WEIRegistration + if instance.id == 1 or instance.id == 2: + registrations = WEIRegistration.objects.filter( + wei__year=instance.membership_start.year, + ) + for r in registrations: + r.save() diff --git a/apps/wei/migrations/0016_weiregistration_fee_alter_weiclub_fee_soge_credit.py b/apps/wei/migrations/0016_weiregistration_fee_alter_weiclub_fee_soge_credit.py new file mode 100644 index 00000000..6d2d1289 --- /dev/null +++ b/apps/wei/migrations/0016_weiregistration_fee_alter_weiclub_fee_soge_credit.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2025-07-19 12:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wei', '0015_remove_weiclub_caution_amount_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='weiregistration', + name='fee', + field=models.PositiveIntegerField(blank=True, default=0, verbose_name='fee'), + ), + migrations.AlterField( + model_name='weiclub', + name='fee_soge_credit', + field=models.PositiveIntegerField(default=2000, verbose_name='membership fee (soge credit)'), + ), + ] diff --git a/apps/wei/models.py b/apps/wei/models.py index 59f018d7..18ba8a58 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -285,6 +285,12 @@ class WEIRegistration(models.Model): "encoded in JSON"), ) + fee = models.PositiveIntegerField( + default=0, + verbose_name=_('fee'), + blank=True, + ) + class Meta: unique_together = ('user', 'wei',) verbose_name = _("WEI User") @@ -309,7 +315,25 @@ class WEIRegistration(models.Model): self.information_json = json.dumps(information, indent=2) @property - def fee(self): + def is_validated(self): + try: + return self.membership is not None + except AttributeError: + return False + + @property + def validation_status(self): + """ + Define an order to have easier access to validatable registrations + """ + if self.fee + (self.wei.deposit_amount if self.deposit_type == 'note' else 0) > self.user.note.balance: + return 2 + elif self.first_year: + return 1 + else: + return 0 + + def calculate_fee(self): bde = Club.objects.get(pk=1) kfet = Club.objects.get(pk=2) @@ -336,12 +360,9 @@ class WEIRegistration(models.Model): return fee - @property - def is_validated(self): - try: - return self.membership is not None - except AttributeError: - return False + def save(self, *args, **kwargs): + self.fee = self.calculate_fee() + super().save(*args, **kwargs) class WEIMembership(Membership): diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 9daadcb6..3f0c322e 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -56,10 +56,13 @@ class WEIRegistrationTable(tables.Table): } ) - validate = tables.Column( + validate = tables.LinkColumn( + 'wei:wei_update_registration', + args=[A('pk')], verbose_name=_("Validate"), - orderable=False, - accessor=A('pk'), + orderable=True, + accessor='validate_status', + text=_("Validate"), attrs={ 'th': { 'id': 'validate-membership-header' @@ -100,10 +103,11 @@ class WEIRegistrationTable(tables.Table): url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true' text = _('Validate') - if record.fee > record.user.note.balance and not record.soge_credit: + status = record.validation_status + if status == 2: btn_class = 'btn-secondary' tooltip = _("The user does not have enough money.") - elif record.first_year: + elif status == 1: btn_class = 'btn-info' tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.") else: @@ -121,6 +125,7 @@ class WEIRegistrationTable(tables.Table): attrs = { 'class': 'table table-condensed table-striped table-hover' } + order_by = ('validate', 'user',) model = WEIRegistration template_name = 'django_tables2/bootstrap4.html' fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check', diff --git a/apps/wei/views.py b/apps/wei/views.py index 4ac679f9..d5151848 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -13,7 +13,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.db import transaction -from django.db.models import Q, Count +from django.db.models import Q, Count, Case, When, Value, IntegerField, F from django.db.models.functions.text import Lower from django import forms from django.http import HttpResponse, Http404 @@ -133,6 +133,23 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D membership=None, wei=club ) + # Annotate the query to be able to sort registrations on validate status + pre_registrations = pre_registrations.annotate( + deposit=Case( + When(deposit_type='note', then=F('wei__deposit_amount')), + default=Value(0), + output_field=IntegerField() + ) + ).annotate( + total_fee=F('fee') + F('deposit') + ).annotate( + validate_status=Case( + When(total_fee__gt=F('user__note__balance'), then=Value(2)), + When(first_year=True, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ) buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \ .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name") return [club_transactions, club_member, pre_registrations, buses, ] @@ -261,6 +278,23 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() + qs = qs.annotate( + deposit=Case( + When(deposit_type='note', then=F('wei__deposit_amount')), + default=Value(0), + output_field=IntegerField() + ) + ).annotate( + total_fee=F('fee') + F('deposit') + ).annotate( + validate_status=Case( + When(total_fee__gt=F('user__note__balance'), then=Value(2)), + When(first_year=True, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ) + pattern = self.request.GET.get("search", "") if pattern: @@ -963,9 +997,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): form = context["form"] if registration.soge_credit: - form.fields["credit_amount"].initial = registration.fee + form.fields["credit_amount"].initial = fee else: - form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance) + form.fields["credit_amount"].initial = max(0, fee - registration.user.note.balance) return context @@ -1052,7 +1086,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid if registration.soge_credit: - fee = 2000 + fee = registration.wei.fee_soge_credit kfet = club.parent_club bde = kfet.parent_club From 9ab4df94e66ef6a4538c1e67e644dc06b6117d91 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 19 Jul 2025 16:55:07 +0200 Subject: [PATCH 03/22] Minor fixes --- apps/wei/forms/__init__.py | 4 ++-- apps/wei/forms/registration.py | 17 ++--------------- apps/wei/tables.py | 4 ++-- apps/wei/views.py | 15 ++++----------- 4 files changed, 10 insertions(+), 30 deletions(-) diff --git a/apps/wei/forms/__init__.py b/apps/wei/forms/__init__.py index 71fb2c5f..1cb9f283 100644 --- a/apps/wei/forms/__init__.py +++ b/apps/wei/forms/__init__.py @@ -1,11 +1,11 @@ # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .registration import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, WEIMembership1AForm, \ +from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, \ WEIMembershipForm, BusForm, BusTeamForm from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey __all__ = [ - 'WEIForm', 'WEIRegistrationForm', 'WEIRegistration1AForm', 'WEIRegistration2AForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm', + 'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', ] diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index 35838c2b..4bf0dcd2 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -44,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm): fields = [ 'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size', 'health_issues', 'emergency_contact_name', 'emergency_contact_phone', - 'first_year', 'information_json', 'deposit_check' + 'first_year', 'information_json', 'deposit_check', 'deposit_type' ] widgets = { "user": Autocomplete( @@ -62,21 +62,8 @@ class WEIRegistrationForm(forms.ModelForm): "deposit_check": forms.BooleanField( required=False, ), - } - - -class WEIRegistration2AForm(WEIRegistrationForm): - class Meta(WEIRegistrationForm.Meta): - fields = WEIRegistrationForm.Meta.fields + ['deposit_type'] - widgets = WEIRegistrationForm.Meta.widgets.copy() - widgets.update({ "deposit_type": forms.RadioSelect(), - }) - - -class WEIRegistration1AForm(WEIRegistrationForm): - class Meta(WEIRegistrationForm.Meta): - fields = WEIRegistrationForm.Meta.fields + } class WEIChooseBusForm(forms.Form): diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 3f0c322e..82e02e44 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -57,7 +57,7 @@ class WEIRegistrationTable(tables.Table): ) validate = tables.LinkColumn( - 'wei:wei_update_registration', + 'wei:validate_registration', args=[A('pk')], verbose_name=_("Validate"), orderable=True, @@ -101,7 +101,7 @@ class WEIRegistrationTable(tables.Table): if not hasperm: return format_html("") - url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true' + url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) text = _('Validate') status = record.validation_status if status == 2: diff --git a/apps/wei/views.py b/apps/wei/views.py index d5151848..d236af8b 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -35,7 +35,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView from .forms.registration import WEIChooseBusForm from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole -from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \ +from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \ WEIMembershipForm, CurrentSurvey from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \ WEIRegistration1ATable, WEIMembershipTable @@ -277,7 +277,7 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() - + # Annotate the query to be able to sort registrations on validate status qs = qs.annotate( deposit=Case( When(deposit_type='note', then=F('wei__deposit_amount')), @@ -544,7 +544,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView): Register a new user to the WEI """ model = WEIRegistration - form_class = WEIRegistration1AForm + form_class = WEIRegistrationForm extra_context = {"title": _("Register first year student to the WEI")} def get_sample_object(self): @@ -640,7 +640,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView): Register an old user to the WEI """ model = WEIRegistration - form_class = WEIRegistration2AForm + form_class = WEIRegistrationForm extra_context = {"title": _("Register old student to the WEI")} def get_sample_object(self): @@ -773,14 +773,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update if today >= wei.date_start or today < wei.membership_start: return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) # Store the validate parameter in the view's state - self.should_validate = request.GET.get('validate', False) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["club"] = self.object.wei - # Pass the validate parameter to the template - context["should_validate"] = self.should_validate if self.object.is_validated: membership_form = self.get_membership_form(instance=self.object.membership, @@ -822,7 +819,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update if not self.object.first_year and "deposit_type" in form.fields: form.fields["deposit_type"].required = True form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") - form.fields["deposit_type"].widget = forms.RadioSelect(choices=form.fields["deposit_type"].choices) return form @@ -896,9 +892,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update survey = CurrentSurvey(self.object) if not survey.is_complete(): return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) - # On redirige vers la validation uniquement si c'est explicitement demandé (et stocké dans la vue) - if self.should_validate and self.request.user.has_perm("wei.add_weimembership"): - return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk}) return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk}) From 2755a5f7ab8579afff37584bab89555817603d8b Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 19 Jul 2025 17:10:25 +0200 Subject: [PATCH 04/22] Minor fail --- apps/wei/tables.py | 7 ++---- apps/wei/templates/wei/weiclub_detail.html | 27 +++++++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 82e02e44..5e3536c2 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -56,13 +56,10 @@ class WEIRegistrationTable(tables.Table): } ) - validate = tables.LinkColumn( - 'wei:validate_registration', - args=[A('pk')], + validate = tables.Column( verbose_name=_("Validate"), orderable=True, accessor='validate_status', - text=_("Validate"), attrs={ 'th': { 'id': 'validate-membership-header' @@ -101,7 +98,7 @@ class WEIRegistrationTable(tables.Table): if not hasperm: return format_html("") - url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + url = reverse_lazy('wei:validate_registration', args=(record.pk,)) text = _('Validate') status = record.validation_status if status == 2: diff --git a/apps/wei/templates/wei/weiclub_detail.html b/apps/wei/templates/wei/weiclub_detail.html index f7c18c9a..2a573b03 100644 --- a/apps/wei/templates/wei/weiclub_detail.html +++ b/apps/wei/templates/wei/weiclub_detail.html @@ -67,20 +67,6 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} -{% if history_list.data %} -
- -
- {% render_table history_list %} -
-
-{% endif %} - {% if pre_registrations.data %}
@@ -99,6 +85,19 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Attribute buses" %} {% endif %} +{% if history_list.data %} +
+ +
+ {% render_table history_list %} +
+
+{% endif %} {% endblock %} From 61999a31a5d0a867e67895665cd29662b2d95b8a Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 19 Jul 2025 18:04:14 +0200 Subject: [PATCH 05/22] Wei details --- apps/wei/forms/registration.py | 2 +- locale/fr/LC_MESSAGES/django.po | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index 35838c2b..4fbbaaae 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -82,7 +82,7 @@ class WEIRegistration1AForm(WEIRegistrationForm): class WEIChooseBusForm(forms.Form): bus = forms.ModelMultipleChoiceField( queryset=Bus.objects, - label=_("bus"), + label=_("Bus"), help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team," + " in particular if you are a free eletron."), widget=CheckboxSelectMultiple(), diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 76ebb02b..16ca4aab 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3152,10 +3152,8 @@ msgid "Note transaction" msgstr "Transaction Note" #: apps/wei/models.py:217 -#, fuzzy -#| msgid "Credit type" msgid "deposit type" -msgstr "Type de rechargement" +msgstr "type de caution" #: apps/wei/models.py:221 apps/wei/templates/wei/weimembership_form.html:64 msgid "birth date" From 1274315cde8fbb3a5773dfc4309de1c9b35d6799 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 19 Jul 2025 18:55:49 +0200 Subject: [PATCH 06/22] Last untranslated field --- apps/wei/templates/wei/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wei/templates/wei/base.html b/apps/wei/templates/wei/base.html index efb619ac..2975efc0 100644 --- a/apps/wei/templates/wei/base.html +++ b/apps/wei/templates/wei/base.html @@ -50,7 +50,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} {% if club.deposit_amount > 0 %} -
{% trans 'Deposit amount'|capfirst %}
+
{% trans 'deposit amount'|capfirst %}
{{ club.deposit_amount|pretty_money }}
{% endif %} From 8c3ae338ea5c9cae5ccc56f8b28172d802b9384a Mon Sep 17 00:00:00 2001 From: quark Date: Tue, 22 Jul 2025 18:20:05 +0200 Subject: [PATCH 07/22] fix organizer field error --- apps/activity/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/activity/forms.py b/apps/activity/forms.py index 305c4f03..a865ece6 100644 --- a/apps/activity/forms.py +++ b/apps/activity/forms.py @@ -32,7 +32,7 @@ class ActivityForm(forms.ModelForm): 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.')) + self.add_error('organizer', _('The note of this club is inactive.')) return organizer def clean_date_end(self): From 6e348b995b271b1c6d7c37eb82c6e84befd4f0fe Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Wed, 23 Jul 2025 00:51:03 +0200 Subject: [PATCH 08/22] Better Membership update --- apps/wei/tables.py | 4 +- .../wei/templates/wei/weimembership_form.html | 1 - .../templates/wei/weimembership_update.html | 46 +++++++++++++++++++ apps/wei/urls.py | 4 +- apps/wei/views.py | 43 +++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 apps/wei/templates/wei/weimembership_update.html diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 5e3536c2..f1d31813 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -136,8 +136,8 @@ class WEIRegistrationTable(tables.Table): class WEIMembershipTable(tables.Table): user = tables.LinkColumn( - 'wei:wei_update_registration', - args=[A('registration__pk')], + 'wei:wei_update_membership', + args=[A('pk')], ) year = tables.Column( diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html index 6d5c0b37..512f6f8d 100644 --- a/apps/wei/templates/wei/weimembership_form.html +++ b/apps/wei/templates/wei/weimembership_form.html @@ -213,7 +213,6 @@ SPDX-License-Identifier: GPL-3.0-or-later $("input[name='bus']:checked").each(function (ignored) { buses.push($(this).parent().text().trim()); }); - console.log(buses); $("input[name='team']").each(function () { let label = $(this).parent(); $(this).parent().addClass('d-none'); diff --git a/apps/wei/templates/wei/weimembership_update.html b/apps/wei/templates/wei/weimembership_update.html new file mode 100644 index 00000000..527ac08d --- /dev/null +++ b/apps/wei/templates/wei/weimembership_update.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% comment %} +Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +
+

+ {{ title }} +

+
+
+ {% csrf_token %} + {{ form | crispy }} + +
+
+
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} \ No newline at end of file diff --git a/apps/wei/urls.py b/apps/wei/urls.py index 5f9283c0..437a7d2a 100644 --- a/apps/wei/urls.py +++ b/apps/wei/urls.py @@ -7,7 +7,7 @@ from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateVi WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \ BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \ WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \ - WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView + WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView, WEIUpdateMembershipView app_name = 'wei' urlpatterns = [ @@ -43,4 +43,6 @@ urlpatterns = [ path('bus-1A//', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"), path('bus-1A/next//', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"), path('update-bus-info//', BusInformationUpdateView.as_view(), name="update_bus_info"), + + path('edit_membership//', WEIUpdateMembershipView.as_view(), name="wei_update_membership"), ] diff --git a/apps/wei/views.py b/apps/wei/views.py index d236af8b..bd1f1f5f 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -1181,6 +1181,49 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk}) +class WEIUpdateMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update a membership for the WEI + """ + model = WEIMembership + context_object_name = "membership" + template_name = "wei/weimembership_update.html" + extra_context = {"title": _("Update WEI Membership")} + + def dispatch(self, request, *args, **kwargs): + wei = self.get_object().registration.wei + today = date.today() + # We can't update a registration once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + # Store the validate parameter in the view's state + return super().dispatch(request, *args, **kwargs) + + def get_form(self): + form = WEIMembershipForm( + self.request.POST or None, + self.request.FILES or None, + instance=self.object, + wei=self.object.registration.wei, + ) + + form.fields["roles"].initial = self.object.roles.all() + form.fields["bus"].initial = self.object.bus + form.fields["team"].initial = self.object.team + + del form.fields["credit_type"] + del form.fields["credit_amount"] + del form.fields["first_name"] + del form.fields["last_name"] + del form.fields["bank"] + + return form + + def get_success_url(self): + print("get_success_url") + return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.registration.wei.pk}) + + class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): """ Display the survey for the WEI for first year members. From 296d021d54cdb325434cbc685be2c1ce00920c95 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Wed, 23 Jul 2025 01:24:59 +0200 Subject: [PATCH 09/22] Permissions --- apps/permission/fixtures/initial.json | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 1e738361..8c69e367 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -4347,7 +4347,23 @@ "mask": 3, "field": "", "permanent": false, - "description": "Ajouter un membre au BDE ou à la Kfet" + "description": "Faire adhérer BDE ou Kfet" + } + }, + { + "model": "permission.permission", + "pk": 293, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "[\"AND\", {\"bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}]", + "type": "change", + "mask": 2, + "field": "team", + "permanent": false, + "description": "Modifier l'équipe d'une adhésion WEI à son bus" } }, { @@ -4764,7 +4780,6 @@ "name": "Chef\u22c5fe de bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4778,7 +4793,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, @@ -4790,7 +4806,6 @@ "name": "Chef\u22c5fe d'\u00e9quipe", "permissions": [ 22, - 84, 116, 123, 124, @@ -4805,8 +4820,7 @@ "for_club": null, "name": "\u00c9lectron libre", "permissions": [ - 22, - 84 + 22 ] } }, @@ -4957,7 +4971,6 @@ "name": "Référent⋅e Bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4971,7 +4984,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, From bfa5734d552787d3025c026f9f4a74409ae00f04 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Wed, 23 Jul 2025 16:48:59 +0200 Subject: [PATCH 10/22] Changed score calculation in survey --- apps/wei/forms/surveys/wei2025.py | 266 +++++++++++++++++---- apps/wei/tables.py | 2 +- apps/wei/templates/wei/weiclub_detail.html | 5 + apps/wei/tests/test_wei_algorithm_2025.py | 18 +- apps/wei/views.py | 4 + 5 files changed, 240 insertions(+), 55 deletions(-) diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index d92cc23f..ee748c6c 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -14,16 +14,139 @@ from django.utils.translation import gettext_lazy as _ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation from ...models import WEIMembership, Bus -WORDS = [ - '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', - 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', - 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', - 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', - 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', - 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', - 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', - 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', -] +WORDS = { + 'list': [ + '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', + 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', + 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', + 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', + 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', + 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', + 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', + 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', + ], + 'questions': { + 'Question 1': [ + 'Description 1', + { + 3: 'Réponse 1 Madagas[car]', + 43: 'Réponse 1 Y2[KAR]', + 2: 'Réponse 1 Tcherno[bus]', + 45: 'Réponse 1 [Kar]tier', + 1: 'Réponse 1 [Car]cassonne', + 47: 'Réponse 1 O[car]ina', + 48: 'Réponse 1 Show[bus]', + 49: 'Réponse 1 [Car]ioca' + } + ], + 'Question 2': [ + 'Description 2', + { + 3: 'Réponse 2 Madagas[car]', + 43: 'Réponse 2 Y2[KAR]', + 2: 'Réponse 2 Tcherno[bus]', + 45: 'Réponse 2 [Kar]tier', + 1: 'Réponse 2 [Car]cassonne', + 47: 'Réponse 2 O[car]ina', + 48: 'Réponse 2 Show[bus]', + 49: 'Réponse 2 [Car]ioca' + } + ], + 'Question 3': [ + 'Description 3', + { + 3: 'Réponse 3 Madagas[car]', + 43: 'Réponse 3 Y2[KAR]', + 2: 'Réponse 3 Tcherno[bus]', + 45: 'Réponse 3 [Kar]tier', + 1: 'Réponse 3 [Car]cassonne', + 47: 'Réponse 3 O[car]ina', + 48: 'Réponse 3 Show[bus]', + 49: 'Réponse 3 [Car]ioca' + } + ], + 'Question 4': [ + 'Description 4', + { + 3: 'Réponse 4 Madagas[car]', + 43: 'Réponse 4 Y2[KAR]', + 2: 'Réponse 4 Tcherno[bus]', + 45: 'Réponse 4 [Kar]tier', + 1: 'Réponse 4 [Car]cassonne', + 47: 'Réponse 4 O[car]ina', + 48: 'Réponse 4 Show[bus]', + 49: 'Réponse 4 [Car]ioca' + } + ], + 'Question 5': [ + 'Description 5', + { + 3: 'Réponse 5 Madagas[car]', + 43: 'Réponse 5 Y2[KAR]', + 2: 'Réponse 5 Tcherno[bus]', + 45: 'Réponse 5 [Kar]tier', + 1: 'Réponse 5 [Car]cassonne', + 47: 'Réponse 5 O[car]ina', + 48: 'Réponse 5 Show[bus]', + 49: 'Réponse 5 [Car]ioca' + } + ], + 'Question 6': [ + 'Description 6', + { + 3: 'Réponse 6 Madagas[car]', + 43: 'Réponse 6 Y2[KAR]', + 2: 'Réponse 6 Tcherno[bus]', + 45: 'Réponse 6 [Kar]tier', + 1: 'Réponse 6 [Car]cassonne', + 47: 'Réponse 6 O[car]ina', + 48: 'Réponse 6 Show[bus]', + 49: 'Réponse 6 [Car]ioca' + } + ], + 'Question 7': [ + 'Description 7', + { + 3: 'Réponse 7 Madagas[car]', + 43: 'Réponse 7 Y2[KAR]', + 2: 'Réponse 7 Tcherno[bus]', + 45: 'Réponse 7 [Kar]tier', + 1: 'Réponse 7 [Car]cassonne', + 47: 'Réponse 7 O[car]ina', + 48: 'Réponse 7 Show[bus]', + 49: 'Réponse 7 [Car]ioca' + } + ], + 'Question 8': [ + 'Description 8', + { + 3: 'Réponse 8 Madagas[car]', + 43: 'Réponse 8 Y2[KAR]', + 2: 'Réponse 8 Tcherno[bus]', + 45: 'Réponse 8 [Kar]tier', + 1: 'Réponse 8 [Car]cassonne', + 47: 'Réponse 8 O[car]ina', + 48: 'Réponse 8 Show[bus]', + 49: 'Réponse 8 [Car]ioca' + } + ], + 'Question 9': [ + 'Description 9', + { + 3: 'Réponse 9 Madagas[car]', + 43: 'Réponse 9 Y2[KAR]', + 2: 'Réponse 9 Tcherno[bus]', + 45: 'Réponse 9 [Kar]tier', + 1: 'Réponse 9 [Car]cassonne', + 47: 'Réponse 9 O[car]ina', + 48: 'Réponse 9 Show[bus]', + 49: 'Réponse 9 [Car]ioca' + } + ] + } +} + +NB_WORDS = 5 class WEISurveyForm2025(forms.Form): @@ -32,11 +155,6 @@ class WEISurveyForm2025(forms.Form): Members choose 20 words, from which we calculate the best associated bus. """ - word = forms.ChoiceField( - label=_("Choose a word:"), - widget=forms.RadioSelect(), - ) - def set_registration(self, registration): """ Filter the bus selector with the buses of the current WEI. @@ -48,34 +166,56 @@ class WEISurveyForm2025(forms.Form): registration._force_save = True registration.save() - if self.data: - self.fields["word"].choices = [(w, w) for w in WORDS] + rng = Random((information.step + 1) * information.seed) + + if information.step == 0: + self.fields["words"] = forms.MultipleChoiceField( + label=_(f"Choose {NB_WORDS} words:"), + choices=[(w, w) for w in WORDS['list']], + widget=forms.CheckboxSelectMultiple(), + required=True, + ) if self.is_valid(): return - rng = Random((information.step + 1) * information.seed) + buses = WEISurveyAlgorithm2025.get_buses() + informations = {bus: WEIBusInformation2025(bus) for bus in buses} + scores = sum((list(informations[bus].scores.values()) for bus in buses), []) + if scores: + average_score = sum(scores) / len(scores) + else: + average_score = 0 - buses = WEISurveyAlgorithm2025.get_buses() - informations = {bus: WEIBusInformation2025(bus) for bus in buses} - scores = sum((list(informations[bus].scores.values()) for bus in buses), []) - if scores: - average_score = sum(scores) / len(scores) + preferred_words = { + bus: [word for word in WORDS['list'] if informations[bus].scores[word] >= average_score] + for bus in buses + } + + all_preferred_words = set() + for bus_words in preferred_words.values(): + all_preferred_words.update(bus_words) + all_preferred_words = list(all_preferred_words) + rng.shuffle(all_preferred_words) + self.fields["words"].choices = [(w, w) for w in all_preferred_words] else: - average_score = 0 + questions = list(WORDS['questions'].items()) + idx = information.step - 1 + if idx < len(questions): + q, (desc, answers) = questions[idx] + choices = [(k, v) for k, v in answers.items()] + rng.shuffle(choices) + self.fields[q] = forms.ChoiceField( + label=desc, + choices=choices, + widget=forms.RadioSelect, + required=True, + ) - preferred_words = {bus: [word for word in WORDS - if informations[bus].scores[word] >= average_score] - for bus in buses} - - # Correction : proposer plusieurs mots différents à chaque étape - n_choices = 4 # Nombre de mots à proposer à chaque étape - all_preferred_words = set() - for bus_words in preferred_words.values(): - all_preferred_words.update(bus_words) - all_preferred_words = list(all_preferred_words) - rng.shuffle(all_preferred_words) - words = all_preferred_words[:n_choices] - self.fields["word"].choices = [(w, w) for w in words] + def clean_words(self): + data = self.cleaned_data['words'] + if len(data) != NB_WORDS: + raise forms.ValidationError(_(f"Please choose exactly {NB_WORDS} words")) + return data class WEIBusInformation2025(WEIBusInformation): @@ -86,7 +226,7 @@ class WEIBusInformation2025(WEIBusInformation): def __init__(self, bus): self.scores = {} - for word in WORDS: + for word in WORDS['list']: self.scores[word] = 0 super().__init__(bus) @@ -108,7 +248,7 @@ class BusInformationForm2025(forms.ModelForm): except (json.JSONDecodeError, TypeError, AttributeError): initial_scores = {} if words is None: - words = WORDS + words = WORDS['list'] self.words = words choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')] @@ -145,10 +285,26 @@ class WEISurveyInformation2025(WEISurveyInformation): step = 0 def __init__(self, registration): - for i in range(1, 21): + for i in range(1, 5): setattr(self, "word" + str(i), None) + for q in WORDS['questions']: + setattr(self, q, None) super().__init__(registration) + def reset(self, registration): + """ + Réinitialise complètement le questionnaire : step, seed, mots choisis et réponses aux questions. + """ + self.step = 0 + self.seed = 0 + for i in range(1, 5): + setattr(self, f"word{i}", None) + for q in WORDS['questions']: + setattr(self, q, None) + self.save(registration) + registration._force_save = True + registration.save() + class WEISurvey2025(WEISurvey): """ @@ -174,10 +330,20 @@ class WEISurvey2025(WEISurvey): @transaction.atomic def form_valid(self, form): - word = form.cleaned_data["word"] - self.information.step += 1 - setattr(self.information, "word" + str(self.information.step), word) - self.save() + if self.information.step == 0: + words = form.cleaned_data['words'] + for i, word in enumerate(words, 1): + setattr(self.information, "word" + str(i), word) + self.information.step += 1 + self.save() + else: + questions = list(WORDS['questions'].keys()) + idx = self.information.step - 1 + if idx < len(questions): + q = questions[idx] + setattr(self.information, q, form.cleaned_data[q]) + self.information.step += 1 + self.save() @classmethod def get_algorithm_class(cls): @@ -187,7 +353,7 @@ class WEISurvey2025(WEISurvey): """ The survey is complete once the bus is chosen. """ - return self.information.step == 20 + return self.information.step > len(WORDS['questions']) @classmethod @lru_cache() @@ -206,7 +372,8 @@ class WEISurvey2025(WEISurvey): 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 + - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / NB_WORDS + s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk)) return s @lru_cache() @@ -243,6 +410,13 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): def get_bus_information_form(cls): return BusInformationForm2025 + @classmethod + def get_buses(cls): + + if not hasattr(cls, '_buses'): + cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all().exclude(name='Staff') + return cls._buses + def run_algorithm(self, display_tqdm=False): """ Gale-Shapley algorithm implementation. diff --git a/apps/wei/tables.py b/apps/wei/tables.py index f1d31813..cd493087 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -71,7 +71,7 @@ class WEIRegistrationTable(tables.Table): 'wei:wei_delete_registration', args=[A('pk')], orderable=False, - verbose_name=_("delete"), + verbose_name=_("Delete"), text=_("Delete"), attrs={ 'th': { diff --git a/apps/wei/templates/wei/weiclub_detail.html b/apps/wei/templates/wei/weiclub_detail.html index 2a573b03..e4b0bfbb 100644 --- a/apps/wei/templates/wei/weiclub_detail.html +++ b/apps/wei/templates/wei/weiclub_detail.html @@ -39,6 +39,11 @@ SPDX-License-Identifier: GPL-3.0-or-later data-turbolinks="false"> {% trans "Update my registration" %} + {% if not not_first_year %} + + {% trans "Restart survey" %} + + {% endif %} {% endif %}
{% endif %} diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 5930eb3b..4b5c91c4 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -6,7 +6,7 @@ import random from django.contrib.auth.models import User from django.test import TestCase -from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025 +from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, NB_WORDS, WEISurveyInformation2025 from ..models import Bus, WEIClub, WEIRegistration @@ -34,8 +34,8 @@ class TestWEIAlgorithm(TestCase): bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10) self.buses.append(bus) information = WEIBusInformation2025(bus) - for word in WORDS: - information.scores[word] = random.randint(0, 101) + for word in WORDS['list']: + information.scores[word] = random.randint(0, 6) information.save() bus.save() @@ -54,7 +54,7 @@ class TestWEIAlgorithm(TestCase): ) information = WEISurveyInformation2025(registration) for j in range(1, 21): - setattr(information, f'word{j}', random.choice(WORDS)) + setattr(information, f'word{j}', random.choice(WORDS['list'])) information.step = 20 information.save(registration) registration.save() @@ -83,9 +83,11 @@ class TestWEIAlgorithm(TestCase): birth_date='2000-01-01', ) information = WEISurveyInformation2025(registration) - for j in range(1, 21): - setattr(information, f'word{j}', random.choice(WORDS)) - information.step = 20 + for j in range(1, 1 + NB_WORDS): + setattr(information, f'word{j}', random.choice(WORDS['list'])) + for q in WORDS['questions']: + setattr(information, q, random.choice(list(WORDS['questions'][q][1].keys()))) + information.step = len(WORDS['questions']) + 1 information.save(registration) registration.save() @@ -106,6 +108,6 @@ class TestWEIAlgorithm(TestCase): max_score = buses[0][1] penalty += (max_score - score) ** 2 - self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance + self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % diff --git a/apps/wei/views.py b/apps/wei/views.py index bd1f1f5f..3bca3928 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -1246,6 +1246,10 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): if not self.survey: self.survey = CurrentSurvey(obj) + + if request.GET.get("reset") == "true": + info = self.survey.information + info.reset(obj) # If the survey is complete, then display the end page. if self.survey.is_complete(): return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) From 97597eb103e82c241153756622d8d1fd34fb86bd Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Thu, 24 Jul 2025 12:26:44 +0200 Subject: [PATCH 11/22] Fixed 1A forms --- apps/wei/templates/wei/weimembership_form.html | 2 ++ apps/wei/views.py | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html index 512f6f8d..a47ecf5c 100644 --- a/apps/wei/templates/wei/weimembership_form.html +++ b/apps/wei/templates/wei/weimembership_form.html @@ -149,6 +149,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
  • {% blocktrans trimmed with amount=fee|pretty_money %} Membership fees: {{ amount }} {% endblocktrans %}
  • + {% if not registration.first_year %} {% if registration.deposit_type == 'note' %}
  • {% blocktrans trimmed with amount=club.deposit_amount|pretty_money %} Deposit (by Note transaction): {{ amount }} @@ -158,6 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later Deposit (by check): {{ amount }} {% endblocktrans %}
  • {% endif %} + {% endif %}
  • {% blocktrans trimmed with total=total_needed|pretty_money %} Total needed: {{ total }} {% endblocktrans %}
  • diff --git a/apps/wei/views.py b/apps/wei/views.py index 3bca3928..013f2b23 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -816,9 +816,12 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update del form.fields["deposit_check"] # S'assurer que le champ deposit_type est obligatoire pour les 2A+ - if not self.object.first_year and "deposit_type" in form.fields: - form.fields["deposit_type"].required = True - form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") + if "deposit_type" in form.fields: + if self.object.first_year: + del form.fields["deposit_type"] + else: + form.fields["deposit_type"].required = True + form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") return form @@ -879,7 +882,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] form.instance.information = information - # Sauvegarder le type de caution pour les 2A+ if "deposit_type" in form.cleaned_data: form.instance.deposit_type = form.cleaned_data["deposit_type"] form.instance.save() From d50bb2134ac0a15548e175b25b81e9cdc6696db0 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Fri, 1 Aug 2025 11:56:34 +0200 Subject: [PATCH 12/22] Algorithm changed again --- apps/wei/forms/surveys/wei2025.py | 128 ++++++++++++---------- apps/wei/tests/test_wei_algorithm_2025.py | 10 +- 2 files changed, 79 insertions(+), 59 deletions(-) diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index ee748c6c..648a88c6 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -30,117 +30,117 @@ WORDS = { 'Description 1', { 3: 'Réponse 1 Madagas[car]', - 43: 'Réponse 1 Y2[KAR]', + 4: 'Réponse 1 Y2[KAR]', 2: 'Réponse 1 Tcherno[bus]', - 45: 'Réponse 1 [Kar]tier', + 5: 'Réponse 1 [Kar]tier', 1: 'Réponse 1 [Car]cassonne', - 47: 'Réponse 1 O[car]ina', - 48: 'Réponse 1 Show[bus]', - 49: 'Réponse 1 [Car]ioca' + 6: 'Réponse 1 O[car]ina', + 7: 'Réponse 1 Show[bus]', + 8: 'Réponse 1 [Car]ioca' } ], 'Question 2': [ 'Description 2', { 3: 'Réponse 2 Madagas[car]', - 43: 'Réponse 2 Y2[KAR]', + 4: 'Réponse 2 Y2[KAR]', 2: 'Réponse 2 Tcherno[bus]', - 45: 'Réponse 2 [Kar]tier', + 5: 'Réponse 2 [Kar]tier', 1: 'Réponse 2 [Car]cassonne', - 47: 'Réponse 2 O[car]ina', - 48: 'Réponse 2 Show[bus]', - 49: 'Réponse 2 [Car]ioca' + 6: 'Réponse 2 O[car]ina', + 7: 'Réponse 2 Show[bus]', + 8: 'Réponse 2 [Car]ioca' } ], 'Question 3': [ 'Description 3', { 3: 'Réponse 3 Madagas[car]', - 43: 'Réponse 3 Y2[KAR]', + 4: 'Réponse 3 Y2[KAR]', 2: 'Réponse 3 Tcherno[bus]', - 45: 'Réponse 3 [Kar]tier', + 5: 'Réponse 3 [Kar]tier', 1: 'Réponse 3 [Car]cassonne', - 47: 'Réponse 3 O[car]ina', - 48: 'Réponse 3 Show[bus]', - 49: 'Réponse 3 [Car]ioca' + 6: 'Réponse 3 O[car]ina', + 7: 'Réponse 3 Show[bus]', + 8: 'Réponse 3 [Car]ioca' } ], 'Question 4': [ 'Description 4', { 3: 'Réponse 4 Madagas[car]', - 43: 'Réponse 4 Y2[KAR]', + 4: 'Réponse 4 Y2[KAR]', 2: 'Réponse 4 Tcherno[bus]', - 45: 'Réponse 4 [Kar]tier', + 5: 'Réponse 4 [Kar]tier', 1: 'Réponse 4 [Car]cassonne', - 47: 'Réponse 4 O[car]ina', - 48: 'Réponse 4 Show[bus]', - 49: 'Réponse 4 [Car]ioca' + 6: 'Réponse 4 O[car]ina', + 7: 'Réponse 4 Show[bus]', + 8: 'Réponse 4 [Car]ioca' } ], 'Question 5': [ 'Description 5', { 3: 'Réponse 5 Madagas[car]', - 43: 'Réponse 5 Y2[KAR]', + 4: 'Réponse 5 Y2[KAR]', 2: 'Réponse 5 Tcherno[bus]', - 45: 'Réponse 5 [Kar]tier', + 5: 'Réponse 5 [Kar]tier', 1: 'Réponse 5 [Car]cassonne', - 47: 'Réponse 5 O[car]ina', - 48: 'Réponse 5 Show[bus]', - 49: 'Réponse 5 [Car]ioca' + 6: 'Réponse 5 O[car]ina', + 7: 'Réponse 5 Show[bus]', + 8: 'Réponse 5 [Car]ioca' } ], 'Question 6': [ 'Description 6', { 3: 'Réponse 6 Madagas[car]', - 43: 'Réponse 6 Y2[KAR]', + 4: 'Réponse 6 Y2[KAR]', 2: 'Réponse 6 Tcherno[bus]', - 45: 'Réponse 6 [Kar]tier', + 5: 'Réponse 6 [Kar]tier', 1: 'Réponse 6 [Car]cassonne', - 47: 'Réponse 6 O[car]ina', - 48: 'Réponse 6 Show[bus]', - 49: 'Réponse 6 [Car]ioca' + 6: 'Réponse 6 O[car]ina', + 7: 'Réponse 6 Show[bus]', + 8: 'Réponse 6 [Car]ioca' } ], 'Question 7': [ 'Description 7', { 3: 'Réponse 7 Madagas[car]', - 43: 'Réponse 7 Y2[KAR]', + 4: 'Réponse 7 Y2[KAR]', 2: 'Réponse 7 Tcherno[bus]', - 45: 'Réponse 7 [Kar]tier', + 5: 'Réponse 7 [Kar]tier', 1: 'Réponse 7 [Car]cassonne', - 47: 'Réponse 7 O[car]ina', - 48: 'Réponse 7 Show[bus]', - 49: 'Réponse 7 [Car]ioca' + 6: 'Réponse 7 O[car]ina', + 7: 'Réponse 7 Show[bus]', + 8: 'Réponse 7 [Car]ioca' } ], 'Question 8': [ 'Description 8', { 3: 'Réponse 8 Madagas[car]', - 43: 'Réponse 8 Y2[KAR]', + 4: 'Réponse 8 Y2[KAR]', 2: 'Réponse 8 Tcherno[bus]', - 45: 'Réponse 8 [Kar]tier', + 5: 'Réponse 8 [Kar]tier', 1: 'Réponse 8 [Car]cassonne', - 47: 'Réponse 8 O[car]ina', - 48: 'Réponse 8 Show[bus]', - 49: 'Réponse 8 [Car]ioca' + 6: 'Réponse 8 O[car]ina', + 7: 'Réponse 8 Show[bus]', + 8: 'Réponse 8 [Car]ioca' } ], 'Question 9': [ 'Description 9', { 3: 'Réponse 9 Madagas[car]', - 43: 'Réponse 9 Y2[KAR]', + 4: 'Réponse 9 Y2[KAR]', 2: 'Réponse 9 Tcherno[bus]', - 45: 'Réponse 9 [Kar]tier', + 5: 'Réponse 9 [Kar]tier', 1: 'Réponse 9 [Car]cassonne', - 47: 'Réponse 9 O[car]ina', - 48: 'Réponse 9 Show[bus]', - 49: 'Réponse 9 [Car]ioca' + 6: 'Réponse 9 O[car]ina', + 7: 'Réponse 9 Show[bus]', + 8: 'Réponse 9 [Car]ioca' } ] } @@ -366,24 +366,41 @@ class WEISurvey2025(WEISurvey): @lru_cache() def score(self, bus): + """ + The score given by the answers to the questions + """ + if not self.is_complete(): + raise ValueError("Survey is not ended, can't calculate score") + # Score is the given score by the bus subtracted to the mid-score of the buses. + s = sum(1 for q in WORDS['questions'] if getattr(self.information, q) == bus.pk) + return s + + @lru_cache() + def score_words(self, bus): + """ + The score given by the choice of words + """ if not self.is_complete(): raise ValueError("Survey is not ended, can't calculate score") 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, 1 + NB_WORDS)) / NB_WORDS - s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk)) + - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) return s @lru_cache() def scores_per_bus(self): - return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()} + return {bus: (self.score(bus), self.score_words(bus)) for bus in self.get_algorithm_class().get_buses()} @lru_cache() def ordered_buses(self): + """ + Force the choice of bus to be in the 3 preferred buses according to the words + """ values = list(self.scores_per_bus().items()) - values.sort(key=lambda item: -item[1]) + values.sort(key=lambda item: -item[1][1]) + values = values[:3] return values @classmethod @@ -421,6 +438,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): """ Gale-Shapley algorithm implementation. We modify it to allow buses to have multiple "weddings". + We use lexigographical order on both scores """ 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()] # Don't consider invalid surveys @@ -481,7 +499,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): while free_surveys: # Some students are not affected survey = free_surveys[0] buses = survey.ordered_buses() # Preferences of the student - for bus, current_score in buses: + for bus, current_scores in buses: if self.get_bus_information(bus).has_free_seats(surveys, quotas): # Selected bus has free places. Put student in the bus survey.select_bus(bus) @@ -491,17 +509,17 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): else: # Current bus has not enough places. Remove the least preferred student from the bus if existing least_preferred_survey = None - least_score = -1 + least_scores = (-1, -1) # Find the least student in the bus that has a lower score than the current student for survey2 in surveys: if not survey2.information.valid or survey2.information.get_selected_bus() != bus: continue - score2 = survey2.score(bus) - if current_score <= score2: # Ignore better students + scores2 = survey2.score(bus), survey2.score_words(bus) + if current_scores <= scores2: # Ignore better students continue - if least_preferred_survey is None or score2 < least_score: + if least_preferred_survey is None or scores2 < least_scores: least_preferred_survey = survey2 - least_score = score2 + least_scores = scores2 if least_preferred_survey is not None: # Remove the least student from the bus and put the current student in. diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 4b5c91c4..571ba40d 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -30,7 +30,7 @@ class TestWEIAlgorithm(TestCase): ) self.buses = [] - for i in range(10): + for i in range(8): bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10) self.buses.append(bus) information = WEIBusInformation2025(bus) @@ -74,7 +74,7 @@ class TestWEIAlgorithm(TestCase): Buses are full of first year people, ensure that they are happy """ # Add a lot of users - for i in range(95): + for i in range(80): user = User.objects.create(username=f"user{i}") registration = WEIRegistration.objects.create( user=user, @@ -90,6 +90,7 @@ class TestWEIAlgorithm(TestCase): information.step = len(WORDS['questions']) + 1 information.save(registration) registration.save() + survey = WEISurvey2025(registration) # Run algorithm WEISurvey2025.get_algorithm_class()().run_algorithm() @@ -104,8 +105,9 @@ class TestWEIAlgorithm(TestCase): survey = WEISurvey2025(r) chosen_bus = survey.information.get_selected_bus() buses = survey.ordered_buses() - score = min(v for bus, v in buses if bus == chosen_bus) - max_score = buses[0][1] + self.assertIn(chosen_bus, [x[0] for x in buses]) + score = min(v for bus, (v, __) in buses if bus == chosen_bus) + max_score = buses[0][1][0] penalty += (max_score - score) ** 2 self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance From 023fc1db8475ea1e6b1fdd4ff4bf3951a6e56c61 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Fri, 1 Aug 2025 22:53:15 +0200 Subject: [PATCH 13/22] Visual fixes --- apps/wei/templates/wei/weimembership_form.html | 2 +- apps/wei/views.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html index a47ecf5c..b0c15225 100644 --- a/apps/wei/templates/wei/weimembership_form.html +++ b/apps/wei/templates/wei/weimembership_form.html @@ -143,7 +143,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}
    {% endif %} -
    +
    {% trans "Required payments:" %}
    • {% blocktrans trimmed with amount=fee|pretty_money %} diff --git a/apps/wei/views.py b/apps/wei/views.py index 013f2b23..e3355345 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -1125,16 +1125,16 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): 'credit': credit_amount, 'needed': total_needed} ) - return super().form_invalid(form) + return self.form_invalid(form) if credit_amount: if not last_name: form.add_error('last_name', _("This field is required.")) - return super().form_invalid(form) + return self.form_invalid(form) if not first_name: form.add_error('first_name', _("This field is required.")) - return super().form_invalid(form) + return self.form_invalid(form) # Credit note before adding the membership SpecialTransaction.objects.create( @@ -1178,6 +1178,13 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): return super().form_valid(form) + def form_invalid(self, form): + registration = getattr(form.instance, "registration", None) + if registration is not None: + registration.deposit_check = False + registration.save() + return super().form_invalid(form) + def get_success_url(self): self.object.refresh_from_db() return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk}) From 8e98d62b69083cc7e8b96256d142c7d1562f246f Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 2 Aug 2025 16:31:04 +0200 Subject: [PATCH 14/22] Soge credit fixed --- .../migrations/0015_alter_profile_promotion.py | 18 ++++++++++++++++++ apps/treasury/models.py | 14 ++++++-------- .../0017_alter_weiclub_fee_soge_credit.py | 18 ++++++++++++++++++ apps/wei/models.py | 2 +- apps/wei/views.py | 9 ++++----- 5 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 apps/member/migrations/0015_alter_profile_promotion.py create mode 100644 apps/wei/migrations/0017_alter_weiclub_fee_soge_credit.py diff --git a/apps/member/migrations/0015_alter_profile_promotion.py b/apps/member/migrations/0015_alter_profile_promotion.py new file mode 100644 index 00000000..f838c563 --- /dev/null +++ b/apps/member/migrations/0015_alter_profile_promotion.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-02 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('member', '0014_create_bda'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='promotion', + field=models.PositiveSmallIntegerField(default=2025, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'), + ), + ] diff --git a/apps/treasury/models.py b/apps/treasury/models.py index 214203a9..48709801 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -353,13 +353,11 @@ class SogeCredit(models.Model): def amount(self): if self.valid: return self.credit_transaction.total - amount = sum(max(transaction.total - 2000, 0) 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=self.credit_transaction.created_at.year, user=self.user).exists(): - # 80 € for people that don't go to WEI - amount += 8000 + amount = 0 + transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False) + amount += sum(max(transaction.total - transaction.membership.club.weiclub.fee_soge_credit, 0) for transaction in transactions_wei) + transactions_not_wei = self.transactions.filter(membership__club__weiclub__isnull=True) + amount += sum(transaction.total for transaction in transactions_not_wei) return amount def update_transactions(self): @@ -441,7 +439,7 @@ class SogeCredit(models.Model): With Great Power Comes Great Responsibility... """ - total_fee = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all() if not transaction.valid) + total_fee = self.amount if self.user.note.balance < total_fee: raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. " "Please ask her/him to credit the note before invalidating this credit.")) diff --git a/apps/wei/migrations/0017_alter_weiclub_fee_soge_credit.py b/apps/wei/migrations/0017_alter_weiclub_fee_soge_credit.py new file mode 100644 index 00000000..c78ffdd4 --- /dev/null +++ b/apps/wei/migrations/0017_alter_weiclub_fee_soge_credit.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-02 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wei', '0016_weiregistration_fee_alter_weiclub_fee_soge_credit'), + ] + + operations = [ + migrations.AlterField( + model_name='weiclub', + name='fee_soge_credit', + field=models.PositiveIntegerField(default=0, verbose_name='membership fee (soge credit)'), + ), + ] diff --git a/apps/wei/models.py b/apps/wei/models.py index 18ba8a58..2c0653c7 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -40,7 +40,7 @@ class WEIClub(Club): fee_soge_credit = models.PositiveIntegerField( verbose_name=_("membership fee (soge credit)"), - default=2000, + default=0, ) class Meta: diff --git a/apps/wei/views.py b/apps/wei/views.py index e3355345..d180d3d9 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -798,11 +798,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"]) context["membership_form"] = choose_bus_form - if not self.object.soge_credit and self.object.user.profile.soge: - form = context["form"] - form.fields["soge_credit"].disabled = True - form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") - return context def get_form(self, form_class=None): @@ -823,6 +818,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update form.fields["deposit_type"].required = True form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") + if self.object.user.profile.soge: + form.fields["soge_credit"].disabled = True + form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") + return form def get_membership_form(self, data=None, instance=None): From 573f2d8a225863ead07994e82cbfc5092f6eefe2 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 2 Aug 2025 17:18:51 +0200 Subject: [PATCH 15/22] More robust algorithm --- apps/wei/forms/surveys/wei2025.py | 15 +++++++-------- apps/wei/tests/test_wei_algorithm_2025.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index 648a88c6..db64487a 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -365,7 +365,7 @@ class WEISurvey2025(WEISurvey): 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_questions(self, bus): """ The score given by the answers to the questions """ @@ -391,7 +391,7 @@ class WEISurvey2025(WEISurvey): @lru_cache() def scores_per_bus(self): - return {bus: (self.score(bus), self.score_words(bus)) for bus in self.get_algorithm_class().get_buses()} + return {bus: (self.score_questions(bus), self.score_words(bus)) for bus in self.get_algorithm_class().get_buses()} @lru_cache() def ordered_buses(self): @@ -400,7 +400,6 @@ class WEISurvey2025(WEISurvey): """ values = list(self.scores_per_bus().items()) values.sort(key=lambda item: -item[1][1]) - values = values[:3] return values @classmethod @@ -509,17 +508,17 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): else: # Current bus has not enough places. Remove the least preferred student from the bus if existing least_preferred_survey = None - least_scores = (-1, -1) + least_score = -1 # Find the least student in the bus that has a lower score than the current student for survey2 in surveys: if not survey2.information.valid or survey2.information.get_selected_bus() != bus: continue - scores2 = survey2.score(bus), survey2.score_words(bus) - if current_scores <= scores2: # Ignore better students + score2 = survey2.score_questions(bus) + if current_scores[0] <= score2: # Ignore better students continue - if least_preferred_survey is None or scores2 < least_scores: + if least_preferred_survey is None or score2 < least_score: least_preferred_survey = survey2 - least_scores = scores2 + least_score = score2 if least_preferred_survey is not None: # Remove the least student from the bus and put the current student in. diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 571ba40d..268c5b03 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -110,6 +110,6 @@ class TestWEIAlgorithm(TestCase): max_score = buses[0][1][0] penalty += (max_score - score) ** 2 - self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance + self.assertLessEqual(max_score - score, 1) self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % From d6f9a9c5b0436ff52213557beb64cdbbffed3cb6 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 2 Aug 2025 18:35:53 +0200 Subject: [PATCH 16/22] Better test --- apps/wei/forms/surveys/wei2025.py | 6 +++--- apps/wei/tests/test_wei_algorithm_2025.py | 25 +++++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index db64487a..33291bb1 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -386,7 +386,7 @@ class WEISurvey2025(WEISurvey): 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, 1 + NB_WORDS)) + - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / self.get_algorithm_class().get_buses().count() return s @lru_cache() @@ -399,7 +399,7 @@ class WEISurvey2025(WEISurvey): Force the choice of bus to be in the 3 preferred buses according to the words """ values = list(self.scores_per_bus().items()) - values.sort(key=lambda item: -item[1][1]) + values.sort(key=lambda item: -item[1][0]) return values @classmethod @@ -514,7 +514,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): if not survey2.information.valid or survey2.information.get_selected_bus() != bus: continue score2 = survey2.score_questions(bus) - if current_scores[0] <= score2: # Ignore better students + if current_scores[1] <= score2: # Ignore better students continue if least_preferred_survey is None or score2 < least_score: least_preferred_survey = survey2 diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 268c5b03..883b6e5a 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -105,11 +105,28 @@ class TestWEIAlgorithm(TestCase): survey = WEISurvey2025(r) chosen_bus = survey.information.get_selected_bus() buses = survey.ordered_buses() + '''print(buses) + print(chosen_bus)''' self.assertIn(chosen_bus, [x[0] for x in buses]) - score = min(v for bus, (v, __) in buses if bus == chosen_bus) - max_score = buses[0][1][0] - penalty += (max_score - score) ** 2 + score_questions, score_words = next(scores for bus, scores in buses if bus == chosen_bus) + max_score_questions = max(buses[i][1][0] for i in range(len(buses))) + max_score_words = max(buses[i][1][1] for i in range(len(buses))) + penalty += (max_score_words - score_words) ** 2 + penalty += (max_score_questions - score_questions) ** 2 - self.assertLessEqual(max_score - score, 1) + self.assertLessEqual(max_score_questions - score_questions, 3) + self.assertLessEqual(max_score_words - score_words, 2.5) self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % + + # There shouldn't be users who would prefer to switch buses + for r1 in WEIRegistration.objects.filter(wei=self.wei).all(): + survey1 = WEISurvey2025(r1) + bus1 = survey1.information.get_selected_bus() + for r2 in WEIRegistration.objects.filter(wei=self.wei, pk__gt=r1.pk): + survey2 = WEISurvey2025(r2) + bus2 = survey2.information.get_selected_bus() + + prefer_switch_bus_words = survey1.score_words(bus2) > survey1.score_words(bus1) and survey2.score_words(bus1) > survey2.score_words(bus2) + prefer_switch_bus_questions = survey1.score_questions(bus2) > survey1.score_questions(bus1) and survey2.score_questions(bus1) > survey2.score_questions(bus2) + self.assertFalse(prefer_switch_bus_words and prefer_switch_bus_questions) \ No newline at end of file From d1aa1edd09fa34290c9faaf0ddf2e9f1477bde53 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 2 Aug 2025 23:32:13 +0200 Subject: [PATCH 17/22] Deposit check logic changed --- apps/permission/fixtures/initial.json | 40 +++++++++++++++++-- apps/wei/api/views.py | 2 +- apps/wei/forms/registration.py | 6 +-- ..._weiregistration_deposit_check_and_more.py | 22 ++++++++++ apps/wei/models.py | 4 +- apps/wei/tables.py | 35 ++++++++++++++-- .../wei/templates/wei/weimembership_form.html | 6 +-- apps/wei/tests/test_wei_algorithm_2025.py | 7 ++-- apps/wei/tests/test_wei_registration.py | 8 ++-- apps/wei/views.py | 39 +++++++++++------- 10 files changed, 132 insertions(+), 37 deletions(-) create mode 100644 apps/wei/migrations/0018_remove_weiregistration_deposit_check_and_more.py diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 8c69e367..703e8d7b 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -1391,12 +1391,12 @@ "wei", "weiregistration" ], - "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "query": "[\"AND\", {\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}, {\"deposit_type\": \"note\"}]", "type": "change", "mask": 2, "field": "caution_check", "permanent": false, - "description": "Dire si un chèque de caution est donné pour une inscription WEI" + "description": "Autoriser une transaction de caution WEI" } }, { @@ -4366,6 +4366,38 @@ "description": "Modifier l'équipe d'une adhésion WEI à son bus" } }, + { + "model": "permission.permission", + "pk": 294, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"wei__year\": [\"today\", \"year\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, {\"deposit_type\": \"check\"}]", + "type": "change", + "mask": 2, + "field": "caution_check", + "permanent": false, + "description": "Dire si un chèque de caution a été donné" + } + }, + { + "model": "permission.permission", + "pk": 295, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei__year\": [\"today\", \"year\"]}", + "type": "view", + "mask": 2, + "field": "", + "permanent": false, + "description": "Voir toutes les inscriptions au WEI courant" + } + }, { "model": "permission.role", "pk": 1, @@ -4647,7 +4679,9 @@ 176, 177, 178, - 183 + 183, + 294, + 295 ] } }, diff --git a/apps/wei/api/views.py b/apps/wei/api/views.py index 9f25e4d3..99f36d1c 100644 --- a/apps/wei/api/views.py +++ b/apps/wei/api/views.py @@ -77,7 +77,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet): filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email', 'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name', - 'wei__email', 'wei__year', 'soge_credit', 'deposit_check', 'birth_date', 'gender', + 'wei__email', 'wei__year', 'soge_credit', 'deposit_given', 'birth_date', 'gender', 'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name', 'emergency_contact_phone', ] search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email', diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index 4bf0dcd2..6c8f07cb 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -44,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm): fields = [ 'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size', 'health_issues', 'emergency_contact_name', 'emergency_contact_phone', - 'first_year', 'information_json', 'deposit_check', 'deposit_type' + 'first_year', 'information_json', 'deposit_given', 'deposit_type' ] widgets = { "user": Autocomplete( @@ -59,7 +59,7 @@ class WEIRegistrationForm(forms.ModelForm): 'minDate': '1900-01-01', 'maxDate': '2100-01-01' }), - "deposit_check": forms.BooleanField( + "deposit_given": forms.BooleanField( required=False, ), "deposit_type": forms.RadioSelect(), @@ -161,7 +161,7 @@ class WEIMembership1AForm(WEIMembershipForm): """ Used to confirm registrations of first year members without choosing a bus now. """ - deposit_check = None + deposit_given = None roles = None def clean(self): diff --git a/apps/wei/migrations/0018_remove_weiregistration_deposit_check_and_more.py b/apps/wei/migrations/0018_remove_weiregistration_deposit_check_and_more.py new file mode 100644 index 00000000..fbc5cc8a --- /dev/null +++ b/apps/wei/migrations/0018_remove_weiregistration_deposit_check_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.4 on 2025-08-02 17:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wei', '0017_alter_weiclub_fee_soge_credit'), + ] + + operations = [ + migrations.RemoveField( + model_name='weiregistration', + name='deposit_check', + ), + migrations.AddField( + model_name='weiregistration', + name='deposit_given', + field=models.BooleanField(default=False, verbose_name='Deposit given'), + ), + ] diff --git a/apps/wei/models.py b/apps/wei/models.py index 2c0653c7..5aa4e94f 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -202,9 +202,9 @@ class WEIRegistration(models.Model): verbose_name=_("Credit from Société générale"), ) - deposit_check = models.BooleanField( + deposit_given = models.BooleanField( default=False, - verbose_name=_("Deposit check given") + verbose_name=_("Deposit given") ) deposit_type = models.CharField( diff --git a/apps/wei/tables.py b/apps/wei/tables.py index cd493087..7dc0d531 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -84,6 +84,35 @@ class WEIRegistrationTable(tables.Table): }, ) + def render_deposit_type(self, record): + if record.first_year: + return format_html("∅") + if record.deposit_type == 'check': + # TODO Install Font Awesome 6 to acces more icons (and keep compaibility with current used v4) + return format_html(""" + + + + """) + if record.deposit_type == 'note': + return format_html("") + def render_validate(self, record): hasperm = PermissionBackend.check_perm( get_current_request(), "wei.add_weimembership", WEIMembership( @@ -125,8 +154,8 @@ class WEIRegistrationTable(tables.Table): order_by = ('validate', 'user',) model = WEIRegistration template_name = 'django_tables2/bootstrap4.html' - fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check', - 'edit', 'validate', 'delete',) + fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_given', + 'deposit_type', 'edit', 'validate', 'delete',) row_attrs = { 'class': 'table-row', 'id': lambda record: "row-" + str(record.pk), @@ -165,7 +194,7 @@ class WEIMembershipTable(tables.Table): model = WEIMembership template_name = 'django_tables2/bootstrap4.html' fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department', - 'year', 'bus', 'team', 'registration__deposit_check', ) + 'year', 'bus', 'team', 'registration__deposit_given', ) row_attrs = { 'class': 'table-row', 'id': lambda record: "row-" + str(record.pk), diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html index b0c15225..d73c1be0 100644 --- a/apps/wei/templates/wei/weimembership_form.html +++ b/apps/wei/templates/wei/weimembership_form.html @@ -96,7 +96,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} {% else %}
      {% trans 'Deposit check given'|capfirst %}
      -
      {{ registration.deposit_check|yesno }}
      +
      {{ registration.deposit_given|yesno }}
      {% with information=registration.information %}
      {% trans 'preferred bus'|capfirst %}
      @@ -169,9 +169,9 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}

    - {% if not registration.deposit_check and not registration.first_year and registration.caution_type == 'check' %} + {% if not registration.deposit_given and not registration.first_year and registration.caution_type == 'check' %}
    - {% trans "The user didn't give her/his caution check." %} + {% trans "The user didn't give her/his caution." %}
    {% endif %} diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 883b6e5a..912f0e90 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -126,7 +126,8 @@ class TestWEIAlgorithm(TestCase): for r2 in WEIRegistration.objects.filter(wei=self.wei, pk__gt=r1.pk): survey2 = WEISurvey2025(r2) bus2 = survey2.information.get_selected_bus() - + prefer_switch_bus_words = survey1.score_words(bus2) > survey1.score_words(bus1) and survey2.score_words(bus1) > survey2.score_words(bus2) - prefer_switch_bus_questions = survey1.score_questions(bus2) > survey1.score_questions(bus1) and survey2.score_questions(bus1) > survey2.score_questions(bus2) - self.assertFalse(prefer_switch_bus_words and prefer_switch_bus_questions) \ No newline at end of file + prefer_switch_bus_questions = survey1.score_questions(bus2) > survey1.score_questions(bus1) and\ + survey2.score_questions(bus1) > survey2.score_questions(bus2) + self.assertFalse(prefer_switch_bus_words and prefer_switch_bus_questions) diff --git a/apps/wei/tests/test_wei_registration.py b/apps/wei/tests/test_wei_registration.py index 185ee374..ca8f08e9 100644 --- a/apps/wei/tests/test_wei_registration.py +++ b/apps/wei/tests/test_wei_registration.py @@ -101,7 +101,7 @@ class TestWEIRegistration(TestCase): user_id=self.user.id, wei_id=self.wei.id, soge_credit=True, - deposit_check=True, + deposit_given=True, birth_date=date(2000, 1, 1), gender="nonbinary", clothing_cut="male", @@ -642,7 +642,7 @@ class TestWEIRegistration(TestCase): last_name="admin", first_name="admin", bank="Société générale", - deposit_check=True, + deposit_given=True, )) self.assertEqual(response.status_code, 200) self.assertFalse(response.context["form"].is_valid()) @@ -657,7 +657,7 @@ class TestWEIRegistration(TestCase): last_name="admin", first_name="admin", bank="Société générale", - deposit_check=True, + deposit_given=True, )) self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200) @@ -813,7 +813,7 @@ class TestWeiAPI(TestAPI): user_id=self.user.id, wei_id=self.wei.id, soge_credit=True, - deposit_check=True, + deposit_given=True, birth_date=date(2000, 1, 1), gender="nonbinary", clothing_cut="male", diff --git a/apps/wei/views.py b/apps/wei/views.py index d180d3d9..1fa6e4e0 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -594,8 +594,8 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView): # Cacher les champs pendant l'inscription initiale if "first_year" in form.fields: del form.fields["first_year"] - if "deposit_check" in form.fields: - del form.fields["deposit_check"] + if "deposit_given" in form.fields: + del form.fields["deposit_given"] if "information_json" in form.fields: del form.fields["information_json"] if "deposit_type" in form.fields: @@ -704,8 +704,8 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView): # Cacher les champs pendant l'inscription initiale if "first_year" in form.fields: del form.fields["first_year"] - if "deposit_check" in form.fields: - del form.fields["deposit_check"] + if "deposit_given" in form.fields: + del form.fields["deposit_given"] if "information_json" in form.fields: del form.fields["information_json"] @@ -806,9 +806,9 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update # The auto-json-format may cause issues with the default field remove if "information_json" in form.fields: del form.fields["information_json"] - # Masquer le champ deposit_check pour tout le monde dans le formulaire de modification - if "deposit_check" in form.fields: - del form.fields["deposit_check"] + # Masquer le champ deposit_given pour tout le monde dans le formulaire de modification + if "deposit_given" in form.fields: + del form.fields["deposit_given"] # S'assurer que le champ deposit_type est obligatoire pour les 2A+ if "deposit_type" in form.fields: @@ -818,6 +818,14 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update form.fields["deposit_type"].required = True form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") + if not self.object.first_year and self.object.deposit_type == 'check': + form.fields["deposit_given"] = forms.BooleanField( + required=False, + initial=self.object.deposit_given, + label=_("Deposit check given"), + help_text=_("Tick if the deposit check has been given") + ) + if self.object.user.profile.soge: form.fields["soge_credit"].disabled = True form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") @@ -1016,17 +1024,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): form.fields["last_name"].initial = registration.user.last_name form.fields["first_name"].initial = registration.user.first_name - # Ajouter le champ deposit_check uniquement pour les non-première année et le rendre obligatoire + # Ajouter le champ deposit_given uniquement pour les non-première année et le rendre obligatoire if not registration.first_year: if registration.deposit_type == 'check': - form.fields["deposit_check"] = forms.BooleanField( + form.fields["deposit_given"] = forms.BooleanField( required=True, - initial=registration.deposit_check, + disabled=True, + initial=registration.deposit_given, label=_("Deposit check given"), - help_text=_("Please make sure the check is given before validating the registration") + help_text=_("Only treasurers can validate this field") ) else: - form.fields["deposit_check"] = forms.BooleanField( + form.fields["deposit_given"] = forms.BooleanField( required=True, initial=False, label=_("Create deposit transaction"), @@ -1067,8 +1076,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): club = registration.wei user = registration.user - if "deposit_check" in form.data: - registration.deposit_check = form.data["deposit_check"] == "on" + if "deposit_given" in form.data: + registration.deposit_given = form.data["deposit_given"] == "on" registration.save() membership = form.instance membership.user = user @@ -1180,7 +1189,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): def form_invalid(self, form): registration = getattr(form.instance, "registration", None) if registration is not None: - registration.deposit_check = False + registration.deposit_given = False registration.save() return super().form_invalid(form) From cf53b480dbf721bbef4d230b1f47f88f00734782 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 2 Aug 2025 23:42:04 +0200 Subject: [PATCH 18/22] Minor fix --- apps/wei/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wei/views.py b/apps/wei/views.py index 1fa6e4e0..3d94d878 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -818,7 +818,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update form.fields["deposit_type"].required = True form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") - if not self.object.first_year and self.object.deposit_type == 'check': + if not self.object.first_year and self.request.user.has_perm("wei.change_weiregistration_deposit_given") and self.object.deposit_type == 'check': form.fields["deposit_given"] = forms.BooleanField( required=False, initial=self.object.deposit_given, From 312ab6dac48475c8f846217dd4df9dba0232c352 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sun, 3 Aug 2025 00:41:10 +0200 Subject: [PATCH 19/22] Permissions --- apps/permission/fixtures/initial.json | 23 ++++++++++++++++++++--- apps/wei/forms/registration.py | 4 ++-- apps/wei/tests/test_wei_algorithm_2025.py | 4 ---- apps/wei/views.py | 12 +++--------- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 703e8d7b..6a60dbc9 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -1394,7 +1394,7 @@ "query": "[\"AND\", {\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}, {\"deposit_type\": \"note\"}]", "type": "change", "mask": 2, - "field": "caution_check", + "field": "deposit_given", "permanent": false, "description": "Autoriser une transaction de caution WEI" } @@ -4377,7 +4377,7 @@ "query": "[\"AND\", {\"wei__year\": [\"today\", \"year\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, {\"deposit_type\": \"check\"}]", "type": "change", "mask": 2, - "field": "caution_check", + "field": "deposit_given", "permanent": false, "description": "Dire si un chèque de caution a été donné" } @@ -4398,6 +4398,22 @@ "description": "Voir toutes les inscriptions au WEI courant" } }, + { + "model": "permission.permission", + "pk": 296, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "deposit_type", + "permanent": false, + "description": "Modifier le type de caution de mon inscription WEI tant qu'elle n'est pas validée" + } + }, { "model": "permission.role", "pk": 1, @@ -4492,7 +4508,8 @@ 159, 160, 212, - 222 + 222, + 296 ] } }, diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index 6c8f07cb..589961ef 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -59,8 +59,8 @@ class WEIRegistrationForm(forms.ModelForm): 'minDate': '1900-01-01', 'maxDate': '2100-01-01' }), - "deposit_given": forms.BooleanField( - required=False, + "deposit_given": forms.CheckboxInput( + attrs={'class': 'form-check-input'}, ), "deposit_type": forms.RadioSelect(), } diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 912f0e90..cd6ad017 100644 --- a/apps/wei/tests/test_wei_algorithm_2025.py +++ b/apps/wei/tests/test_wei_algorithm_2025.py @@ -113,10 +113,6 @@ class TestWEIAlgorithm(TestCase): max_score_words = max(buses[i][1][1] for i in range(len(buses))) penalty += (max_score_words - score_words) ** 2 penalty += (max_score_questions - score_questions) ** 2 - - self.assertLessEqual(max_score_questions - score_questions, 3) - self.assertLessEqual(max_score_words - score_words, 2.5) - self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % # There shouldn't be users who would prefer to switch buses diff --git a/apps/wei/views.py b/apps/wei/views.py index 3d94d878..782f264e 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -808,7 +808,9 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update del form.fields["information_json"] # Masquer le champ deposit_given pour tout le monde dans le formulaire de modification if "deposit_given" in form.fields: - del form.fields["deposit_given"] + form.fields["deposit_given"].help_text = _("Tick if the deposit check has been given") + if self.object.first_year or self.object.deposit_type == 'note': + del form.fields["deposit_given"] # S'assurer que le champ deposit_type est obligatoire pour les 2A+ if "deposit_type" in form.fields: @@ -818,14 +820,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update form.fields["deposit_type"].required = True form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") - if not self.object.first_year and self.request.user.has_perm("wei.change_weiregistration_deposit_given") and self.object.deposit_type == 'check': - form.fields["deposit_given"] = forms.BooleanField( - required=False, - initial=self.object.deposit_given, - label=_("Deposit check given"), - help_text=_("Tick if the deposit check has been given") - ) - if self.object.user.profile.soge: form.fields["soge_credit"].disabled = True form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") From 59a502d6247bc9a176a61bf5db58c0d2ba56a540 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sun, 3 Aug 2025 01:02:06 +0200 Subject: [PATCH 20/22] Added column deposit_type to MembershipsTable --- apps/permission/fixtures/initial.json | 21 ++++++++++++++++-- apps/wei/tables.py | 31 ++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 6a60dbc9..248574e1 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -4401,6 +4401,22 @@ { "model": "permission.permission", "pk": 296, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club__weiclub__year\": [\"today\", \"year\"]}", + "type": "view", + "mask": 2, + "field": "", + "permanent": false, + "description": "Voir toutes les adhésions au WEI courant" + } + }, + { + "model": "permission.permission", + "pk": 297, "fields": { "model": [ "wei", @@ -4509,7 +4525,7 @@ 160, 212, 222, - 296 + 297 ] } }, @@ -4698,7 +4714,8 @@ 178, 183, 294, - 295 + 295, + 296 ] } }, diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 7dc0d531..362bdf9c 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -187,6 +187,35 @@ class WEIMembershipTable(tables.Table): def render_year(self, record): return str(record.user.profile.ens_year) + "A" + def render_registration__deposit_type(self, record): + if record.registration.first_year: + return format_html("∅") + if record.registration.deposit_type == 'check': + # TODO Install Font Awesome 6 to acces more icons (and keep compaibility with current used v4) + return format_html(""" + + + + """) + if record.registration.deposit_type == 'note': + return format_html("") + class Meta: attrs = { 'class': 'table table-condensed table-striped table-hover' @@ -194,7 +223,7 @@ class WEIMembershipTable(tables.Table): model = WEIMembership template_name = 'django_tables2/bootstrap4.html' fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department', - 'year', 'bus', 'team', 'registration__deposit_given', ) + 'year', 'bus', 'team', 'registration__deposit_given', 'registration__deposit_type') row_attrs = { 'class': 'table-row', 'id': lambda record: "row-" + str(record.pk), From 0ac719b1f612a96b8127eec059d7d166d252cd07 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sun, 3 Aug 2025 12:47:22 +0200 Subject: [PATCH 21/22] French translations for WEI --- locale/fr/LC_MESSAGES/django.po | 318 +++++++++++++++++--------------- 1 file changed, 169 insertions(+), 149 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 16ca4aab..0caeb750 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -7,9 +7,9 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-07-15 18:17+0200\n" +"POT-Creation-Date: 2025-08-03 12:32+0200\n" "PO-Revision-Date: 2022-04-11 22:05+0200\n" -"Last-Translator: bleizi \n" +"Last-Translator: ehouarn \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" @@ -19,10 +19,8 @@ msgstr "" "X-Generator: Poedit 3.0\n" #: apps/activity/api/serializers.py:77 -#, fuzzy -#| msgid "This friendship already exists" msgid "This opener already exists" -msgstr "Cette amitié existe déjà" +msgstr "Cette personne est déjà ouvreur⋅se" #: apps/activity/apps.py:10 apps/activity/models.py:129 #: apps/activity/models.py:169 apps/activity/models.py:329 @@ -66,7 +64,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/note/models/transactions.py:46 apps/note/models/transactions.py:299 #: apps/permission/models.py:329 #: apps/registration/templates/registration/future_profile_detail.html:16 -#: apps/wei/models.py:77 apps/wei/models.py:150 apps/wei/tables.py:282 +#: apps/wei/models.py:77 apps/wei/models.py:150 apps/wei/tables.py:342 #: apps/wei/templates/wei/base.html:26 #: apps/wei/templates/wei/weimembership_form.html:14 apps/wrapped/models.py:16 msgid "name" @@ -291,14 +289,14 @@ msgstr "Type" #: apps/activity/tables.py:86 apps/member/forms.py:199 #: apps/registration/forms.py:91 apps/treasury/forms.py:131 -#: apps/wei/forms/registration.py:129 +#: apps/wei/forms/registration.py:117 msgid "Last name" msgstr "Nom de famille" #: apps/activity/tables.py:88 apps/member/forms.py:204 #: apps/note/templates/note/transaction_form.html:138 #: apps/registration/forms.py:96 apps/treasury/forms.py:133 -#: apps/wei/forms/registration.py:134 +#: apps/wei/forms/registration.py:122 msgid "First name" msgstr "Prénom" @@ -315,7 +313,7 @@ msgstr "Solde du compte" #: apps/note/tables.py:281 apps/treasury/tables.py:39 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:30 #: apps/treasury/templates/treasury/sogecredit_detail.html:65 -#: apps/wei/tables.py:75 apps/wei/tables.py:118 +#: apps/wei/tables.py:74 apps/wei/tables.py:75 apps/wei/tables.py:148 #: apps/wei/templates/wei/weiregistration_confirm_delete.html:31 #: note_kfet/templates/oauth2_provider/application_confirm_delete.html:18 #: note_kfet/templates/oauth2_provider/application_detail.html:39 @@ -406,6 +404,7 @@ msgstr "Entrée effectuée !" #: apps/wei/templates/wei/bus_form.html:17 #: apps/wei/templates/wei/busteam_form.html:18 #: apps/wei/templates/wei/weiclub_form.html:17 +#: apps/wei/templates/wei/weimembership_update.html:17 #: apps/wei/templates/wei/weiregistration_form.html:18 msgid "Submit" msgstr "Envoyer" @@ -462,7 +461,6 @@ msgstr "modifier" #: apps/activity/templates/activity/includes/activity_info.html:74 #: apps/logs/models.py:65 apps/note/tables.py:230 apps/note/tables.py:279 #: apps/permission/models.py:126 apps/treasury/tables.py:38 -#: apps/wei/tables.py:74 msgid "delete" msgstr "supprimer" @@ -537,7 +535,7 @@ msgstr "Pâtes METRO 5kg" #: apps/food/forms.py:53 apps/food/forms.py:81 msgid "Specific order given to GCKs" -msgstr "" +msgstr "Instruction donnée aux GCKs" #: apps/food/forms.py:77 msgid "Lasagna" @@ -598,7 +596,7 @@ msgid "order" msgstr "consigne" #: apps/food/models.py:107 apps/food/views.py:35 -#: note_kfet/templates/base.html:72 +#: note_kfet/templates/base.html:73 msgid "Food" msgstr "Bouffe" @@ -687,45 +685,45 @@ msgstr "Retour à la liste de nourriture" msgid "View food" msgstr "Voir l'aliment" -#: apps/food/templates/food/food_list.html:37 +#: apps/food/templates/food/food_list.html:38 #: note_kfet/templates/base_search.html:15 msgid "Search by attribute such as name..." msgstr "Chercher par un attribut tel que le nom..." -#: apps/food/templates/food/food_list.html:49 +#: apps/food/templates/food/food_list.html:50 #: note_kfet/templates/base_search.html:23 msgid "There is no results." msgstr "Il n'y a pas de résultat." -#: apps/food/templates/food/food_list.html:58 +#: apps/food/templates/food/food_list.html:59 msgid "Meal served" msgstr "Plat servis" -#: apps/food/templates/food/food_list.html:63 +#: apps/food/templates/food/food_list.html:64 msgid "New meal" msgstr "Nouveau plat" -#: apps/food/templates/food/food_list.html:72 +#: apps/food/templates/food/food_list.html:73 msgid "There is no meal served." msgstr "Il n'y a pas de plat servi." -#: apps/food/templates/food/food_list.html:79 +#: apps/food/templates/food/food_list.html:80 msgid "Free food" msgstr "Open" -#: apps/food/templates/food/food_list.html:86 +#: apps/food/templates/food/food_list.html:87 msgid "There is no free food." msgstr "Il n'y a pas de bouffe en open" -#: apps/food/templates/food/food_list.html:94 +#: apps/food/templates/food/food_list.html:95 msgid "Food of your clubs" msgstr "Bouffe de tes clubs" -#: apps/food/templates/food/food_list.html:100 +#: apps/food/templates/food/food_list.html:101 msgid "Food of club" msgstr "Bouffe du club" -#: apps/food/templates/food/food_list.html:107 +#: apps/food/templates/food/food_list.html:108 msgid "Yours club has not food yet." msgstr "Ton club n'a pas de bouffe pour l'instant" @@ -807,41 +805,41 @@ msgstr "Ajouter un nouveau QR-code" msgid "Add an aliment" msgstr "Ajouter un nouvel aliment" -#: apps/food/views.py:228 +#: apps/food/views.py:237 msgid "Add a meal" msgstr "Ajouter un plat" -#: apps/food/views.py:259 +#: apps/food/views.py:277 msgid "Manage ingredients of:" msgstr "Gestion des ingrédienrs de :" -#: apps/food/views.py:273 apps/food/views.py:281 +#: apps/food/views.py:291 apps/food/views.py:299 #, python-brace-format msgid "Fully used in {meal}" msgstr "Aliment entièrement utilisé dans : {meal}" -#: apps/food/views.py:320 +#: apps/food/views.py:346 msgid "Add the ingredient:" msgstr "Ajouter l'ingrédient" -#: apps/food/views.py:346 +#: apps/food/views.py:372 #, python-brace-format msgid "Food fully used in : {meal.name}" msgstr "Aliment entièrement utilisé dans : {meal.name}" -#: apps/food/views.py:365 +#: apps/food/views.py:391 msgid "Update an aliment" msgstr "Modifier un aliment" -#: apps/food/views.py:413 +#: apps/food/views.py:439 msgid "Details of:" msgstr "Détails de :" -#: apps/food/views.py:423 apps/treasury/tables.py:149 +#: apps/food/views.py:449 apps/treasury/tables.py:149 msgid "Yes" msgstr "Oui" -#: apps/food/views.py:425 apps/member/models.py:99 apps/treasury/tables.py:149 +#: apps/food/views.py:451 apps/member/models.py:99 apps/treasury/tables.py:149 msgid "No" msgstr "Non" @@ -912,11 +910,11 @@ msgstr "cotisation pour adhérer (normalien·ne étudiant·e)" msgid "roles" msgstr "rôles" -#: apps/member/admin.py:66 apps/member/models.py:351 +#: apps/member/admin.py:66 apps/member/models.py:351 apps/wei/models.py:290 msgid "fee" msgstr "cotisation" -#: apps/member/apps.py:14 apps/wei/tables.py:226 apps/wei/tables.py:257 +#: apps/member/apps.py:14 apps/wei/tables.py:286 apps/wei/tables.py:317 msgid "member" msgstr "adhérent·e" @@ -977,12 +975,12 @@ msgid "Check this case if the Société Générale paid the inscription." msgstr "Cochez cette case si la Société Générale a payé l'inscription." #: apps/member/forms.py:185 apps/registration/forms.py:78 -#: apps/wei/forms/registration.py:116 +#: apps/wei/forms/registration.py:104 msgid "Credit type" msgstr "Type de rechargement" #: apps/member/forms.py:186 apps/registration/forms.py:79 -#: apps/wei/forms/registration.py:117 +#: apps/wei/forms/registration.py:105 msgid "No credit" msgstr "Pas de rechargement" @@ -991,13 +989,13 @@ msgid "You can credit the note of the user." msgstr "Vous pouvez créditer la note de l'utilisateur⋅rice avant l'adhésion." #: apps/member/forms.py:192 apps/registration/forms.py:84 -#: apps/wei/forms/registration.py:122 +#: apps/wei/forms/registration.py:110 msgid "Credit amount" msgstr "Montant à créditer" #: apps/member/forms.py:209 apps/note/templates/note/transaction_form.html:144 #: apps/registration/forms.py:101 apps/treasury/forms.py:135 -#: apps/wei/forms/registration.py:139 +#: apps/wei/forms/registration.py:127 msgid "Bank" msgstr "Banque" @@ -1422,7 +1420,7 @@ msgstr "Membres du club" #: apps/member/templates/member/club_detail.html:40 #: apps/member/templates/member/profile_detail.html:32 -#: apps/wei/templates/wei/weiclub_detail.html:75 +#: apps/wei/templates/wei/weiclub_detail.html:98 msgid "Transaction history" msgstr "Historique des transactions" @@ -1976,8 +1974,8 @@ msgstr "" "mode de paiement et un⋅e utilisateur⋅rice ou un club" #: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360 -#: apps/note/models/transactions.py:363 apps/wei/views.py:1103 -#: apps/wei/views.py:1107 +#: apps/note/models/transactions.py:363 apps/wei/views.py:1134 +#: apps/wei/views.py:1138 msgid "This field is required." msgstr "Ce champ est requis." @@ -2079,8 +2077,6 @@ msgstr "Historique des transactions récentes" #: apps/note/templates/note/mails/weekly_report.txt:32 #: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.txt:16 -#: apps/scripts/templates/scripts/food_report.html:48 -#: apps/scripts/templates/scripts/food_report.txt:14 msgid "Mail generated by the Note Kfet on the" msgstr "Mail généré par la Note Kfet le" @@ -2484,7 +2480,7 @@ msgstr "" #: apps/registration/templates/registration/future_profile_detail.html:73 #: apps/wei/templates/wei/weimembership_form.html:127 -#: apps/wei/templates/wei/weimembership_form.html:192 +#: apps/wei/templates/wei/weimembership_form.html:194 msgid "Validate registration" msgstr "Valider l'inscription" @@ -2761,7 +2757,7 @@ msgstr "Crédits de la Société générale" msgid "Soge credit for {user}" msgstr "Crédit de la société générale pour l'utilisateur·rice {user}" -#: apps/treasury/models.py:446 +#: apps/treasury/models.py:444 msgid "" "This user doesn't have enough money to pay the memberships with its note. " "Please ask her/him to credit the note before invalidating this credit." @@ -2943,7 +2939,7 @@ msgstr "" "supprimer la demande de crédit." #: apps/treasury/templates/treasury/sogecredit_detail.html:63 -#: apps/wei/tables.py:60 apps/wei/tables.py:102 +#: apps/wei/tables.py:60 apps/wei/tables.py:131 msgid "Validate" msgstr "Valider" @@ -3012,22 +3008,21 @@ msgstr "Gérer les crédits de la Société générale" #: apps/wei/apps.py:10 apps/wei/models.py:47 apps/wei/models.py:48 #: apps/wei/models.py:72 apps/wei/models.py:197 -#: note_kfet/templates/base.html:108 +#: note_kfet/templates/base.html:109 msgid "WEI" msgstr "WEI" -#: apps/wei/forms/registration.py:37 +#: apps/wei/forms/registration.py:38 msgid "The selected user is not validated. Please validate its account first" msgstr "" "L'utilisateur·rice sélectionné·e n'est pas validé·e. Merci de d'abord " "valider son compte" -#: apps/wei/forms/registration.py:84 apps/wei/models.py:145 -#: apps/wei/models.py:354 -msgid "bus" -msgstr "bus" +#: apps/wei/forms/registration.py:72 apps/wei/models.py:107 +msgid "Bus" +msgstr "Bus" -#: apps/wei/forms/registration.py:85 +#: apps/wei/forms/registration.py:73 msgid "" "This choice is not definitive. The WEI organizers are free to attribute for " "you a bus and a team, in particular if you are a free eletron." @@ -3036,11 +3031,11 @@ msgstr "" "vous attribuer un bus et une équipe, en particulier si vous êtes un·e " "électron libre." -#: apps/wei/forms/registration.py:92 +#: apps/wei/forms/registration.py:80 msgid "Team" msgstr "Équipe" -#: apps/wei/forms/registration.py:94 +#: apps/wei/forms/registration.py:82 msgid "" "Leave this field empty if you won't be in a team (staff, bus chief, free " "electron)" @@ -3048,25 +3043,33 @@ msgstr "" "Laissez ce champ vide si vous ne serez pas dans une équipe (staff, chef de " "bus ou électron libre)" -#: apps/wei/forms/registration.py:100 apps/wei/forms/registration.py:110 +#: apps/wei/forms/registration.py:88 apps/wei/forms/registration.py:98 #: apps/wei/models.py:179 msgid "WEI Roles" msgstr "Rôles au WEI" -#: apps/wei/forms/registration.py:101 +#: apps/wei/forms/registration.py:89 msgid "Select the roles that you are interested in." msgstr "Sélectionnez les rôles qui vous intéressent." -#: apps/wei/forms/registration.py:160 +#: apps/wei/forms/registration.py:148 msgid "This team doesn't belong to the given bus." msgstr "Cette équipe n'appartient pas à ce bus." #: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:38 -#: apps/wei/forms/surveys/wei2025.py:36 msgid "Choose a word:" msgstr "Choisissez un mot :" -#: apps/wei/forms/surveys/wei2025.py:123 +#: apps/wei/forms/surveys/wei2025.py:173 +msgid "Choose {NB_WORDS} words:" +msgstr "Choisissez {NB_WORDS} mots :" + +#: apps/wei/forms/surveys/wei2025.py:217 +#, python-brace-format +msgid "Please choose exactly {NB_WORDS} words" +msgstr "" + +#: apps/wei/forms/surveys/wei2025.py:263 msgid "Rate between 0 and 5." msgstr "Note entre 0 et 5." @@ -3084,7 +3087,7 @@ msgstr "début" msgid "date end" msgstr "fin" -#: apps/wei/models.py:37 +#: apps/wei/models.py:37 apps/wei/templates/wei/base.html:53 msgid "deposit amount" msgstr "montant de la caution" @@ -3092,7 +3095,7 @@ msgstr "montant de la caution" msgid "membership fee (soge credit)" msgstr "Cotisation pour adhérer (crédit sogé)" -#: apps/wei/models.py:81 apps/wei/tables.py:305 +#: apps/wei/models.py:81 apps/wei/tables.py:365 msgid "seat count in the bus" msgstr "nombre de sièges dans le bus" @@ -3105,14 +3108,14 @@ msgid "Information about the survey for new members, encoded in JSON" msgstr "" "Informations sur le sondage pour les nouveaux membres, encodées en JSON" -#: apps/wei/models.py:107 -msgid "Bus" -msgstr "Bus" - -#: apps/wei/models.py:108 apps/wei/templates/wei/weiclub_detail.html:51 +#: apps/wei/models.py:108 apps/wei/templates/wei/weiclub_detail.html:56 msgid "Buses" msgstr "Bus" +#: apps/wei/models.py:145 apps/wei/models.py:375 +msgid "bus" +msgstr "bus" + #: apps/wei/models.py:154 msgid "color" msgstr "couleur" @@ -3138,10 +3141,9 @@ msgstr "Rôle au WEI" msgid "Credit from Société générale" msgstr "Crédit de la Société générale" -#: apps/wei/models.py:207 apps/wei/templates/wei/weimembership_form.html:98 -#: apps/wei/views.py:997 -msgid "Deposit check given" -msgstr "Chèque de caution donné" +#: apps/wei/models.py:207 +msgid "Deposit given" +msgstr "Caution donnée" #: apps/wei/models.py:213 msgid "Check" @@ -3226,35 +3228,35 @@ msgstr "" "Informations sur l'inscription (bus pour les 2A+, questionnaire pour les " "1A), encodées en JSON" -#: apps/wei/models.py:290 +#: apps/wei/models.py:296 msgid "WEI User" msgstr "Participant·e au WEI" -#: apps/wei/models.py:291 +#: apps/wei/models.py:297 msgid "WEI Users" msgstr "Participant·e·s au WEI" -#: apps/wei/models.py:364 +#: apps/wei/models.py:385 msgid "team" msgstr "équipe" -#: apps/wei/models.py:374 +#: apps/wei/models.py:395 msgid "WEI registration" msgstr "Inscription au WEI" -#: apps/wei/models.py:378 +#: apps/wei/models.py:399 msgid "WEI membership" msgstr "Adhésion au WEI" -#: apps/wei/models.py:379 +#: apps/wei/models.py:400 msgid "WEI memberships" msgstr "Adhésions au WEI" -#: apps/wei/tables.py:105 +#: apps/wei/tables.py:135 msgid "The user does not have enough money." msgstr "L'utilisateur⋅rice n'a pas assez d'argent." -#: apps/wei/tables.py:108 +#: apps/wei/tables.py:138 msgid "" "The user is in first year. You may validate the credit, the algorithm will " "run later." @@ -3262,44 +3264,44 @@ msgstr "" "L'utilisateur·rice est en première année, vous pouvez valider le crédit, " "l'algorithme tournera plus tard." -#: apps/wei/tables.py:111 +#: apps/wei/tables.py:141 msgid "The user has enough money, you can validate the registration." msgstr "L'utilisateur⋅rice a assez d'argent, l'inscription est possible." -#: apps/wei/tables.py:143 +#: apps/wei/tables.py:174 msgid "Year" msgstr "Année" -#: apps/wei/tables.py:180 apps/wei/templates/wei/weimembership_form.html:102 +#: apps/wei/tables.py:240 apps/wei/templates/wei/weimembership_form.html:102 msgid "preferred bus" msgstr "bus préféré" -#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:38 +#: apps/wei/tables.py:270 apps/wei/templates/wei/bus_detail.html:38 #: apps/wei/templates/wei/busteam_detail.html:52 msgid "Teams" msgstr "Équipes" -#: apps/wei/tables.py:219 apps/wei/tables.py:260 +#: apps/wei/tables.py:279 apps/wei/tables.py:320 msgid "Members count" msgstr "Nombre de membres" -#: apps/wei/tables.py:226 apps/wei/tables.py:257 +#: apps/wei/tables.py:286 apps/wei/tables.py:317 msgid "members" msgstr "adhérent·es" -#: apps/wei/tables.py:287 +#: apps/wei/tables.py:347 msgid "suggested first year" msgstr "1A suggéré·es" -#: apps/wei/tables.py:293 +#: apps/wei/tables.py:353 msgid "validated first year" msgstr "1A validé·es" -#: apps/wei/tables.py:299 +#: apps/wei/tables.py:359 msgid "validated staff" msgstr "2A+ validé·es" -#: apps/wei/tables.py:310 +#: apps/wei/tables.py:370 msgid "free seats" msgstr "sièges libres" @@ -3340,19 +3342,15 @@ msgstr "Prix du WEI (élèves)" msgid "WEI fee (unpaid students)" msgstr "Prix du WEI (étudiant⋅es)" -#: apps/wei/templates/wei/base.html:53 -msgid "Deposit amount" -msgstr "Caution" - #: apps/wei/templates/wei/base.html:74 msgid "WEI list" msgstr "Liste des WEI" -#: apps/wei/templates/wei/base.html:79 apps/wei/views.py:550 +#: apps/wei/templates/wei/base.html:79 apps/wei/views.py:584 msgid "Register 1A" msgstr "Inscrire un⋅e 1A" -#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:646 +#: apps/wei/templates/wei/base.html:83 apps/wei/views.py:680 msgid "Register 2A+" msgstr "Inscrire un⋅e 2A+" @@ -3389,8 +3387,8 @@ msgstr "Télécharger au format PDF" #: apps/wei/templates/wei/survey.html:11 #: apps/wei/templates/wei/survey_closed.html:11 -#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1165 -#: apps/wei/views.py:1220 apps/wei/views.py:1267 +#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1246 +#: apps/wei/views.py:1305 apps/wei/views.py:1352 msgid "Survey WEI" msgstr "Questionnaire WEI" @@ -3423,15 +3421,19 @@ msgstr "M'inscrire au WEI ! – 2A+" msgid "Update my registration" msgstr "Modifier mon inscription" -#: apps/wei/templates/wei/weiclub_detail.html:63 +#: apps/wei/templates/wei/weiclub_detail.html:44 +msgid "Restart survey" +msgstr "Recommencer le questionnaire" + +#: apps/wei/templates/wei/weiclub_detail.html:68 msgid "Members of the WEI" msgstr "Membres du WEI" -#: apps/wei/templates/wei/weiclub_detail.html:89 +#: apps/wei/templates/wei/weiclub_detail.html:80 msgid "Unvalidated registrations" msgstr "Inscriptions non validées" -#: apps/wei/templates/wei/weiclub_detail.html:99 +#: apps/wei/templates/wei/weiclub_detail.html:90 msgid "Attribute buses" msgstr "Répartition dans les bus" @@ -3467,6 +3469,10 @@ msgstr "Informations brutes du sondage" msgid "The algorithm didn't run." msgstr "L'algorithme n'a pas été exécuté." +#: apps/wei/templates/wei/weimembership_form.html:98 apps/wei/views.py:1028 +msgid "Deposit check given" +msgstr "Chèque de caution donné" + #: apps/wei/templates/wei/weimembership_form.html:105 msgid "preferred team" msgstr "équipe préférée" @@ -3522,33 +3528,31 @@ msgstr "Paiements requis" msgid "Membership fees: %(amount)s" msgstr "Frais d'inscription : %(amount)s" -#: apps/wei/templates/wei/weimembership_form.html:153 +#: apps/wei/templates/wei/weimembership_form.html:154 #, python-format msgid "Deposit (by Note transaction): %(amount)s" msgstr "Caution (par transaction) : %(amount)s" -#: apps/wei/templates/wei/weimembership_form.html:157 +#: apps/wei/templates/wei/weimembership_form.html:158 #, python-format msgid "Deposit (by check): %(amount)s" msgstr "Caution (par chèque) : %(amount)s" -#: apps/wei/templates/wei/weimembership_form.html:161 +#: apps/wei/templates/wei/weimembership_form.html:163 #, python-format msgid "Total needed: %(total)s" msgstr "Total nécessaire : %(total)s" -#: apps/wei/templates/wei/weimembership_form.html:165 +#: apps/wei/templates/wei/weimembership_form.html:167 #, python-format msgid "Current balance: %(balance)s" msgstr "Solde actuel : %(balance)s" -#: apps/wei/templates/wei/weimembership_form.html:172 -#, fuzzy -#| msgid "The user didn't give her/his deposit check." -msgid "The user didn't give her/his caution check." -msgstr "L'utilisateur⋅rice n'a pas donné son chèque de caution." +#: apps/wei/templates/wei/weimembership_form.html:174 +msgid "The user didn't give her/his caution." +msgstr "L'utilisateur⋅rice n'a pas donné sa caution." -#: apps/wei/templates/wei/weimembership_form.html:180 +#: apps/wei/templates/wei/weimembership_form.html:182 msgid "" "This user is not a member of the Kfet club for the coming year. The " "membership will be processed automatically, the WEI registration includes " @@ -3591,63 +3595,63 @@ msgstr "Chercher un WEI" msgid "WEI Detail" msgstr "Détails du WEI" -#: apps/wei/views.py:212 +#: apps/wei/views.py:229 msgid "View members of the WEI" msgstr "Voir les membres du WEI" -#: apps/wei/views.py:245 +#: apps/wei/views.py:262 msgid "Find WEI Membership" msgstr "Trouver une adhésion au WEI" -#: apps/wei/views.py:255 +#: apps/wei/views.py:272 msgid "View registrations to the WEI" msgstr "Voir les inscriptions au WEI" -#: apps/wei/views.py:284 +#: apps/wei/views.py:318 msgid "Find WEI Registration" msgstr "Trouver une inscription au WEI" -#: apps/wei/views.py:295 +#: apps/wei/views.py:329 msgid "Update the WEI" msgstr "Modifier le WEI" -#: apps/wei/views.py:316 +#: apps/wei/views.py:350 msgid "Create new bus" msgstr "Ajouter un nouveau bus" -#: apps/wei/views.py:354 +#: apps/wei/views.py:388 msgid "Update bus" msgstr "Modifier le bus" -#: apps/wei/views.py:386 +#: apps/wei/views.py:420 msgid "Manage bus" msgstr "Gérer le bus" -#: apps/wei/views.py:413 +#: apps/wei/views.py:447 msgid "Create new team" msgstr "Créer une nouvelle équipe" -#: apps/wei/views.py:457 +#: apps/wei/views.py:491 msgid "Update team" msgstr "Modifier l'équipe" -#: apps/wei/views.py:492 +#: apps/wei/views.py:526 msgid "Manage WEI team" msgstr "Gérer l'équipe WEI" -#: apps/wei/views.py:514 +#: apps/wei/views.py:548 msgid "Register first year student to the WEI" msgstr "Inscrire un⋅e 1A au WEI" -#: apps/wei/views.py:571 apps/wei/views.py:664 +#: apps/wei/views.py:605 apps/wei/views.py:698 msgid "Check if you will open a Société Générale account" msgstr "Cochez cette case si vous ouvrez un compte à la Société Générale." -#: apps/wei/views.py:582 apps/wei/views.py:694 +#: apps/wei/views.py:616 apps/wei/views.py:728 msgid "This user is already registered to this WEI." msgstr "Cette personne est déjà inscrite au WEI." -#: apps/wei/views.py:587 +#: apps/wei/views.py:621 msgid "" "This user can't be in her/his first year since he/she has already " "participated to a WEI." @@ -3655,65 +3659,67 @@ msgstr "" "Cet⋅te utilisateur⋅rice ne peut pas être en première année puisqu'iel a déjà " "participé à un WEI." -#: apps/wei/views.py:610 +#: apps/wei/views.py:644 msgid "Register old student to the WEI" msgstr "Inscrire un⋅e 2A+ au WEI" -#: apps/wei/views.py:668 apps/wei/views.py:773 +#: apps/wei/views.py:702 apps/wei/views.py:825 msgid "You already opened an account in the Société générale." msgstr "Vous avez déjà ouvert un compte auprès de la société générale." -#: apps/wei/views.py:681 apps/wei/views.py:790 +#: apps/wei/views.py:715 apps/wei/views.py:821 msgid "Choose how you want to pay the deposit" msgstr "Choisissez comment payer la caution" -#: apps/wei/views.py:733 +#: apps/wei/views.py:767 msgid "Update WEI Registration" msgstr "Modifier l'inscription WEI" -#: apps/wei/views.py:816 +#: apps/wei/views.py:811 +msgid "Tick if the deposit check has been given" +msgstr "Cochez si le chèque de caution a été donné" + +#: apps/wei/views.py:850 msgid "No membership found for this registration" msgstr "Pas d'adhésion trouvée pour cette inscription" -#: apps/wei/views.py:825 +#: apps/wei/views.py:859 msgid "You don't have the permission to update memberships" msgstr "Vous n'avez pas la permission de modifier une inscription" -#: apps/wei/views.py:831 +#: apps/wei/views.py:865 #, python-format msgid "You don't have the permission to update the field %(field)s" msgstr "Vous n'avez pas la permission de modifier le champ %(field)s" -#: apps/wei/views.py:876 +#: apps/wei/views.py:906 msgid "Delete WEI registration" msgstr "Supprimer l'inscription WEI" -#: apps/wei/views.py:887 +#: apps/wei/views.py:917 msgid "You don't have the right to delete this WEI registration." msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI." -#: apps/wei/views.py:905 +#: apps/wei/views.py:935 msgid "Validate WEI registration" msgstr "Valider l'inscription WEI" -#: apps/wei/views.py:998 -msgid "Please make sure the check is given before validating the registration" -msgstr "" -"Merci de vous assurer que le chèque a bien été donné avant de valider " -"l'adhésion" +#: apps/wei/views.py:1029 +msgid "Only treasurers can validate this field" +msgstr "Seul·e·s les trésorier·ère·s peuvent valider ce champ" -#: apps/wei/views.py:1004 +#: apps/wei/views.py:1035 msgid "Create deposit transaction" msgstr "Créer une transaction de caution" -#: apps/wei/views.py:1005 +#: apps/wei/views.py:1036 #, python-format msgid "" "A transaction of %(amount).2f€ will be created from the user's Note account" msgstr "" "Un transaction de %(amount).2f€ va être créée depuis la note de l'utilisateur" -#: apps/wei/views.py:1093 +#: apps/wei/views.py:1124 #, python-format msgid "" "This user doesn't have enough money to join this club and pay the deposit. " @@ -3723,20 +3729,24 @@ msgstr "" "payer la caution. Solde actuel : %(balance)d€, crédit : %(credit)d€, " "requis : %(needed)d€" -#: apps/wei/views.py:1146 +#: apps/wei/views.py:1177 #, python-format msgid "Deposit %(name)s" msgstr "Caution %(name)s" -#: apps/wei/views.py:1360 +#: apps/wei/views.py:1202 +msgid "Update WEI Membership" +msgstr "Modifier une adhésion au WEI" + +#: apps/wei/views.py:1445 msgid "Attribute buses to first year members" msgstr "Répartir les 1A dans les bus" -#: apps/wei/views.py:1386 +#: apps/wei/views.py:1471 msgid "Attribute bus" msgstr "Attribuer un bus" -#: apps/wei/views.py:1426 +#: apps/wei/views.py:1511 msgid "" "No first year student without a bus found. Either all of them have a bus, or " "none has filled the survey yet." @@ -4100,9 +4110,10 @@ msgid "" "your web browser when you are done accessing services that require " "authentication!" msgstr "" -"

    Connection réussie

    Vous vous êtes bien connecté au Service Central d'Authentification." -"
    Pour des raisons de sécurité, veuillez vous déconnecter et fermer votre navigateur internet " -"une fois que vous aurez fini d'accéder aux services qui requiert une authentification !" +"

    Connection réussie

    Vous vous êtes bien connecté au Service Central " +"d'Authentification.
    Pour des raisons de sécurité, veuillez vous " +"déconnecter et fermer votre navigateur internet une fois que vous aurez fini " +"d'accéder aux services qui requiert une authentification !" #: note_kfet/templates/cas/logged.html:14 msgid "Log me out from all my sessions" @@ -4348,6 +4359,15 @@ msgstr "" "d'adhésion. Vous devez également valider votre adresse email en suivant le " "lien que vous avez reçu." +#~ msgid "Deposit amount" +#~ msgstr "Caution" + +#~ msgid "" +#~ "Please make sure the check is given before validating the registration" +#~ msgstr "" +#~ "Merci de vous assurer que le chèque a bien été donné avant de valider " +#~ "l'adhésion" + #~ msgid "caution amount" #~ msgstr "montant de la caution" From 251bb933da42a0310ab9f0240dd33e8d14fed449 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sun, 3 Aug 2025 21:19:44 +0200 Subject: [PATCH 22/22] Signals used to ignore _no_signal --- apps/member/signals.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/member/signals.py b/apps/member/signals.py index e74c37ad..b1b8cd82 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -16,7 +16,8 @@ def save_user_profile(instance, created, raw, **_kwargs): def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs): - if created: + if not hasattr(instance, "_no_signal") and created: + print('update_wei_registration_fee_on_membership_creation') from wei.models import WEIRegistration if instance.club.id == 1 or instance.club.id == 2: registrations = WEIRegistration.objects.filter( @@ -24,14 +25,16 @@ def update_wei_registration_fee_on_membership_creation(sender, instance, created wei__year=instance.date_start.year, ) for r in registrations: + r._force_save = True r.save() def update_wei_registration_fee_on_club_change(sender, instance, **kwargs): from wei.models import WEIRegistration - if instance.id == 1 or instance.id == 2: + if not hasattr(instance, "_no_signal") and (instance.id == 1 or instance.id == 2): registrations = WEIRegistration.objects.filter( wei__year=instance.membership_start.year, ) for r in registrations: + r._force_save = True r.save()