1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-23 17:26:46 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
bfa5734d55 Changed score calculation in survey 2025-07-23 16:48:59 +02:00
296d021d54 Permissions 2025-07-23 01:24:59 +02:00
6e348b995b Better Membership update 2025-07-23 00:51:03 +02:00
1274315cde Last untranslated field 2025-07-19 18:55:49 +02:00
2755a5f7ab Minor fail 2025-07-19 17:10:25 +02:00
9ab4df94e6 Minor fixes 2025-07-19 16:55:07 +02:00
edb6abfff5 Add fee field to WEIRegistration to be able to sort on validation status 2025-07-19 16:24:25 +02:00
16 changed files with 505 additions and 126 deletions

View File

@ -6,7 +6,7 @@ from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _ 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): class MemberConfig(AppConfig):
@ -17,7 +17,16 @@ class MemberConfig(AppConfig):
""" """
Define app internal signals to interact with other apps Define app internal signals to interact with other apps
""" """
from .models import Membership, Club
post_save.connect( post_save.connect(
save_user_profile, save_user_profile,
sender=settings.AUTH_USER_MODEL, 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
)

View File

@ -13,3 +13,25 @@ def save_user_profile(instance, created, raw, **_kwargs):
instance.profile.email_confirmed = True instance.profile.email_confirmed = True
instance.profile.registration_valid = True instance.profile.registration_valid = True
instance.profile.save() 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()

View File

@ -4347,7 +4347,23 @@
"mask": 3, "mask": 3,
"field": "", "field": "",
"permanent": false, "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", "name": "Chef\u22c5fe de bus",
"permissions": [ "permissions": [
22, 22,
84,
115, 115,
117, 117,
118, 118,
@ -4778,7 +4793,8 @@
287, 287,
289, 289,
290, 290,
291 291,
293
] ]
} }
}, },
@ -4790,7 +4806,6 @@
"name": "Chef\u22c5fe d'\u00e9quipe", "name": "Chef\u22c5fe d'\u00e9quipe",
"permissions": [ "permissions": [
22, 22,
84,
116, 116,
123, 123,
124, 124,
@ -4805,8 +4820,7 @@
"for_club": null, "for_club": null,
"name": "\u00c9lectron libre", "name": "\u00c9lectron libre",
"permissions": [ "permissions": [
22, 22
84
] ]
} }
}, },
@ -4957,7 +4971,6 @@
"name": "Référent⋅e Bus", "name": "Référent⋅e Bus",
"permissions": [ "permissions": [
22, 22,
84,
115, 115,
117, 117,
118, 118,
@ -4971,7 +4984,8 @@
287, 287,
289, 289,
290, 290,
291 291,
293
] ]
} }
}, },

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .registration import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, WEIMembership1AForm, \ from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, \
WEIMembershipForm, BusForm, BusTeamForm WEIMembershipForm, BusForm, BusTeamForm
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
__all__ = [ __all__ = [
'WEIForm', 'WEIRegistrationForm', 'WEIRegistration1AForm', 'WEIRegistration2AForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm', 'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
] ]

View File

