diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 1e738361..8c69e367 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -4347,7 +4347,23 @@ "mask": 3, "field": "", "permanent": false, - "description": "Ajouter un membre au BDE ou à la Kfet" + "description": "Faire adhérer BDE ou Kfet" + } + }, + { + "model": "permission.permission", + "pk": 293, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "[\"AND\", {\"bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}]", + "type": "change", + "mask": 2, + "field": "team", + "permanent": false, + "description": "Modifier l'équipe d'une adhésion WEI à son bus" } }, { @@ -4764,7 +4780,6 @@ "name": "Chef\u22c5fe de bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4778,7 +4793,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, @@ -4790,7 +4806,6 @@ "name": "Chef\u22c5fe d'\u00e9quipe", "permissions": [ 22, - 84, 116, 123, 124, @@ -4805,8 +4820,7 @@ "for_club": null, "name": "\u00c9lectron libre", "permissions": [ - 22, - 84 + 22 ] } }, @@ -4957,7 +4971,6 @@ "name": "Référent⋅e Bus", "permissions": [ 22, - 84, 115, 117, 118, @@ -4971,7 +4984,8 @@ 287, 289, 290, - 291 + 291, + 293 ] } }, diff --git a/apps/wei/forms/surveys/wei2025.py b/apps/wei/forms/surveys/wei2025.py index d92cc23f..ee748c6c 100644 --- a/apps/wei/forms/surveys/wei2025.py +++ b/apps/wei/forms/surveys/wei2025.py @@ -14,16 +14,139 @@ from django.utils.translation import gettext_lazy as _ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation from ...models import WEIMembership, Bus -WORDS = [ - '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', - 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', - 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', - 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', - 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', - 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', - 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', - 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', -] +WORDS = { + 'list': [ + '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', + 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', + 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', + 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', + 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', + 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', + 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', + 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', + ], + 'questions': { + 'Question 1': [ + 'Description 1', + { + 3: 'Réponse 1 Madagas[car]', + 43: 'Réponse 1 Y2[KAR]', + 2: 'Réponse 1 Tcherno[bus]', + 45: 'Réponse 1 [Kar]tier', + 1: 'Réponse 1 [Car]cassonne', + 47: 'Réponse 1 O[car]ina', + 48: 'Réponse 1 Show[bus]', + 49: 'Réponse 1 [Car]ioca' + } + ], + 'Question 2': [ + 'Description 2', + { + 3: 'Réponse 2 Madagas[car]', + 43: 'Réponse 2 Y2[KAR]', + 2: 'Réponse 2 Tcherno[bus]', + 45: 'Réponse 2 [Kar]tier', + 1: 'Réponse 2 [Car]cassonne', + 47: 'Réponse 2 O[car]ina', + 48: 'Réponse 2 Show[bus]', + 49: 'Réponse 2 [Car]ioca' + } + ], + 'Question 3': [ + 'Description 3', + { + 3: 'Réponse 3 Madagas[car]', + 43: 'Réponse 3 Y2[KAR]', + 2: 'Réponse 3 Tcherno[bus]', + 45: 'Réponse 3 [Kar]tier', + 1: 'Réponse 3 [Car]cassonne', + 47: 'Réponse 3 O[car]ina', + 48: 'Réponse 3 Show[bus]', + 49: 'Réponse 3 [Car]ioca' + } + ], + 'Question 4': [ + 'Description 4', + { + 3: 'Réponse 4 Madagas[car]', + 43: 'Réponse 4 Y2[KAR]', + 2: 'Réponse 4 Tcherno[bus]', + 45: 'Réponse 4 [Kar]tier', + 1: 'Réponse 4 [Car]cassonne', + 47: 'Réponse 4 O[car]ina', + 48: 'Réponse 4 Show[bus]', + 49: 'Réponse 4 [Car]ioca' + } + ], + 'Question 5': [ + 'Description 5', + { + 3: 'Réponse 5 Madagas[car]', + 43: 'Réponse 5 Y2[KAR]', + 2: 'Réponse 5 Tcherno[bus]', + 45: 'Réponse 5 [Kar]tier', + 1: 'Réponse 5 [Car]cassonne', + 47: 'Réponse 5 O[car]ina', + 48: 'Réponse 5 Show[bus]', + 49: 'Réponse 5 [Car]ioca' + } + ], + 'Question 6': [ + 'Description 6', + { + 3: 'Réponse 6 Madagas[car]', + 43: 'Réponse 6 Y2[KAR]', + 2: 'Réponse 6 Tcherno[bus]', + 45: 'Réponse 6 [Kar]tier', + 1: 'Réponse 6 [Car]cassonne', + 47: 'Réponse 6 O[car]ina', + 48: 'Réponse 6 Show[bus]', + 49: 'Réponse 6 [Car]ioca' + } + ], + 'Question 7': [ + 'Description 7', + { + 3: 'Réponse 7 Madagas[car]', + 43: 'Réponse 7 Y2[KAR]', + 2: 'Réponse 7 Tcherno[bus]', + 45: 'Réponse 7 [Kar]tier', + 1: 'Réponse 7 [Car]cassonne', + 47: 'Réponse 7 O[car]ina', + 48: 'Réponse 7 Show[bus]', + 49: 'Réponse 7 [Car]ioca' + } + ], + 'Question 8': [ + 'Description 8', + { + 3: 'Réponse 8 Madagas[car]', + 43: 'Réponse 8 Y2[KAR]', + 2: 'Réponse 8 Tcherno[bus]', + 45: 'Réponse 8 [Kar]tier', + 1: 'Réponse 8 [Car]cassonne', + 47: 'Réponse 8 O[car]ina', + 48: 'Réponse 8 Show[bus]', + 49: 'Réponse 8 [Car]ioca' + } + ], + 'Question 9': [ + 'Description 9', + { + 3: 'Réponse 9 Madagas[car]', + 43: 'Réponse 9 Y2[KAR]', + 2: 'Réponse 9 Tcherno[bus]', + 45: 'Réponse 9 [Kar]tier', + 1: 'Réponse 9 [Car]cassonne', + 47: 'Réponse 9 O[car]ina', + 48: 'Réponse 9 Show[bus]', + 49: 'Réponse 9 [Car]ioca' + } + ] + } +} + +NB_WORDS = 5 class WEISurveyForm2025(forms.Form): @@ -32,11 +155,6 @@ class WEISurveyForm2025(forms.Form): Members choose 20 words, from which we calculate the best associated bus. """ - word = forms.ChoiceField( - label=_("Choose a word:"), - widget=forms.RadioSelect(), - ) - def set_registration(self, registration): """ Filter the bus selector with the buses of the current WEI. @@ -48,34 +166,56 @@ class WEISurveyForm2025(forms.Form): registration._force_save = True registration.save() - if self.data: - self.fields["word"].choices = [(w, w) for w in WORDS] + rng = Random((information.step + 1) * information.seed) + + if information.step == 0: + self.fields["words"] = forms.MultipleChoiceField( + label=_(f"Choose {NB_WORDS} words:"), + choices=[(w, w) for w in WORDS['list']], + widget=forms.CheckboxSelectMultiple(), + required=True, + ) if self.is_valid(): return - rng = Random((information.step + 1) * information.seed) + buses = WEISurveyAlgorithm2025.get_buses() + informations = {bus: WEIBusInformation2025(bus) for bus in buses} + scores = sum((list(informations[bus].scores.values()) for bus in buses), []) + if scores: + average_score = sum(scores) / len(scores) + else: + average_score = 0 - buses = WEISurveyAlgorithm2025.get_buses() - informations = {bus: WEIBusInformation2025(bus) for bus in buses} - scores = sum((list(informations[bus].scores.values()) for bus in buses), []) - if scores: - average_score = sum(scores) / len(scores) + preferred_words = { + bus: [word for word in WORDS['list'] if informations[bus].scores[word] >= average_score] + for bus in buses + } + + all_preferred_words = set() + for bus_words in preferred_words.values(): + all_preferred_words.update(bus_words) + all_preferred_words = list(all_preferred_words) + rng.shuffle(all_preferred_words) + self.fields["words"].choices = [(w, w) for w in all_preferred_words] else: - average_score = 0 + questions = list(WORDS['questions'].items()) + idx = information.step - 1 + if idx < len(questions): + q, (desc, answers) = questions[idx] + choices = [(k, v) for k, v in answers.items()] + rng.shuffle(choices) + self.fields[q] = forms.ChoiceField( + label=desc, + choices=choices, + widget=forms.RadioSelect, + required=True, + ) - preferred_words = {bus: [word for word in WORDS - if informations[bus].scores[word] >= average_score] - for bus in buses} - - # Correction : proposer plusieurs mots différents à chaque étape - n_choices = 4 # Nombre de mots à proposer à chaque étape - all_preferred_words = set() - for bus_words in preferred_words.values(): - all_preferred_words.update(bus_words) - all_preferred_words = list(all_preferred_words) - rng.shuffle(all_preferred_words) - words = all_preferred_words[:n_choices] - self.fields["word"].choices = [(w, w) for w in words] + def clean_words(self): + data = self.cleaned_data['words'] + if len(data) != NB_WORDS: + raise forms.ValidationError(_(f"Please choose exactly {NB_WORDS} words")) + return data class WEIBusInformation2025(WEIBusInformation): @@ -86,7 +226,7 @@ class WEIBusInformation2025(WEIBusInformation): def __init__(self, bus): self.scores = {} - for word in WORDS: + for word in WORDS['list']: self.scores[word] = 0 super().__init__(bus) @@ -108,7 +248,7 @@ class BusInformationForm2025(forms.ModelForm): except (json.JSONDecodeError, TypeError, AttributeError): initial_scores = {} if words is None: - words = WORDS + words = WORDS['list'] self.words = words choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')] @@ -145,10 +285,26 @@ class WEISurveyInformation2025(WEISurveyInformation): step = 0 def __init__(self, registration): - for i in range(1, 21): + for i in range(1, 5): setattr(self, "word" + str(i), None) + for q in WORDS['questions']: + setattr(self, q, None) super().__init__(registration) + def reset(self, registration): + """ + Réinitialise complètement le questionnaire : step, seed, mots choisis et réponses aux questions. + """ + self.step = 0 + self.seed = 0 + for i in range(1, 5): + setattr(self, f"word{i}", None) + for q in WORDS['questions']: + setattr(self, q, None) + self.save(registration) + registration._force_save = True + registration.save() + class WEISurvey2025(WEISurvey): """ @@ -174,10 +330,20 @@ class WEISurvey2025(WEISurvey): @transaction.atomic def form_valid(self, form): - word = form.cleaned_data["word"] - self.information.step += 1 - setattr(self.information, "word" + str(self.information.step), word) - self.save() + if self.information.step == 0: + words = form.cleaned_data['words'] + for i, word in enumerate(words, 1): + setattr(self.information, "word" + str(i), word) + self.information.step += 1 + self.save() + else: + questions = list(WORDS['questions'].keys()) + idx = self.information.step - 1 + if idx < len(questions): + q = questions[idx] + setattr(self.information, q, form.cleaned_data[q]) + self.information.step += 1 + self.save() @classmethod def get_algorithm_class(cls): @@ -187,7 +353,7 @@ class WEISurvey2025(WEISurvey): """ The survey is complete once the bus is chosen. """ - return self.information.step == 20 + return self.information.step > len(WORDS['questions']) @classmethod @lru_cache() @@ -206,7 +372,8 @@ class WEISurvey2025(WEISurvey): bus_info = self.get_algorithm_class().get_bus_information(bus) # Score is the given score by the bus subtracted to the mid-score of the buses. s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))] - - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20 + - self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 1 + NB_WORDS)) / NB_WORDS + s += sum(1 for q in WORDS['questions'] if getattr(self.information, q) == str(bus.pk)) return s @lru_cache() @@ -243,6 +410,13 @@ class WEISurveyAlgorithm2025(WEISurveyAlgorithm): def get_bus_information_form(cls): return BusInformationForm2025 + @classmethod + def get_buses(cls): + + if not hasattr(cls, '_buses'): + cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all().exclude(name='Staff') + return cls._buses + def run_algorithm(self, display_tqdm=False): """ Gale-Shapley algorithm implementation. diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 5e3536c2..cd493087 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -71,7 +71,7 @@ class WEIRegistrationTable(tables.Table): 'wei:wei_delete_registration', args=[A('pk')], orderable=False, - verbose_name=_("delete"), + verbose_name=_("Delete"), text=_("Delete"), attrs={ 'th': { @@ -136,8 +136,8 @@ class WEIRegistrationTable(tables.Table): class WEIMembershipTable(tables.Table): user = tables.LinkColumn( - 'wei:wei_update_registration', - args=[A('registration__pk')], + 'wei:wei_update_membership', + args=[A('pk')], ) year = tables.Column( diff --git a/apps/wei/templates/wei/base.html b/apps/wei/templates/wei/base.html index efb619ac..2975efc0 100644 --- a/apps/wei/templates/wei/base.html +++ b/apps/wei/templates/wei/base.html @@ -50,7 +50,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} {% if club.deposit_amount > 0 %} -