1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-08-03 22:24:34 +02:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Ehouarn
251bb933da Signals used to ignore _no_signal 2025-08-03 21:19:44 +02:00
Ehouarn
59a502d624 Added column deposit_type to MembershipsTable 2025-08-03 01:02:06 +02:00
Ehouarn
312ab6dac4 Permissions 2025-08-03 00:41:10 +02:00
Ehouarn
cf53b480db Minor fix 2025-08-02 23:42:04 +02:00
Ehouarn
d1aa1edd09 Deposit check logic changed 2025-08-02 23:32:13 +02:00
Ehouarn
d6f9a9c5b0 Better test 2025-08-02 18:35:53 +02:00
Ehouarn
573f2d8a22 More robust algorithm 2025-08-02 17:18:51 +02:00
Ehouarn
8e98d62b69 Soge credit fixed 2025-08-02 16:31:04 +02:00
Ehouarn
023fc1db84 Visual fixes 2025-08-01 22:53:15 +02:00
Ehouarn
d50bb2134a Algorithm changed again 2025-08-01 11:56:34 +02:00
17 changed files with 357 additions and 120 deletions

View File

@@ -32,7 +32,7 @@ class ActivityForm(forms.ModelForm):
def clean_organizer(self): def clean_organizer(self):
organizer = self.cleaned_data['organizer'] organizer = self.cleaned_data['organizer']
if not organizer.note.is_active: if not organizer.note.is_active:
self.add_error('organizer', _('The note of this club is inactive.')) self.add_error('organiser', _('The note of this club is inactive.'))
return organizer return organizer
def clean_date_end(self): def clean_date_end(self):

View File

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

View File

@@ -16,7 +16,8 @@ def save_user_profile(instance, created, raw, **_kwargs):
def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs): def update_wei_registration_fee_on_membership_creation(sender, instance, created, **kwargs):
if created: if not hasattr(instance, "_no_signal") and created:
print('update_wei_registration_fee_on_membership_creation')
from wei.models import WEIRegistration from wei.models import WEIRegistration
if instance.club.id == 1 or instance.club.id == 2: if instance.club.id == 1 or instance.club.id == 2:
registrations = WEIRegistration.objects.filter( registrations = WEIRegistration.objects.filter(
@@ -24,14 +25,16 @@ def update_wei_registration_fee_on_membership_creation(sender, instance, created
wei__year=instance.date_start.year, wei__year=instance.date_start.year,
) )
for r in registrations: for r in registrations:
r._force_save = True
r.save() r.save()
def update_wei_registration_fee_on_club_change(sender, instance, **kwargs): def update_wei_registration_fee_on_club_change(sender, instance, **kwargs):
from wei.models import WEIRegistration from wei.models import WEIRegistration
if instance.id == 1 or instance.id == 2: if not hasattr(instance, "_no_signal") and (instance.id == 1 or instance.id == 2):
registrations = WEIRegistration.objects.filter( registrations = WEIRegistration.objects.filter(
wei__year=instance.membership_start.year, wei__year=instance.membership_start.year,
) )
for r in registrations: for r in registrations:
r._force_save = True
r.save() r.save()

View File

@@ -1391,12 +1391,12 @@
"wei", "wei",
"weiregistration" "weiregistration"
], ],
"query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", "query": "[\"AND\", {\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}, {\"deposit_type\": \"note\"}]",
"type": "change", "type": "change",
"mask": 2, "mask": 2,
"field": "caution_check", "field": "deposit_given",
"permanent": false, "permanent": false,
"description": "Dire si un chèque de caution est donné pour une inscription WEI" "description": "Autoriser une transaction de caution WEI"
} }
}, },
{ {
@@ -4366,6 +4366,70 @@
"description": "Modifier l'équipe d'une adhésion WEI à son bus" "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"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@@ -4460,7 +4524,8 @@
159, 159,
160, 160,
212, 212,
222 222,
297
] ]
} }
}, },
@@ -4647,7 +4712,10 @@
176, 176,
177, 177,
178, 178,
183 183,
294,
295,
296
] ]
} }
}, },

