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): 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/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/member/signals.py b/apps/member/signals.py index 869f9117..b1b8cd82 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -13,3 +13,28 @@ 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 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( + user=instance.user, + 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 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() diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 1e738361..248574e1 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", + "field": "deposit_given", "permanent": false, - "description": "Dire si un chèque de caution est donné pour une inscription WEI" + "description": "Autoriser une transaction de caution WEI" } }, { @@ -4347,7 +4347,87 @@ "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" + } + }, + { + "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": "deposit_given", + "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.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", + "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" } }, { @@ -4444,7 +4524,8 @@ 159, 160, 212, - 222 + 222, + 297 ] } }, @@ -4631,7 +4712,10 @@ 176, 177, 178, - 183 + 183, + 294, + 295, + 296 ] } }, @@ -4764,7 +4848,6 @@ "name": "Chef\u22c5fe de bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4778,7 +4861,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, @@ -4790,7 +4874,6 @@ "name": "Chef\u22c5fe d'\u00e9quipe", "permissions": [ 22, - 84, 116, 123, 124, @@ -4805,8 +4888,7 @@ "for_club": null, "name": "\u00c9lectron libre", "permissions": [ - 22, - 84 + 22 ] } }, @@ -4957,7 +5039,6 @@ "name": "Référent⋅e Bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4971,7 +5052,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, 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/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/__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..31f42f89 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_given', 'deposit_type' ] widgets = { "user": Autocomplete( @@ -59,30 +59,17 @@ class WEIRegistrationForm(forms.ModelForm): 'minDate': '1900-01-01', 'maxDate': '2100-01-01' }), - "deposit_check": forms.BooleanField( - required=False, + "deposit_given": forms.CheckboxInput( + attrs={'class': 'form-check-input'}, ), - } - - -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): 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(), @@ -174,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/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index d92cc23f..33291bb1 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]', + 4: 'Réponse 1 Y2[KAR]', + 2: 'Réponse 1 Tcherno[bus]', + 5: 'Réponse 1 [Kar]tier', + 1: 'Réponse 1 [Car]cassonne', + 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]', + 4: 'Réponse 2 Y2[KAR]', + 2: 'Réponse 2 Tcherno[bus]', + 5: 'Réponse 2 [Kar]tier', + 1: 'Réponse 2 [Car]cassonne', + 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]', + 4: 'Réponse 3 Y2[KAR]', + 2: 'Réponse 3 Tcherno[bus]', + 5: 'Réponse 3 [Kar]tier', + 1: 'Réponse 3 [Car]cassonne', + 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]', + 4: 'Réponse 4 Y2[KAR]', + 2: 'Réponse 4 Tcherno[bus]', + 5: 'Réponse 4 [Kar]tier', + 1: 'Réponse 4 [Car]cassonne', + 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]', + 4: 'Réponse 5 Y2[KAR]', + 2: 'Réponse 5 Tcherno[bus]', + 5: 'Réponse 5 [Kar]tier', + 1: 'Réponse 5 [Car]cassonne', + 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]', + 4: 'Réponse 6 Y2[KAR]', + 2: 'Réponse 6 Tcherno[bus]', + 5: 'Réponse 6 [Kar]tier', + 1: 'Réponse 6 [Car]cassonne', + 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]', + 4: 'Réponse 7 Y2[KAR]', + 2: 'Réponse 7 Tcherno[bus]', + 5: 'Réponse 7 [Kar]tier', + 1: 'Réponse 7 [Car]cassonne', + 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]', + 4: 'Réponse 8 Y2[KAR]', + 2: 'Réponse 8 Tcherno[bus]', + 5: 'Réponse 8 [Kar]tier', + 1: 'Réponse 8 [Car]cassonne', + 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]', + 4: 'Réponse 9 Y2[KAR]', + 2: 'Réponse 9 Tcherno[bus]', + 5: 'Réponse 9 [Kar]tier', + 1: 'Réponse 9 [Car]cassonne', + 6: 'Réponse 9 O[car]ina', + 7: 'Réponse 9 Show[bus]', + 8: '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() @@ -199,24 +365,41 @@ 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 + """ + 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, 21)) / 20 + - 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() def scores_per_bus(self): - return {bus: self.score(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): + """ + 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][0]) return values @classmethod @@ -243,10 +426,18 @@ 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. 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 @@ -307,7 +498,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) @@ -322,8 +513,8 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): 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 + score2 = survey2.score_questions(bus) + 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/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/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/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 59f018d7..5aa4e94f 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: @@ -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( @@ -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..362bdf9c 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table): validate = tables.Column( verbose_name=_("Validate"), - orderable=False, - accessor=A('pk'), + orderable=True, + accessor='validate_status', attrs={ 'th': { 'id': 'validate-membership-header' @@ -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': { @@ -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( @@ -98,12 +127,13 @@ 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:validate_registration', args=(record.pk,)) 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,10 +151,11 @@ 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', - '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), @@ -134,8 +165,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( @@ -156,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' @@ -163,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_check', ) + 'year', 'bus', 'team', 'registration__deposit_given', 'registration__deposit_type') row_attrs = { 'class': 'table-row', 'id': lambda record: "row-" + str(record.pk), 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 %} diff --git a/apps/wei/templates/wei/weiclub_detail.html b/apps/wei/templates/wei/weiclub_detail.html index f7c18c9a..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 %} @@ -67,20 +72,6 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} -{% if history_list.data %} -
-
- - {% trans "Transaction history" %} - -
-
- {% render_table history_list %} -
-
-{% endif %} - {% if pre_registrations.data %}
@@ -99,6 +90,19 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Attribute buses" %} {% endif %} +{% if history_list.data %} +
+ +
+ {% render_table history_list %} +
+
+{% endif %} {% endblock %} diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html index 6d5c0b37..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 %}
@@ -143,12 +143,13 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}
{% endif %} -
+
{% trans "Required payments:" %}
  • {% 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 %}
  • @@ -167,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 %} @@ -213,7 +215,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/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py index 5930eb3b..cd6ad017 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 @@ -30,12 +30,12 @@ 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) - 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() @@ -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, @@ -83,11 +83,14 @@ 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() + survey = WEISurvey2025(registration) # Run algorithm WEISurvey2025.get_algorithm_class()().run_algorithm() @@ -102,10 +105,25 @@ 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] - penalty += (max_score - score) ** 2 - - self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance - + '''print(buses) + print(chosen_bus)''' + self.assertIn(chosen_bus, [x[0] for x in buses]) + 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(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) 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/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 4ac679f9..782f264e 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 @@ -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 @@ -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, ] @@ -260,6 +277,23 @@ 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')), + 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", "") @@ -510,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): @@ -560,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: @@ -606,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): @@ -670,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"] @@ -739,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, @@ -767,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): @@ -780,15 +806,23 @@ 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: + 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 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) + 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") + + 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 @@ -849,7 +883,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() @@ -862,9 +895,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}) @@ -963,9 +993,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 @@ -988,17 +1018,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"), @@ -1039,8 +1070,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 @@ -1052,7 +1083,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 @@ -1096,16 +1127,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( @@ -1149,11 +1180,61 @@ 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_given = 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}) +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. @@ -1176,6 +1257,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,))) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 1be7fb0f..2e193678 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-08-12 22:55+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 @@ -68,7 +66,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" @@ -297,14 +295,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:130 +#: 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:135 +#: apps/wei/forms/registration.py:122 msgid "First name" msgstr "Prénom" @@ -323,7 +321,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 @@ -420,6 +418,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" @@ -476,7 +475,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" @@ -953,7 +951,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" @@ -1231,10 +1229,19 @@ msgstr "Aliment entièrement utilisé dans : {meal}" msgid "Add the ingredient:" msgstr "Ajouter l'ingrédient" +#: 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:391 msgid "Update an aliment" msgstr "Modifier un aliment" +#: apps/food/views.py:439 +msgid "Details of:" +msgstr "Détails de :" + #: apps/food/views.py:449 apps/treasury/tables.py:149 msgid "Yes" msgstr "Oui" @@ -1310,11 +1317,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" @@ -1375,12 +1382,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:117 +#: 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:118 +#: apps/wei/forms/registration.py:105 msgid "No credit" msgstr "Pas de rechargement" @@ -1389,13 +1396,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:123 +#: 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:140 +#: apps/wei/forms/registration.py:127 msgid "Bank" msgstr "Banque" @@ -1800,7 +1807,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" @@ -2342,8 +2349,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." @@ -2846,7 +2853,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" @@ -3120,7 +3127,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." @@ -3301,6 +3308,11 @@ msgstr "" "Merci de demander à l'utilisateur·rice de recharger sa note avant de " "supprimer la demande de crédit." +#: apps/treasury/templates/treasury/sogecredit_detail.html:63 +#: apps/wei/tables.py:60 apps/wei/tables.py:131 +msgid "Validate" +msgstr "Valider" + #: apps/treasury/templates/treasury/sogecredit_detail.html:71 msgid "Return to credit list" msgstr "Retour à la liste des crédits" @@ -3376,12 +3388,11 @@ msgstr "" "L'utilisateur·rice sélectionné·e n'est pas validé·e. Merci de d'abord " "valider son compte" -#: apps/wei/forms/registration.py:85 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:86 +#: 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." @@ -3390,11 +3401,11 @@ msgstr "" "vous attribuer un bus et une équipe, en particulier si vous êtes un·e " "électron libre." -#: apps/wei/forms/registration.py:93 +#: apps/wei/forms/registration.py:80 msgid "Team" msgstr "Équipe" -#: apps/wei/forms/registration.py:95 +#: apps/wei/forms/registration.py:82 msgid "" "Leave this field empty if you won't be in a team (staff, bus chief, free " "electron)" @@ -3402,25 +3413,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:101 apps/wei/forms/registration.py:111 +#: 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:102 +#: 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:161 +#: 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." @@ -3434,7 +3453,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" @@ -3442,7 +3461,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" @@ -3455,14 +3474,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" @@ -3488,10 +3507,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" @@ -3502,10 +3520,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" @@ -3578,35 +3594,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." @@ -3614,44 +3630,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" @@ -3692,19 +3708,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+" @@ -3741,8 +3753,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" @@ -3775,15 +3787,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" @@ -3819,6 +3835,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" @@ -3874,33 +3894,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 " @@ -3943,63 +3961,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." @@ -4007,65 +4025,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. " @@ -4075,20 +4095,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." @@ -4442,6 +4466,58 @@ msgstr "Charte Info (FR)" msgid "FAQ (FR)" msgstr "FAQ (FR)" +#: note_kfet/templates/base.html:222 +msgid "Managed by BDE" +msgstr "Géré par le BDE" + +#: note_kfet/templates/base.html:224 +msgid "Hosted by Cr@ns" +msgstr "Hébergé par le Cr@ans" + +#: note_kfet/templates/base.html:266 +msgid "The note is not available for now" +msgstr "La note est indisponible pour le moment" + +#: note_kfet/templates/base.html:268 +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/cas/logged.html:8 +msgid "" +"

Log In Successful

You have successfully logged into the Central " +"Authentication Service.
For security reasons, please Log Out and Exit " +"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 !" + +#: note_kfet/templates/cas/logged.html:14 +msgid "Log me out from all my sessions" +msgstr "Me déconnecter de toutes mes sessions" + +#: note_kfet/templates/cas/logged.html:20 +msgid "Forget the identity provider" +msgstr "Oublier le fournisseur d'identité" + +#: note_kfet/templates/cas/logged.html:24 +msgid "Logout" +msgstr "Déconnexion" + +#: note_kfet/templates/cas/login.html:11 +msgid "Please log in" +msgstr "Veuillez vous connecter" + +#: note_kfet/templates/cas/login.html:23 +msgid "Login" +msgstr "Connexion" + +#: note_kfet/templates/cas/warn.html:14 +msgid "Connect to the service" +msgstr "Connexion au service" + #: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8 #: static/note_kfet/templates/oauth2_provider/application_confirm_delete.html:8 msgid "Are you sure to delete the application" @@ -4706,10 +4782,30 @@ msgstr "" "d'adhésion. Vous devez également valider votre adresse email en suivant le " "lien que vous avez reçu." -#, fuzzy -#~| msgid "Invitation" -#~ msgid "Syndication" -#~ msgstr "Invitation" +#~ 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" + +#~ msgid "caution type" +#~ msgstr "type de caution" + +#~ msgid "Caution amount" +#~ msgstr "Montant de la caution" + +#~ msgid "caution check given" +#~ msgstr "chèque de caution donné" + +#, python-format +#~ msgid "Caution %(name)s" +#~ msgstr "Caution %(name)s" #, fuzzy #~| msgid "There is no results."