@ -44,7 +44,7 @@ class WEIRegistrationForm(forms.ModelForm):
fields = [ fields = [
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size', 'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
'health_issues', 'emergency_contact_name', 'emergency_contact_phone', 'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
'first_year', 'information_json', 'deposit_check' 'first_year', 'information_json', 'deposit_check', 'deposit_type'
] ]
widgets = { widgets = {
"user": Autocomplete( "user": Autocomplete(
@ -62,21 +62,8 @@ class WEIRegistrationForm(forms.ModelForm):
"deposit_check": forms.BooleanField( "deposit_check": forms.BooleanField(
required=False, 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(), "deposit_type": forms.RadioSelect(),
}) }
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
class WEIChooseBusForm(forms.Form): class WEIChooseBusForm(forms.Form):

View File

@ -14,7 +14,8 @@ from django.utils.translation import gettext_lazy as _
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership, Bus from ...models import WEIMembership, Bus
WORDS = [ WORDS = {
'list': [
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
@ -23,7 +24,129 @@ WORDS = [
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', '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', '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', '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): class WEISurveyForm2025(forms.Form):
@ -32,11 +155,6 @@ class WEISurveyForm2025(forms.Form):
Members choose 20 words, from which we calculate the best associated bus. 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): def set_registration(self, registration):
""" """
Filter the bus selector with the buses of the current WEI. Filter the bus selector with the buses of the current WEI.
@ -48,13 +166,18 @@ class WEISurveyForm2025(forms.Form):
registration._force_save = True registration._force_save = True
registration.save() registration.save()
if self.data: rng = Random((information.step + 1) * information.seed)
self.fields["word"].choices = [(w, w) for w in WORDS]
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(): if self.is_valid():
return return
rng = Random((information.step + 1) * information.seed)
buses = WEISurveyAlgorithm2025.get_buses() buses = WEISurveyAlgorithm2025.get_buses()
informations = {bus: WEIBusInformation2025(bus) for bus in buses} informations = {bus: WEIBusInformation2025(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), []) scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
@ -63,19 +186,36 @@ class WEISurveyForm2025(forms.Form):
else: else:
average_score = 0 average_score = 0
preferred_words = {bus: [word for word in WORDS preferred_words = {
if informations[bus].scores[word] >= average_score] bus: [word for word in WORDS['list'] if informations[bus].scores[word] >= average_score]
for bus in buses} 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() all_preferred_words = set()
for bus_words in preferred_words.values(): for bus_words in preferred_words.values():
all_preferred_words.update(bus_words) all_preferred_words.update(bus_words)
all_preferred_words = list(all_preferred_words) all_preferred_words = list(all_preferred_words)
rng.shuffle(all_preferred_words) rng.shuffle(all_preferred_words)
words = all_preferred_words[:n_choices] self.fields["words"].choices = [(w, w) for w in all_preferred_words]
self.fields["word"].choices = [(w, w) for w in words] else:
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,
)
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): class WEIBusInformation2025(WEIBusInformation):
@ -86,7 +226,7 @@ class WEIBusInformation2025(WEIBusInformation):
def __init__(self, bus): def __init__(self, bus):
self.scores = {} self.scores = {}
for word in WORDS: for word in WORDS['list']:
self.scores[word] = 0 self.scores[word] = 0
super().__init__(bus) super().__init__(bus)
@ -108,7 +248,7 @@ class BusInformationForm2025(forms.ModelForm):
except (json.JSONDecodeError, TypeError, AttributeError): except (json.JSONDecodeError, TypeError, AttributeError):
initial_scores = {} initial_scores = {}
if words is None: if words is None:
words = WORDS words = WORDS['list']
self.words = words self.words = words
choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')] choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
@ -145,10 +285,26 @@ class WEISurveyInformation2025(WEISurveyInformation):
step = 0 step = 0
def __init__(self, registration): def __init__(self, registration):
for i in range(1, 21): for i in range(1, 5):
setattr(self, "word" + str(i), None) setattr(self, "word" + str(i), None)
for q in WORDS['questions']:
setattr(self, q, None)
super().__init__(registration) 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): class WEISurvey2025(WEISurvey):
""" """
@ -174,9 +330,19 @@ class WEISurvey2025(WEISurvey):
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
word = form.cleaned_data["word"] 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.information.step += 1
setattr(self.information, "word" + str(self.information.step), word)
self.save() self.save()
@classmethod @classmethod
@ -187,7 +353,7 @@ class WEISurvey2025(WEISurvey):
""" """
The survey is complete once the bus is chosen. The survey is complete once the bus is chosen.
""" """
return self.information.step == 20 return self.information.step > len(WORDS['questions'])
@classmethod @classmethod
@lru_cache() @lru_cache()
@ -206,7 +372,8 @@ class WEISurvey2025(WEISurvey):
bus_info = self.get_algorithm_class().get_bus_information(bus) 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. # 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))] 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 return s
@lru_cache() @lru_cache()
@ -243,6 +410,13 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
def get_bus_information_form(cls): def get_bus_information_form(cls):
return BusInformationForm2025 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): def run_algorithm(self, display_tqdm=False):
""" """
Gale-Shapley algorithm implementation. Gale-Shapley algorithm implementation.

View File

@ -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)'),
),
]

View File

@ -285,6 +285,12 @@ class WEIRegistration(models.Model):
"encoded in JSON"), "encoded in JSON"),
) )
fee = models.PositiveIntegerField(
default=0,
verbose_name=_('fee'),
blank=True,
)
class Meta: class Meta:
unique_together = ('user', 'wei',) unique_together = ('user', 'wei',)
verbose_name = _("WEI User") verbose_name = _("WEI User")
@ -309,7 +315,25 @@ class WEIRegistration(models.Model):
self.information_json = json.dumps(information, indent=2) self.information_json = json.dumps(information, indent=2)
@property @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) bde = Club.objects.get(pk=1)
kfet = Club.objects.get(pk=2) kfet = Club.objects.get(pk=2)
@ -336,12 +360,9 @@ class WEIRegistration(models.Model):
return fee return fee
@property def save(self, *args, **kwargs):
def is_validated(self): self.fee = self.calculate_fee()
try: super().save(*args, **kwargs)
return self.membership is not None
except AttributeError:
return False
class WEIMembership(Membership): class WEIMembership(Membership):