View File

@@ -353,13 +353,11 @@ class SogeCredit(models.Model):
def amount(self): def amount(self):
if self.valid: if self.valid:
return self.credit_transaction.total return self.credit_transaction.total
amount = sum(max(transaction.total - 2000, 0) for transaction in self.transactions.all()) amount = 0
if 'wei' in settings.INSTALLED_APPS: transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False)
from wei.models import WEIMembership amount += sum(max(transaction.total - transaction.membership.club.weiclub.fee_soge_credit, 0) for transaction in transactions_wei)
if not WEIMembership.objects\ transactions_not_wei = self.transactions.filter(membership__club__weiclub__isnull=True)
.filter(club__weiclub__year=self.credit_transaction.created_at.year, user=self.user).exists(): amount += sum(transaction.total for transaction in transactions_not_wei)
# 80 € for people that don't go to WEI
amount += 8000
return amount return amount
def update_transactions(self): def update_transactions(self):
@@ -441,7 +439,7 @@ class SogeCredit(models.Model):
With Great Power Comes Great Responsibility... 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: if self.user.note.balance < total_fee:
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. " 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.")) "Please ask her/him to credit the note before invalidating this credit."))

View File

@@ -77,7 +77,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email', 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', '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', 'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
'emergency_contact_phone', ] 'emergency_contact_phone', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email', search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',

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', 'deposit_type' 'first_year', 'information_json', 'deposit_given', 'deposit_type'
] ]
widgets = { widgets = {
"user": Autocomplete( "user": Autocomplete(
@@ -59,8 +59,8 @@ class WEIRegistrationForm(forms.ModelForm):
'minDate': '1900-01-01', 'minDate': '1900-01-01',
'maxDate': '2100-01-01' 'maxDate': '2100-01-01'
}), }),
"deposit_check": forms.BooleanField( "deposit_given": forms.CheckboxInput(
required=False, attrs={'class': 'form-check-input'},
), ),
"deposit_type": forms.RadioSelect(), "deposit_type": forms.RadioSelect(),
} }
@@ -69,7 +69,7 @@ class WEIRegistrationForm(forms.ModelForm):
class WEIChooseBusForm(forms.Form): class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField( bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects, 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," 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."), + " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
@@ -161,7 +161,7 @@ class WEIMembership1AForm(WEIMembershipForm):
""" """
Used to confirm registrations of first year members without choosing a bus now. Used to confirm registrations of first year members without choosing a bus now.
""" """
deposit_check = None deposit_given = None
roles = None roles = None
def clean(self): def clean(self):

View File

