diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py
index fd6fea2c..455d77ca 100644
--- a/apps/wei/forms/registration.py
+++ b/apps/wei/forms/registration.py
@@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
-from django.forms import CheckboxSelectMultiple
+from django.forms import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@@ -140,6 +140,19 @@ class WEIMembershipForm(forms.ModelForm):
required=False,
)
+ def __init__(self, *args, wei=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ if 'bus' in self.fields:
+ if wei is not None:
+ self.fields['bus'].queryset = Bus.objects.filter(wei=wei)
+ else:
+ self.fields['bus'].queryset = Bus.objects.none()
+ if 'team' in self.fields:
+ if wei is not None:
+ self.fields['team'].queryset = BusTeam.objects.filter(bus__wei=wei)
+ else:
+ self.fields['team'].queryset = BusTeam.objects.none()
+
def clean(self):
cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \
@@ -151,21 +164,8 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
- "bus": Autocomplete(
- Bus,
- attrs={
- 'api_url': '/api/wei/bus/',
- 'placeholder': 'Bus ...',
- }
- ),
- "team": Autocomplete(
- BusTeam,
- attrs={
- 'api_url': '/api/wei/team/',
- 'placeholder': 'Équipe ...',
- },
- resetable=True,
- ),
+ "bus": RadioSelect(),
+ "team": RadioSelect(),
}
@@ -213,4 +213,3 @@ class BusTeamForm(forms.ModelForm):
),
"color": ColorWidget(),
}
- # "color": ColorWidget(),
diff --git a/apps/wei/forms/surveys/__init__.py b/apps/wei/forms/surveys/__init__.py
index 0a33bc16..ef692d25 100644
--- a/apps/wei/forms/surveys/__init__.py
+++ b/apps/wei/forms/surveys/__init__.py
@@ -2,11 +2,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
-from .wei2024 import WEISurvey2024
+from .wei2025 import WEISurvey2025
__all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]
-CurrentSurvey = WEISurvey2024
+CurrentSurvey = WEISurvey2025
diff --git a/apps/wei/forms/surveys/base.py b/apps/wei/forms/surveys/base.py
index 3f3bff3b..c2fde39d 100644
--- a/apps/wei/forms/surveys/base.py
+++ b/apps/wei/forms/surveys/base.py
@@ -121,6 +121,13 @@ class WEISurveyAlgorithm:
"""
raise NotImplementedError
+ @classmethod
+ def get_bus_information_form(cls):
+ """
+ The class of the form to update the bus information.
+ """
+ raise NotImplementedError
+
class WEISurvey:
"""
diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py
new file mode 100644
index 00000000..d92cc23f
--- /dev/null
+++ b/apps/wei/forms/surveys/wei2025.py
@@ -0,0 +1,347 @@
+# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import time
+import json
+from functools import lru_cache
+from random import Random
+
+from django import forms
+from django.db import transaction
+from django.db.models import Q
+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',
+]
+
+
+class WEISurveyForm2025(forms.Form):
+ """
+ Survey form for the year 2025.
+ 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.
+ """
+ information = WEISurveyInformation2025(registration)
+ if not information.seed:
+ information.seed = int(1000 * time.time())
+ information.save(registration)
+ registration._force_save = True
+ registration.save()
+
+ if self.data:
+ self.fields["word"].choices = [(w, w) for w in WORDS]
+ 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
+
+ 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]
+
+
+class WEIBusInformation2025(WEIBusInformation):
+ """
+ For each word, the bus has a score
+ """
+ scores: dict
+
+ def __init__(self, bus):
+ self.scores = {}
+ for word in WORDS:
+ self.scores[word] = 0
+ super().__init__(bus)
+
+
+class BusInformationForm2025(forms.ModelForm):
+ class Meta:
+ model = Bus
+ fields = ['information_json']
+ widgets = {}
+
+ def __init__(self, *args, words=None, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ initial_scores = {}
+ if self.instance and self.instance.information_json:
+ try:
+ info = json.loads(self.instance.information_json)
+ initial_scores = info.get("scores", {})
+ except (json.JSONDecodeError, TypeError, AttributeError):
+ initial_scores = {}
+ if words is None:
+ words = WORDS
+ self.words = words
+
+ choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
+ for word in words:
+ self.fields[word] = forms.TypedChoiceField(
+ label=word,
+ choices=choices,
+ coerce=int,
+ initial=initial_scores.get(word, 0),
+ required=True,
+ widget=forms.RadioSelect,
+ help_text=_("Rate between 0 and 5."),
+ )
+
+ def clean(self):
+ cleaned_data = super().clean()
+ scores = {}
+ for word in self.words:
+ value = cleaned_data.get(word)
+ if value is not None:
+ scores[word] = value
+ # On encode en JSON
+ cleaned_data['information_json'] = json.dumps({"scores": scores})
+ return cleaned_data
+
+
+class WEISurveyInformation2025(WEISurveyInformation):
+ """
+ We store the id of the selected bus. We store only the name, but is not used in the selection:
+ that's only for humans that try to read data.
+ """
+ # Random seed that is stored at the first time to ensure that words are generated only once
+ seed = 0
+ step = 0
+
+ def __init__(self, registration):
+ for i in range(1, 21):
+ setattr(self, "word" + str(i), None)
+ super().__init__(registration)
+
+
+class WEISurvey2025(WEISurvey):
+ """
+ Survey for the year 2025.
+ """
+
+ @classmethod
+ def get_year(cls):
+ return 2025
+
+ @classmethod
+ def get_survey_information_class(cls):
+ return WEISurveyInformation2025
+
+ def get_form_class(self):
+ return WEISurveyForm2025
+
+ def update_form(self, form):
+ """
+ Filter the bus selector with the buses of the WEI.
+ """
+ form.set_registration(self.registration)
+
+ @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()
+
+ @classmethod
+ def get_algorithm_class(cls):
+ return WEISurveyAlgorithm2025
+
+ def is_complete(self) -> bool:
+ """
+ The survey is complete once the bus is chosen.
+ """
+ return self.information.step == 20
+
+ @classmethod
+ @lru_cache()
+ def word_mean(cls, word):
+ """
+ Calculate the mid-score given by all buses.
+ """
+ buses = cls.get_algorithm_class().get_buses()
+ return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
+
+ @lru_cache()
+ def score(self, bus):
+ 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
+ return s
+
+ @lru_cache()
+ def scores_per_bus(self):
+ return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
+
+ @lru_cache()
+ def ordered_buses(self):
+ values = list(self.scores_per_bus().items())
+ values.sort(key=lambda item: -item[1])
+ return values
+
+ @classmethod
+ def clear_cache(cls):
+ cls.word_mean.cache_clear()
+ return super().clear_cache()
+
+
+class WEISurveyAlgorithm2025(WEISurveyAlgorithm):
+ """
+ The algorithm class for the year 2025.
+ We use Gale-Shapley algorithm to attribute 1y students into buses.
+ """
+
+ @classmethod
+ def get_survey_class(cls):
+ return WEISurvey2025
+
+ @classmethod
+ def get_bus_information_class(cls):
+ return WEIBusInformation2025
+
+ @classmethod
+ def get_bus_information_form(cls):
+ return BusInformationForm2025
+
+ def run_algorithm(self, display_tqdm=False):
+ """
+ Gale-Shapley algorithm implementation.
+ We modify it to allow buses to have multiple "weddings".
+ """
+ surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
+ surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
+ # Don't manage hardcoded people
+ surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
+
+ # Reset previous algorithm run
+ for survey in surveys:
+ survey.free()
+ survey.save()
+
+ non_men = [s for s in surveys if s.registration.gender != 'male']
+ men = [s for s in surveys if s.registration.gender == 'male']
+
+ quotas = {}
+ registrations = self.get_registrations()
+ non_men_total = registrations.filter(~Q(gender='male')).count()
+ for bus in self.get_buses():
+ free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
+ # Remove hardcoded people
+ free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
+ registration__information_json__icontains="hardcoded").count()
+ quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
+
+ tqdm_obj = None
+ if display_tqdm:
+ from tqdm import tqdm
+ tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
+
+ # Repartition for non men people first
+ self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
+
+ quotas = {}
+ for bus in self.get_buses():
+ free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
+ free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
+ # Remove hardcoded people
+ free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
+ registration__information_json__icontains="hardcoded").count()
+ quotas[bus] = free_seats
+
+ if display_tqdm:
+ tqdm_obj.close()
+
+ from tqdm import tqdm
+ tqdm_obj = tqdm(total=len(men), desc="Hommes")
+
+ self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
+
+ if display_tqdm:
+ tqdm_obj.close()
+
+ # Clear cache information after running algorithm
+ WEISurvey2025.clear_cache()
+
+ def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
+ free_surveys = surveys.copy() # Remaining surveys
+ while free_surveys: # Some students are not affected
+ survey = free_surveys[0]
+ buses = survey.ordered_buses() # Preferences of the student
+ for bus, current_score 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)
+ survey.save()
+ free_surveys.remove(survey)
+ break
+ else:
+ # Current bus has not enough places. Remove the least preferred student from the bus if existing
+ least_preferred_survey = None
+ least_score = -1
+ # Find the least student in the bus that has a lower score than the current student
+ for survey2 in surveys:
+ if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
+ continue
+ score2 = survey2.score(bus)
+ if current_score <= score2: # Ignore better students
+ continue
+ if least_preferred_survey is None or score2 < least_score:
+ least_preferred_survey = survey2
+ least_score = score2
+
+ if least_preferred_survey is not None:
+ # Remove the least student from the bus and put the current student in.
+ # If it does not exist, choose the next bus.
+ least_preferred_survey.free()
+ least_preferred_survey.save()
+ free_surveys.append(least_preferred_survey)
+ survey.select_bus(bus)
+ survey.save()
+ free_surveys.remove(survey)
+ break
+ else:
+ raise ValueError(f"User {survey.registration.user} has no free seat")
+
+ if tqdm_obj is not None:
+ tqdm_obj.n = len(surveys) - len(free_surveys)
+ tqdm_obj.refresh()
diff --git a/apps/wei/templates/wei/bus_detail.html b/apps/wei/templates/wei/bus_detail.html
index 04ef5f9a..af4eaccb 100644
--- a/apps/wei/templates/wei/bus_detail.html
+++ b/apps/wei/templates/wei/bus_detail.html
@@ -22,6 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
{% trans "Edit" %}
+ {% trans "Edit information" %}
{% trans "Add team" %}
diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html
index f0f3d800..a9c85d5d 100644
--- a/apps/wei/templates/wei/weimembership_form.html
+++ b/apps/wei/templates/wei/weimembership_form.html
@@ -210,4 +210,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
}
}
+
{% endblock %}
diff --git a/apps/wei/tests/test_wei_algorithm_2024.py b/apps/wei/tests/test_wei_algorithm_2024.py
index bae36399..d1e5f428 100644
--- a/apps/wei/tests/test_wei_algorithm_2024.py
+++ b/apps/wei/tests/test_wei_algorithm_2024.py
@@ -6,8 +6,6 @@ from datetime import date, timedelta
from django.contrib.auth.models import User
from django.test import TestCase
-from django.urls import reverse
-from note.models import NoteUser
from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024
from ..models import Bus, WEIClub, WEIRegistration
@@ -129,44 +127,3 @@ class TestWEIAlgorithm(TestCase):
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
-
- def test_register_1a(self):
- """
- Test register a first year member to the WEI and complete the survey
- """
- response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
- self.assertEqual(response.status_code, 200)
-
- user = User.objects.create(username="toto", email="toto@example.com")
- NoteUser.objects.create(user=user)
- response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
- user=user.id,
- soge_credit=True,
- birth_date=date(2000, 1, 1),
- gender='nonbinary',
- clothing_cut='female',
- clothing_size='XS',
- health_issues='I am a bot',
- emergency_contact_name='NoteKfet2020',
- emergency_contact_phone='+33123456789',
- ))
- qs = WEIRegistration.objects.filter(user_id=user.id)
- self.assertTrue(qs.exists())
- registration = qs.get()
- self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
- for question in WORDS:
- # Fill 1A Survey, 10 pages
- # be careful if questionnary form change (number of page, type of answer...)
- response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
- question: "1"
- })
- registration.refresh_from_db()
- survey = WEISurvey2024(registration)
- self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
- 302 if survey.is_complete() else 200)
- self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
- survey = WEISurvey2024(registration)
- self.assertTrue(survey.is_complete())
- survey.select_bus(self.buses[0])
- survey.save()
- self.assertIsNotNone(survey.information.get_selected_bus())
diff --git a/apps/wei/tests/test_wei_algorithm_2025.py b/apps/wei/tests/test_wei_algorithm_2025.py
new file mode 100644
index 00000000..5930eb3b
--- /dev/null
+++ b/apps/wei/tests/test_wei_algorithm_2025.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import random
+
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from ..forms.surveys.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025
+from ..models import Bus, WEIClub, WEIRegistration
+
+
+class TestWEIAlgorithm(TestCase):
+ """
+ Run some tests to ensure that the WEI algorithm is working well.
+ """
+ fixtures = ('initial',)
+
+ def setUp(self):
+ """
+ Create some test data, with one WEI and 10 buses with random score attributions.
+ """
+ self.wei = WEIClub.objects.create(
+ name="WEI 2025",
+ email="wei2025@example.com",
+ date_start='2025-09-12',
+ date_end='2025-09-14',
+ year=2025,
+ membership_start='2025-06-01'
+ )
+
+ self.buses = []
+ for i in range(10):
+ 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)
+ information.save()
+ bus.save()
+
+ def test_survey_algorithm_small(self):
+ """
+ There are only a few people in each bus, ensure that each person has its best bus
+ """
+ # Add a few users
+ for i in range(10):
+ user = User.objects.create(username=f"user{i}")
+ registration = WEIRegistration.objects.create(
+ user=user,
+ wei=self.wei,
+ first_year=True,
+ 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
+ information.save(registration)
+ registration.save()
+
+ # Run algorithm
+ WEISurvey2025.get_algorithm_class()().run_algorithm()
+
+ # Ensure that everyone has its first choice
+ for r in WEIRegistration.objects.filter(wei=self.wei).all():
+ survey = WEISurvey2025(r)
+ preferred_bus = survey.ordered_buses()[0][0]
+ chosen_bus = survey.information.get_selected_bus()
+ self.assertEqual(preferred_bus, chosen_bus)
+
+ def test_survey_algorithm_full(self):
+ """
+ Buses are full of first year people, ensure that they are happy
+ """
+ # Add a lot of users
+ for i in range(95):
+ user = User.objects.create(username=f"user{i}")
+ registration = WEIRegistration.objects.create(
+ user=user,
+ wei=self.wei,
+ first_year=True,
+ 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
+ information.save(registration)
+ registration.save()
+
+ # Run algorithm
+ WEISurvey2025.get_algorithm_class()().run_algorithm()
+
+ penalty = 0
+ # Ensure that everyone seems to be happy
+ # We attribute a penalty for each user that didn't have its first choice
+ # The penalty is the square of the distance between the score of the preferred bus
+ # and the score of the attributed bus
+ # We consider it acceptable if the mean of this distance is lower than 5 %
+ for r in WEIRegistration.objects.filter(wei=self.wei).all():
+ 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
+
+ self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
diff --git a/apps/wei/tests/test_wei_registration.py b/apps/wei/tests/test_wei_registration.py
index f7ce5ac0..d286581c 100644
--- a/apps/wei/tests/test_wei_registration.py
+++ b/apps/wei/tests/test_wei_registration.py
@@ -778,7 +778,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
- self.assertEqual(CurrentSurvey.get_year(), 2024)
+ self.assertEqual(CurrentSurvey.get_year(), 2025)
class TestWeiAPI(TestAPI):
diff --git a/apps/wei/urls.py b/apps/wei/urls.py
index 3084dd51..5f9283c0 100644
--- a/apps/wei/urls.py
+++ b/apps/wei/urls.py
@@ -4,7 +4,7 @@
from django.urls import path
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
- WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \
+ WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
@@ -42,4 +42,5 @@ urlpatterns = [
path('detail//closed/', WEIClosedView.as_view(), name="wei_closed"),
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"),
]
diff --git a/apps/wei/views.py b/apps/wei/views.py
index 34b274a2..bfc7c616 100644
--- a/apps/wei/views.py
+++ b/apps/wei/views.py
@@ -788,7 +788,8 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
return form
def get_membership_form(self, data=None, instance=None):
- membership_form = WEIMembershipForm(data if data else None, instance=instance)
+ registration = self.get_object()
+ membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"]
@@ -969,6 +970,13 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return WEIMembership1AForm
return WEIMembershipForm
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
+ wei = registration.wei
+ kwargs['wei'] = wei
+ return kwargs
+
def get_form(self, form_class=None):
form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
@@ -1422,3 +1430,29 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
# On redirige vers la page d'attribution pour le premier étudiant trouvé
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,))
+
+
+class BusInformationUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
+ model = Bus
+
+ def get_form_class(self):
+ return CurrentSurvey.get_algorithm_class().get_bus_information_form()
+
+ def dispatch(self, request, *args, **kwargs):
+ wei = self.get_object().wei
+ today = date.today()
+ # We can't update a bus once the WEI is started
+ if today >= wei.date_start:
+ return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["club"] = self.object.wei
+ context["information"] = CurrentSurvey.get_algorithm_class().get_bus_information(self.object)
+ self.object.save()
+ return context
+
+ def get_success_url(self):
+ self.object.refresh_from_db()
+ return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 70aa20ce..9832c986 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-06-20 13:50+0200\n"
+"POT-Creation-Date: 2025-06-27 19:15+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi \n"
"Language-Team: French \n"
@@ -3046,9 +3046,14 @@ 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
+msgid "Rate between 0 and 5."
+msgstr "Note entre 0 et 5."
+
#: apps/wei/models.py:25 apps/wei/templates/wei/base.html:36
msgid "year"
msgstr "année"
@@ -3250,7 +3255,7 @@ msgstr "Année"
msgid "preferred bus"
msgstr "bus préféré"
-#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:36
+#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:38
#: apps/wei/templates/wei/busteam_detail.html:52
msgid "Teams"
msgstr "Équipes"
@@ -3344,18 +3349,23 @@ msgstr "Voir le WEI"
#: apps/wei/templates/wei/bus_detail.html:21
msgid "View club"
-msgstr "Voir le lub"
+msgstr "Voir le club"
#: apps/wei/templates/wei/bus_detail.html:26
+#| msgid "survey information"
+msgid "Edit information"
+msgstr "Modifier les informations"
+
+#: apps/wei/templates/wei/bus_detail.html:28
#: apps/wei/templates/wei/busteam_detail.html:24
msgid "Add team"
msgstr "Ajouter une équipe"
-#: apps/wei/templates/wei/bus_detail.html:49
+#: apps/wei/templates/wei/bus_detail.html:51
msgid "Members"
msgstr "Membres"
-#: apps/wei/templates/wei/bus_detail.html:58
+#: apps/wei/templates/wei/bus_detail.html:60
#: apps/wei/templates/wei/busteam_detail.html:62
#: apps/wei/templates/wei/weimembership_list.html:31
msgid "View as PDF"
@@ -3708,11 +3718,11 @@ msgstr "montant total"
msgid "Attribute buses to first year members"
msgstr "Répartir les 1A dans les bus"
-#: apps/wei/views.py:1379
+#: apps/wei/views.py:1380
msgid "Attribute bus"
msgstr "Attribuer un bus"
-#: apps/wei/views.py:1419
+#: apps/wei/views.py:1420
msgid ""
"No first year student without a bus found. Either all of them have a bus, or "
"none has filled the survey yet."