View File

@ -58,8 +58,8 @@ class WEIRegistrationTable(tables.Table):
validate = tables.Column( validate = tables.Column(
verbose_name=_("Validate"), verbose_name=_("Validate"),
orderable=False, orderable=True,
accessor=A('pk'), accessor='validate_status',
attrs={ attrs={
'th': { 'th': {
'id': 'validate-membership-header' 'id': 'validate-membership-header'
@ -71,7 +71,7 @@ class WEIRegistrationTable(tables.Table):
'wei:wei_delete_registration', 'wei:wei_delete_registration',
args=[A('pk')], args=[A('pk')],
orderable=False, orderable=False,
verbose_name=_("delete"), verbose_name=_("Delete"),
text=_("Delete"), text=_("Delete"),
attrs={ attrs={
'th': { 'th': {
@ -98,12 +98,13 @@ class WEIRegistrationTable(tables.Table):
if not hasperm: if not hasperm:
return format_html("<span class='no-perm'></span>") return format_html("<span class='no-perm'></span>")
url = reverse_lazy('wei:wei_update_registration', args=(record.pk,)) + '?validate=true' url = reverse_lazy('wei:validate_registration', args=(record.pk,))
text = _('Validate') 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' btn_class = 'btn-secondary'
tooltip = _("The user does not have enough money.") tooltip = _("The user does not have enough money.")
elif record.first_year: elif status == 1:
btn_class = 'btn-info' btn_class = 'btn-info'
tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.") tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.")
else: else:
@ -121,6 +122,7 @@ class WEIRegistrationTable(tables.Table):
attrs = { attrs = {
'class': 'table table-condensed table-striped table-hover' 'class': 'table table-condensed table-striped table-hover'
} }
order_by = ('validate', 'user',)
model = WEIRegistration model = WEIRegistration
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check', fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'deposit_check',
@ -134,8 +136,8 @@ class WEIRegistrationTable(tables.Table):
class WEIMembershipTable(tables.Table): class WEIMembershipTable(tables.Table):
user = tables.LinkColumn( user = tables.LinkColumn(
'wei:wei_update_registration', 'wei:wei_update_membership',
args=[A('registration__pk')], args=[A('pk')],
) )
year = tables.Column( year = tables.Column(

View File

@ -50,7 +50,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
{% if club.deposit_amount > 0 %} {% if club.deposit_amount > 0 %}
<dt class="col-xl-6">{% trans 'Deposit amount'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'deposit amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd> <dd class="col-xl-6">{{ club.deposit_amount|pretty_money }}</dd>
{% endif %} {% endif %}

View File

@ -39,6 +39,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
data-turbolinks="false"> data-turbolinks="false">
{% trans "Update my registration" %} {% trans "Update my registration" %}
</a> </a>
{% if not not_first_year %}
<a class="btn btn-warning" href="{% url "wei:wei_survey" pk=my_registration.pk %}?reset=true" data-turbolinks="false">
{% trans "Restart survey" %}
</a>
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -67,20 +72,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if history_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% if pre_registrations.data %} {% if pre_registrations.data %}
<div class="card bg-white mb-3"> <div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
@ -99,6 +90,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a> <a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %} {% endif %}
{% if history_list.data %}
<div class="card bg-white mt-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -213,7 +213,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
$("input[name='bus']:checked").each(function (ignored) { $("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim()); buses.push($(this).parent().text().trim());
}); });
console.log(buses);
$("input[name='team']").each(function () { $("input[name='team']").each(function () {
let label = $(this).parent(); let label = $(this).parent();
$(this).parent().addClass('d-none'); $(this).parent().addClass('d-none');

View File

@ -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 %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
$("input[name='team']").each(function () {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function (bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endblock %}

View File

@ -6,7 +6,7 @@ import random
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase 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 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) bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
self.buses.append(bus) self.buses.append(bus)
information = WEIBusInformation2025(bus) information = WEIBusInformation2025(bus)
for word in WORDS: for word in WORDS['list']:
information.scores[word] = random.randint(0, 101) information.scores[word] = random.randint(0, 6)
information.save() information.save()
bus.save() bus.save()
@ -54,7 +54,7 @@ class TestWEIAlgorithm(TestCase):
) )
information = WEISurveyInformation2025(registration) information = WEISurveyInformation2025(registration)
for j in range(1, 21): 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.step = 20
information.save(registration) information.save(registration)
registration.save() registration.save()
@ -83,9 +83,11 @@ class TestWEIAlgorithm(TestCase):
birth_date='2000-01-01', birth_date='2000-01-01',
) )
information = WEISurveyInformation2025(registration) information = WEISurveyInformation2025(registration)
for j in range(1, 21): for j in range(1, 1 + NB_WORDS):
setattr(information, f'word{j}', random.choice(WORDS)) setattr(information, f'word{j}', random.choice(WORDS['list']))
information.step = 20 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) information.save(registration)
registration.save() registration.save()
@ -106,6 +108,6 @@ class TestWEIAlgorithm(TestCase):
max_score = buses[0][1] max_score = buses[0][1]
penalty += (max_score - score) ** 2 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 % self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -7,7 +7,7 @@ from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateVi
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \ WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \ BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \ WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView, WEIUpdateMembershipView
app_name = 'wei' app_name = 'wei'
urlpatterns = [ urlpatterns = [
@ -43,4 +43,6 @@ urlpatterns = [
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"), path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"), path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"), path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
path('edit_membership/<int:pk>/', WEIUpdateMembershipView.as_view(), name="wei_update_membership"),
] ]

View File

@ -13,7 +13,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q, Count from django.db.models import Q, Count, Case, When, Value, IntegerField, F
from django.db.models.functions.text import Lower from django.db.models.functions.text import Lower
from django import forms from django import forms
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
@ -35,7 +35,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \ from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \ from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable WEIRegistration1ATable, WEIMembershipTable
@ -133,6 +133,23 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
membership=None, membership=None,
wei=club 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")) \ buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name") .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
return [club_transactions, club_member, pre_registrations, buses, ] return [club_transactions, club_member, pre_registrations, buses, ]
@ -260,6 +277,23 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() 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", "") pattern = self.request.GET.get("search", "")
@ -510,7 +544,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI Register a new user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistration1AForm form_class = WEIRegistrationForm
extra_context = {"title": _("Register first year student to the WEI")} extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -606,7 +640,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI Register an old user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistration2AForm form_class = WEIRegistrationForm
extra_context = {"title": _("Register old student to the WEI")} extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -739,14 +773,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
if today >= wei.date_start or today < wei.membership_start: if today >= wei.date_start or today < wei.membership_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
# Store the validate parameter in the view's state # Store the validate parameter in the view's state
self.should_validate = request.GET.get('validate', False)
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["club"] = self.object.wei context["club"] = self.object.wei
# Pass the validate parameter to the template
context["should_validate"] = self.should_validate
if self.object.is_validated: if self.object.is_validated:
membership_form = self.get_membership_form(instance=self.object.membership, membership_form = self.get_membership_form(instance=self.object.membership,
@ -788,7 +819,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
if not self.object.first_year and "deposit_type" in form.fields: if not self.object.first_year and "deposit_type" in form.fields:
form.fields["deposit_type"].required = True form.fields["deposit_type"].required = True
form.fields["deposit_type"].help_text = _("Choose how you want to pay the deposit") 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 return form
@ -862,9 +892,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
survey = CurrentSurvey(self.object) survey = CurrentSurvey(self.object)
if not survey.is_complete(): if not survey.is_complete():
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) 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}) return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk})
@ -963,9 +990,9 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form = context["form"] form = context["form"]
if registration.soge_credit: if registration.soge_credit:
form.fields["credit_amount"].initial = registration.fee form.fields["credit_amount"].initial = fee
else: 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 return context
@ -1052,7 +1079,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if registration.soge_credit: if registration.soge_credit:
fee = 2000 fee = registration.wei.fee_soge_credit
kfet = club.parent_club kfet = club.parent_club
bde = kfet.parent_club bde = kfet.parent_club
@ -1154,6 +1181,49 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk}) 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): class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
""" """
Display the survey for the WEI for first year members. Display the survey for the WEI for first year members.
@ -1176,6 +1246,10 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
if not self.survey: if not self.survey:
self.survey = CurrentSurvey(obj) 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 the survey is complete, then display the end page.
if self.survey.is_complete(): if self.survey.is_complete():
return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,)))