@@ -30,117 +30,117 @@ WORDS = {
'Description 1', 'Description 1',
{ {
3: 'Réponse 1 Madagas[car]', 3: 'Réponse 1 Madagas[car]',
43: 'Réponse 1 Y2[KAR]', 4: 'Réponse 1 Y2[KAR]',
2: 'Réponse 1 Tcherno[bus]', 2: 'Réponse 1 Tcherno[bus]',
45: 'Réponse 1 [Kar]tier', 5: 'Réponse 1 [Kar]tier',
1: 'Réponse 1 [Car]cassonne', 1: 'Réponse 1 [Car]cassonne',
47: 'Réponse 1 O[car]ina', 6: 'Réponse 1 O[car]ina',
48: 'Réponse 1 Show[bus]', 7: 'Réponse 1 Show[bus]',
49: 'Réponse 1 [Car]ioca' 8: 'Réponse 1 [Car]ioca'
} }
], ],
'Question 2': [ 'Question 2': [
'Description 2', 'Description 2',
{ {
3: 'Réponse 2 Madagas[car]', 3: 'Réponse 2 Madagas[car]',
43: 'Réponse 2 Y2[KAR]', 4: 'Réponse 2 Y2[KAR]',
2: 'Réponse 2 Tcherno[bus]', 2: 'Réponse 2 Tcherno[bus]',
45: 'Réponse 2 [Kar]tier', 5: 'Réponse 2 [Kar]tier',
1: 'Réponse 2 [Car]cassonne', 1: 'Réponse 2 [Car]cassonne',
47: 'Réponse 2 O[car]ina', 6: 'Réponse 2 O[car]ina',
48: 'Réponse 2 Show[bus]', 7: 'Réponse 2 Show[bus]',
49: 'Réponse 2 [Car]ioca' 8: 'Réponse 2 [Car]ioca'
} }
], ],
'Question 3': [ 'Question 3': [
'Description 3', 'Description 3',
{ {
3: 'Réponse 3 Madagas[car]', 3: 'Réponse 3 Madagas[car]',
43: 'Réponse 3 Y2[KAR]', 4: 'Réponse 3 Y2[KAR]',
2: 'Réponse 3 Tcherno[bus]', 2: 'Réponse 3 Tcherno[bus]',
45: 'Réponse 3 [Kar]tier', 5: 'Réponse 3 [Kar]tier',
1: 'Réponse 3 [Car]cassonne', 1: 'Réponse 3 [Car]cassonne',
47: 'Réponse 3 O[car]ina', 6: 'Réponse 3 O[car]ina',
48: 'Réponse 3 Show[bus]', 7: 'Réponse 3 Show[bus]',
49: 'Réponse 3 [Car]ioca' 8: 'Réponse 3 [Car]ioca'
} }
], ],
'Question 4': [ 'Question 4': [
'Description 4', 'Description 4',
{ {
3: 'Réponse 4 Madagas[car]', 3: 'Réponse 4 Madagas[car]',
43: 'Réponse 4 Y2[KAR]', 4: 'Réponse 4 Y2[KAR]',
2: 'Réponse 4 Tcherno[bus]', 2: 'Réponse 4 Tcherno[bus]',
45: 'Réponse 4 [Kar]tier', 5: 'Réponse 4 [Kar]tier',
1: 'Réponse 4 [Car]cassonne', 1: 'Réponse 4 [Car]cassonne',
47: 'Réponse 4 O[car]ina', 6: 'Réponse 4 O[car]ina',
48: 'Réponse 4 Show[bus]', 7: 'Réponse 4 Show[bus]',
49: 'Réponse 4 [Car]ioca' 8: 'Réponse 4 [Car]ioca'
} }
], ],
'Question 5': [ 'Question 5': [
'Description 5', 'Description 5',
{ {
3: 'Réponse 5 Madagas[car]', 3: 'Réponse 5 Madagas[car]',
43: 'Réponse 5 Y2[KAR]', 4: 'Réponse 5 Y2[KAR]',
2: 'Réponse 5 Tcherno[bus]', 2: 'Réponse 5 Tcherno[bus]',
45: 'Réponse 5 [Kar]tier', 5: 'Réponse 5 [Kar]tier',
1: 'Réponse 5 [Car]cassonne', 1: 'Réponse 5 [Car]cassonne',
47: 'Réponse 5 O[car]ina', 6: 'Réponse 5 O[car]ina',
48: 'Réponse 5 Show[bus]', 7: 'Réponse 5 Show[bus]',
49: 'Réponse 5 [Car]ioca' 8: 'Réponse 5 [Car]ioca'
} }
], ],
'Question 6': [ 'Question 6': [
'Description 6', 'Description 6',
{ {
3: 'Réponse 6 Madagas[car]', 3: 'Réponse 6 Madagas[car]',
43: 'Réponse 6 Y2[KAR]', 4: 'Réponse 6 Y2[KAR]',
2: 'Réponse 6 Tcherno[bus]', 2: 'Réponse 6 Tcherno[bus]',
45: 'Réponse 6 [Kar]tier', 5: 'Réponse 6 [Kar]tier',
1: 'Réponse 6 [Car]cassonne', 1: 'Réponse 6 [Car]cassonne',
47: 'Réponse 6 O[car]ina', 6: 'Réponse 6 O[car]ina',
48: 'Réponse 6 Show[bus]', 7: 'Réponse 6 Show[bus]',
49: 'Réponse 6 [Car]ioca' 8: 'Réponse 6 [Car]ioca'
} }
], ],
'Question 7': [ 'Question 7': [
'Description 7', 'Description 7',
{ {
3: 'Réponse 7 Madagas[car]', 3: 'Réponse 7 Madagas[car]',
43: 'Réponse 7 Y2[KAR]', 4: 'Réponse 7 Y2[KAR]',
2: 'Réponse 7 Tcherno[bus]', 2: 'Réponse 7 Tcherno[bus]',
45: 'Réponse 7 [Kar]tier', 5: 'Réponse 7 [Kar]tier',
1: 'Réponse 7 [Car]cassonne', 1: 'Réponse 7 [Car]cassonne',
47: 'Réponse 7 O[car]ina', 6: 'Réponse 7 O[car]ina',
48: 'Réponse 7 Show[bus]', 7: 'Réponse 7 Show[bus]',
49: 'Réponse 7 [Car]ioca' 8: 'Réponse 7 [Car]ioca'
} }
], ],
'Question 8': [ 'Question 8': [
'Description 8', 'Description 8',
{ {
3: 'Réponse 8 Madagas[car]', 3: 'Réponse 8 Madagas[car]',
43: 'Réponse 8 Y2[KAR]', 4: 'Réponse 8 Y2[KAR]',
2: 'Réponse 8 Tcherno[bus]', 2: 'Réponse 8 Tcherno[bus]',
45: 'Réponse 8 [Kar]tier', 5: 'Réponse 8 [Kar]tier',
1: 'Réponse 8 [Car]cassonne', 1: 'Réponse 8 [Car]cassonne',
47: 'Réponse 8 O[car]ina', 6: 'Réponse 8 O[car]ina',
48: 'Réponse 8 Show[bus]', 7: 'Réponse 8 Show[bus]',
49: 'Réponse 8 [Car]ioca' 8: 'Réponse 8 [Car]ioca'
} }
], ],
'Question 9': [ 'Question 9': [
'Description 9', 'Description 9',
{ {
3: 'Réponse 9 Madagas[car]', 3: 'Réponse 9 Madagas[car]',
43: 'Réponse 9 Y2[KAR]', 4: 'Réponse 9 Y2[KAR]',
2: 'Réponse 9 Tcherno[bus]', 2: 'Réponse 9 Tcherno[bus]',
45: 'Réponse 9 [Kar]tier', 5: 'Réponse 9 [Kar]tier',
1: 'Réponse 9 [Car]cassonne', 1: 'Réponse 9 [Car]cassonne',
47: 'Réponse 9 O[car]ina', 6: 'Réponse 9 O[car]ina',
48: 'Réponse 9 Show[bus]', 7: 'Réponse 9 Show[bus]',
49: 'Réponse 9 [Car]ioca' 8: 'Réponse 9 [Car]ioca'
} }
] ]
} }
@@ -365,25 +365,41 @@ class WEISurvey2025(WEISurvey):
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count() return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
@lru_cache() @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(): if not self.is_complete():
raise ValueError("Survey is not ended, can't calculate score") raise ValueError("Survey is not ended, can't calculate score")
bus_info = self.get_algorithm_class().get_bus_information(bus) 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, 1 + NB_WORDS)) / NB_WORDS - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / self.get_algorithm_class().get_buses().count()
s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk))
return s return s
@lru_cache() @lru_cache()
def scores_per_bus(self): def scores_per_bus(self):
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()} return {bus: (self.score_questions(bus), self.score_words(bus)) for bus in self.get_algorithm_class().get_buses()}
@lru_cache() @lru_cache()
def ordered_buses(self): 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 = list(self.scores_per_bus().items())
values.sort(key=lambda item: -item[1]) values.sort(key=lambda item: -item[1][0])
return values return values
@classmethod @classmethod
@@ -421,6 +437,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
""" """
Gale-Shapley algorithm implementation. Gale-Shapley algorithm implementation.
We modify it to allow buses to have multiple "weddings". We modify it to allow buses to have multiple "weddings".
We use lexigographical order on both scores
""" """
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
@@ -481,7 +498,7 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
while free_surveys: # Some students are not affected while free_surveys: # Some students are not affected
survey = free_surveys[0] survey = free_surveys[0]
buses = survey.ordered_buses() # Preferences of the student buses = survey.ordered_buses() # Preferences of the student
for bus, current_score in buses: for bus, current_scores in buses:
if self.get_bus_information(bus).has_free_seats(surveys, quotas): if self.get_bus_information(bus).has_free_seats(surveys, quotas):
# Selected bus has free places. Put student in the bus # Selected bus has free places. Put student in the bus
survey.select_bus(bus) survey.select_bus(bus)
@@ -496,8 +513,8 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
for survey2 in surveys: for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus: if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue continue
score2 = survey2.score(bus) score2 = survey2.score_questions(bus)
if current_score <= score2: # Ignore better students if current_scores[1] <= score2: # Ignore better students
continue continue
if least_preferred_survey is None or score2 < least_score: if least_preferred_survey is None or score2 < least_score:
least_preferred_survey = survey2 least_preferred_survey = survey2

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ class WEIClub(Club):
fee_soge_credit = models.PositiveIntegerField( fee_soge_credit = models.PositiveIntegerField(
verbose_name=_("membership fee (soge credit)"), verbose_name=_("membership fee (soge credit)"),
default=2000, default=0,
) )
class Meta: class Meta:
@@ -202,9 +202,9 @@ class WEIRegistration(models.Model):
verbose_name=_("Credit from Société générale"), verbose_name=_("Credit from Société générale"),
) )
deposit_check = models.BooleanField( deposit_given = models.BooleanField(
default=False, default=False,
verbose_name=_("Deposit check given") verbose_name=_("Deposit given")
) )
deposit_type = models.CharField( deposit_type = models.CharField(

View File

@@ -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("""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="1.5em" height="1.5em"
fill="currentColor" style="position: relative; left: -0.15em;">
<path d="
M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512
C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128z
M360 352L488 352C501.3 352 512 362.7 512 376C512 389.3 501.3 400 488 400L360 400
C346.7 400 336 389.3 336 376C336 362.7 346.7 352 360 352z
M336 264C336 250.7 346.7 240 360 240L488 240C501.3 240 512 250.7 512 264
C512 277.3 501.3 288 488 288L360 288C346.7 288 336 277.3 336 264z
M212 208C223 208 232 217 232 228L232 232L240 232C251 232 260 241 260 252
C260 263 251 272 240 272L192.5 272C185.6 272 180 277.6 180 284.5
C180 290.6 184.4 295.8 190.4 296.8L232.1 303.8C257.4 308 276 329.9 276 355.6
C276 381.7 257 403.3 232 407.4L232 412.1C232 423.1 223 432.1 212 432.1
C201 432.1 192 423.1 192 412.1L192 408.1L168 408.1C157 408.1 148 399.1 148 388.1
C148 377.1 157 368.1 168 368.1L223.5 368.1C230.4 368.1 236 362.5 236 355.6
C236 349.5 231.6 344.3 225.6 343.3L183.9 336.3C158.5 332 140 310.1 140 284.5
C140 255.7 163.2 232.3 192 232L192 228C192 217 201 208 212 208z
" />
</svg>
""")
if record.deposit_type == 'note':
return format_html("<i class=\"fa fa-exchange\"></i>")
def render_validate(self, record): def render_validate(self, record):
hasperm = PermissionBackend.check_perm( hasperm = PermissionBackend.check_perm(
get_current_request(), "wei.add_weimembership", WEIMembership( get_current_request(), "wei.add_weimembership", WEIMembership(
@@ -125,8 +154,8 @@ class WEIRegistrationTable(tables.Table):
order_by = ('validate', 'user',) 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_given',
'edit', 'validate', 'delete',) 'deposit_type', 'edit', 'validate', 'delete',)
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),
@@ -158,6 +187,35 @@ class WEIMembershipTable(tables.Table):
def render_year(self, record): def render_year(self, record):
return str(record.user.profile.ens_year) + "A" 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("""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="1.5em" height="1.5em"
fill="currentColor" style="position: relative; left: -0.15em;">
<path d="
M128 128C92.7 128 64 156.7 64 192L64 448C64 483.3 92.7 512 128 512L512 512
C547.3 512 576 483.3 576 448L576 192C576 156.7 547.3 128 512 128L128 128z
M360 352L488 352C501.3 352 512 362.7 512 376C512 389.3 501.3 400 488 400L360 400
C346.7 400 336 389.3 336 376C336 362.7 346.7 352 360 352z
M336 264C336 250.7 346.7 240 360 240L488 240C501.3 240 512 250.7 512 264
C512 277.3 501.3 288 488 288L360 288C346.7 288 336 277.3 336 264z
M212 208C223 208 232 217 232 228L232 232L240 232C251 232 260 241 260 252
C260 263 251 272 240 272L192.5 272C185.6 272 180 277.6 180 284.5
C180 290.6 184.4 295.8 190.4 296.8L232.1 303.8C257.4 308 276 329.9 276 355.6
C276 381.7 257 403.3 232 407.4L232 412.1C232 423.1 223 432.1 212 432.1
C201 432.1 192 423.1 192 412.1L192 408.1L168 408.1C157 408.1 148 399.1 148 388.1
C148 377.1 157 368.1 168 368.1L223.5 368.1C230.4 368.1 236 362.5 236 355.6
C236 349.5 231.6 344.3 225.6 343.3L183.9 336.3C158.5 332 140 310.1 140 284.5
C140 255.7 163.2 232.3 192 232L192 228C192 217 201 208 212 208z
" />
</svg>
""")
if record.registration.deposit_type == 'note':
return format_html("<i class=\"fa fa-exchange\"></i>")
class Meta: class Meta:
attrs = { attrs = {
'class': 'table table-condensed table-striped table-hover' 'class': 'table table-condensed table-striped table-hover'
@@ -165,7 +223,7 @@ class WEIMembershipTable(tables.Table):
model = WEIMembership model = WEIMembership
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department', 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 = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),

View File

@@ -96,7 +96,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
{% else %} {% else %}
<dt class="col-xl-6">{% trans 'Deposit check given'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'Deposit check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.deposit_check|yesno }}</dd> <dd class="col-xl-6">{{ registration.deposit_given|yesno }}</dd>
{% with information=registration.information %} {% with information=registration.information %}
<dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt>
@@ -143,7 +143,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endif %} {% endif %}
<div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}"> <div class="alert {% if registration.validation_status == 2 %}alert-danger{% else %}alert-success{% endif %}">
<h5>{% trans "Required payments:" %}</h5> <h5>{% trans "Required payments:" %}</h5>
<ul> <ul>
<li>{% blocktrans trimmed with amount=fee|pretty_money %} <li>{% blocktrans trimmed with amount=fee|pretty_money %}
@@ -169,9 +169,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}</p> {% endblocktrans %}</p>
</div> </div>
{% 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' %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %} {% trans "The user didn't give her/his caution." %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -30,7 +30,7 @@ class TestWEIAlgorithm(TestCase):
) )
self.buses = [] 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) 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)
@@ -74,7 +74,7 @@ class TestWEIAlgorithm(TestCase):
Buses are full of first year people, ensure that they are happy Buses are full of first year people, ensure that they are happy
""" """
# Add a lot of users # Add a lot of users
for i in range(95): for i in range(80):
user = User.objects.create(username=f"user{i}") user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create( registration = WEIRegistration.objects.create(
user=user, user=user,
@@ -90,6 +90,7 @@ class TestWEIAlgorithm(TestCase):
information.step = len(WORDS['questions']) + 1 information.step = len(WORDS['questions']) + 1
information.save(registration) information.save(registration)
registration.save() registration.save()
survey = WEISurvey2025(registration)
# Run algorithm # Run algorithm
WEISurvey2025.get_algorithm_class()().run_algorithm() WEISurvey2025.get_algorithm_class()().run_algorithm()
@@ -104,10 +105,25 @@ class TestWEIAlgorithm(TestCase):
survey = WEISurvey2025(r) survey = WEISurvey2025(r)
chosen_bus = survey.information.get_selected_bus() chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses() buses = survey.ordered_buses()
score = min(v for bus, v in buses if bus == chosen_bus) '''print(buses)
max_score = buses[0][1] print(chosen_bus)'''
penalty += (max_score - score) ** 2 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)
self.assertLessEqual(max_score - score, 1) # Always less than 25 % of tolerance 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 % 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)

View File

@@ -101,7 +101,7 @@ class TestWEIRegistration(TestCase):
user_id=self.user.id, user_id=self.user.id,
wei_id=self.wei.id, wei_id=self.wei.id,
soge_credit=True, soge_credit=True,
deposit_check=True, deposit_given=True,
birth_date=date(2000, 1, 1), birth_date=date(2000, 1, 1),
gender="nonbinary", gender="nonbinary",
clothing_cut="male", clothing_cut="male",
@@ -642,7 +642,7 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", bank="Société générale",
deposit_check=True, deposit_given=True,
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["form"].is_valid()) self.assertFalse(response.context["form"].is_valid())
@@ -657,7 +657,7 @@ class TestWEIRegistration(TestCase):
last_name="admin", last_name="admin",
first_name="admin", first_name="admin",
bank="Société générale", 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) 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, user_id=self.user.id,
wei_id=self.wei.id, wei_id=self.wei.id,
soge_credit=True, soge_credit=True,
deposit_check=True, deposit_given=True,
birth_date=date(2000, 1, 1), birth_date=date(2000, 1, 1),
gender="nonbinary", gender="nonbinary",
clothing_cut="male", clothing_cut="male",

