1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-27 20:22:15 +02:00

Compare commits

..

3 Commits
main ... wei

Author SHA1 Message Date
bc517f02e5 Traduction 2025-06-27 19:26:58 +02:00
e83ee8015f Tests 2025-06-27 18:50:37 +02:00
c26534b6b7 Année et algorithme 2025-06-27 16:56:17 +02:00
16 changed files with 528 additions and 98 deletions

View File

@ -63,8 +63,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
valid_regex = is_regex(pattern)
suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
| Q(**{f'owner__name{suffix}': prefix + pattern}))
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern}))
else:
qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))

View File

@ -1,10 +1,8 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes
from member.models import Club
from note.models import Alias
from note_kfet.middlewares import get_current_request
from .backends import PermissionBackend
@ -19,46 +17,25 @@ class PermissionScopes(BaseScopes):
"""
def get_all_scopes(self):
scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()}
scopes['openid'] = "OpenID Connect"
return scopes
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()}
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
scopes = [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes.append('openid')
return scopes
return [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
scopes = [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
class PermissionOAuth2Validator(OAuth2Validator):
oidc_claim_scope = OAuth2Validator.oidc_claim_scope
oidc_claim_scope.update({"name": 'openid',
"normalized_name": 'openid',
"email": 'openid',
})
def get_additional_claims(self, request):
return {
"name": request.user.username,
"normalized_name": Alias.normalize(request.user.username),
"email": request.user.email,
}
def get_discovery_claims(self, request):
claims = super().get_discovery_claims(self)
return claims + ["name", "normalized_name", "email"]
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
@ -77,8 +54,6 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes:
valid_scopes.add(scope)
if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes

View File

@ -19,7 +19,6 @@ EXCLUDED = [
'oauth2_provider.accesstoken',
'oauth2_provider.grant',
'oauth2_provider.refreshtoken',
'oauth2_provider.idtoken',
'sessions.session',
]

View File

@ -171,7 +171,7 @@ class ScopesView(LoginRequiredMixin, TemplateView):
available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict()
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
# items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
for k, v in items:
context["scopes"][app][k] = v

View File

@ -213,4 +213,3 @@ class BusTeamForm(forms.ModelForm):
),
"color": ColorWidget(),
}
# "color": ColorWidget(),

View File

@ -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

View File

@ -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:
"""

View File

@ -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()

View File

@ -22,6 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_info' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit information" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>

View File

@ -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())

View File

@ -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 %

View File

@ -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):

View File

@ -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/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
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('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
]

View File

@ -1422,3 +1422,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})

View File

@ -136,7 +136,7 @@ de diffusion utiles.
Faîtes attention, donc où la sortie est stockée.
Il prend 4 options :
Il prend 2 options :
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es
@ -149,10 +149,7 @@ Il prend 4 options :
pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe
laquelle des ``n+1`` dernières années.
* ``--email``, qui prend en argument une chaine de caractère contenant une adresse email.
Si aucun email n'est renseigné, le script sort sur la sortie standard la liste des adresses mails à inscrire.
Dans le cas contraire, la liste est envoyée à l'adresse passée en argument.
Le script sort sur la sortie standard la liste des adresses mails à inscrire.
Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est
malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives.

View File

@ -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 <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\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."