View File

@@ -594,8 +594,8 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
# Cacher les champs pendant l'inscription initiale # Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields: if "first_year" in form.fields:
del form.fields["first_year"] del form.fields["first_year"]
if "deposit_check" in form.fields: if "deposit_given" in form.fields:
del form.fields["deposit_check"] del form.fields["deposit_given"]
if "information_json" in form.fields: if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
if "deposit_type" in form.fields: if "deposit_type" in form.fields:
@@ -704,8 +704,8 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
# Cacher les champs pendant l'inscription initiale # Cacher les champs pendant l'inscription initiale
if "first_year" in form.fields: if "first_year" in form.fields:
del form.fields["first_year"] del form.fields["first_year"]
if "deposit_check" in form.fields: if "deposit_given" in form.fields:
del form.fields["deposit_check"] del form.fields["deposit_given"]
if "information_json" in form.fields: if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
@@ -798,11 +798,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"]) choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
context["membership_form"] = choose_bus_form 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 return context
def get_form(self, form_class=None): def get_form(self, form_class=None):
@@ -811,9 +806,11 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# The auto-json-format may cause issues with the default field remove # The auto-json-format may cause issues with the default field remove
if "information_json" in form.fields: if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
# Masquer le champ deposit_check pour tout le monde dans le formulaire de modification # Masquer le champ deposit_given pour tout le monde dans le formulaire de modification
if "deposit_check" in form.fields: if "deposit_given" in form.fields:
del form.fields["deposit_check"] 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+ # S'assurer que le champ deposit_type est obligatoire pour les 2A+
if "deposit_type" in form.fields: if "deposit_type" in form.fields:
@@ -823,6 +820,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
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")
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 return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
@@ -1017,17 +1018,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["last_name"].initial = registration.user.last_name form.fields["last_name"].initial = registration.user.last_name
form.fields["first_name"].initial = registration.user.first_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 not registration.first_year:
if registration.deposit_type == 'check': if registration.deposit_type == 'check':
form.fields["deposit_check"] = forms.BooleanField( form.fields["deposit_given"] = forms.BooleanField(
required=True, required=True,
initial=registration.deposit_check, disabled=True,
initial=registration.deposit_given,
label=_("Deposit check 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: else:
form.fields["deposit_check"] = forms.BooleanField( form.fields["deposit_given"] = forms.BooleanField(
required=True, required=True,
initial=False, initial=False,
label=_("Create deposit transaction"), label=_("Create deposit transaction"),
@@ -1068,8 +1070,8 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
club = registration.wei club = registration.wei
user = registration.user user = registration.user
if "deposit_check" in form.data: if "deposit_given" in form.data:
registration.deposit_check = form.data["deposit_check"] == "on" registration.deposit_given = form.data["deposit_given"] == "on"
registration.save() registration.save()
membership = form.instance membership = form.instance
membership.user = user membership.user = user
@@ -1125,16 +1127,16 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
'credit': credit_amount, 'credit': credit_amount,
'needed': total_needed} 'needed': total_needed}
) )
return super().form_invalid(form) return self.form_invalid(form)
if credit_amount: if credit_amount:
if not last_name: if not last_name:
form.add_error('last_name', _("This field is required.")) form.add_error('last_name', _("This field is required."))
return super().form_invalid(form) return self.form_invalid(form)
if not first_name: if not first_name:
form.add_error('first_name', _("This field is required.")) 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 # Credit note before adding the membership
SpecialTransaction.objects.create( SpecialTransaction.objects.create(
@@ -1178,6 +1180,13 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return super().form_valid(form) 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): def get_success_url(self):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk}) return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk})

View File

@@ -3152,8 +3152,10 @@ msgid "Note transaction"
msgstr "Transaction Note" msgstr "Transaction Note"
#: apps/wei/models.py:217 #: apps/wei/models.py:217
#, fuzzy
#| msgid "Credit type"
msgid "deposit type" msgid "deposit type"
msgstr "type de caution" msgstr "Type de rechargement"
#: apps/wei/models.py:221 apps/wei/templates/wei/weimembership_form.html:64 #: apps/wei/models.py:221 apps/wei/templates/wei/weimembership_form.html:64
msgid "birth date" msgid "birth date"
@@ -4093,6 +4095,14 @@ msgstr "La note est indisponible pour le moment"
msgid "Thank you for your understanding -- The Respos Info of BDE" msgid "Thank you for your understanding -- The Respos Info of BDE"
msgstr "Merci de votre compréhension -- Les Respos Info du BDE" msgstr "Merci de votre compréhension -- Les Respos Info du BDE"
#: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name..."
msgstr "Chercher par un attribut tel que le nom..."
#: note_kfet/templates/base_search.html:23
msgid "There is no results."
msgstr "Il n'y a pas de résultat."
#: note_kfet/templates/cas/logged.html:8 #: note_kfet/templates/cas/logged.html:8
msgid "" msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central " "<h3>Log In Successful</h3>You have successfully logged into the Central "