diff --git a/apps/wei/forms/surveys/wei2023.py b/apps/wei/forms/surveys/wei2023.py index bf010108..70bd814e 100644 --- a/apps/wei/forms/surveys/wei2023.py +++ b/apps/wei/forms/surveys/wei2023.py @@ -1,90 +1,189 @@ # Copyright (C) 2018-2023 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -import time 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 -WORDS = [ - 'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art', - 'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé', - 'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré', - 'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor', - 'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte', - 'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée', - 'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff', - 'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap', - 'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno', - 'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up', - 'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique' -] +WORDS = { + "ambiance": ["Ambiance de bus :", { + 1: "Ambiance calme et posée", + 2: "Ambiance rigolage entre copaing", + 3: "Ambiance danse de camping autour d'une piscine inexistante", + 4: "Grosse soirée avec de la musique qui fait bouger", + 5: "On retourne le camping et le bus (dans le respect et le savoir vivre)" + }], + "musique": ["Musique :", { + 1: "Musique tranquille", + 2: "Musique commerciale", + 3: "Chansons paillardes", + 4: "Musique de Colonie de vacances", + 5: "Grosse techno" + }], + "boisson": ["Boissons :", { + 1: "Boisson soft", + 2: "Des cocktails de temps en temps", + 3: "Des coktails fancy de pétasse (parce que c'est les meilleurs)", + 4: "Bière !", + 5: "L'alcool c'est dans les céréales" + }], + "beauferie": ["Échelle de la beauferie :", { + 1: "Je suis toujours classe", + 2: "Je rote de temps en temps", + 3: "Claquette chaussette, c'est confortable", + 4: "L'aviron bayonnais est dans ma plyaylist", + 5: "Je suis champion⋅ne de concours de rots et d'éclatage de gobelet sur mon front" + }], + "sommeil": ["Échelle de ton sommeil pendant le WEI :", { + 1: "Dormir, c'est pour les faibles", + 2: "5h maximum", + 3: "10h", + 4: "15h", + 5: "Deux bonnes nuits de sommeil, c'est important pour être en forme pour les activités proposées par nos supers GC WEI" + }], + "vacances": ["Tes vacances de rêve :", { + 1: "Dans ma chambre", + 2: "Retourner chez popa et moman pour pouvoir enfin arrêter de manger des pasta box", + 3: "Être une grosse larve sous le soleil des troopiiiiiiiiques", + 4: "Faire un road trip camping sauvage, manger des racines et boire son pipi", + 5: "Le crime ne prend pas de vacances" + }], + "activite": ["T'as une heure de trou pendant ton WEI, que fais-tu ?", { + 1: "Je cherche des copaines pour faire un petit jeu de société", + 2: "Je cherche un moyen de me dépenser, n'importe quel ballon ferait l'affaire", + 3: "Je cherche un endroit où il y a de la musique pour bouger sur le dancefloor", + 4: "Petit apéro, petite pétanque avec les collègues autour d'un bon pastaga", + 5: "Je cherche une connerie à faire (mais pas trop méchante, pour ne pas embêter mes GC WEI préférés)" + }], + "hygiene": ["Échelle de ton hygiène :", { + 1: "La douche, c'eest tous les jours", + 2: "La règle des 2 jours, c'est un droit et un devoir", + 3: "Je ne me lave qu'après le sport", + 4: "« Ne vous inquiétez pas, je pue pas »", + 5: "Y a que les sales qui se lavent" + }], + "animal": ["Tu décrirais ton animal totem plutôt comme :", { + 1: "Un dragon qui raserait des villes entières d'un seul souffle", + 2: "Une mouette qui pique des frites aux dunkerquois", + 3: "Un poulpe tout meunion", + 4: "Un pitbull qui au fond cache un petit cœur en sucre", + 5: "Un canard en plastique au bord d'une baignoire qui n'a pas servi depuis 10 ans" + }], + "fensfoire": ["Quel est ton rapport à la F[ENS]foire ?", { + 1: "Je réveille les autres à 6h avec mon instrument", + 2: "Je la suis partout", + 3: "J'aime bien l'écouter de temps en temps", + 4: "Je mets des bouchons d'oreilles pour ne pas l'entendre", + 5: "La quoi ?" + }], + "kokarde": ["Qu'est-ce que le mot Kokarde t'évoque ?", { + 1: "Vraiment pas mon truc les soirées…", + 2: "Bof, je viens pour manger et je repars aussitôt", + 3: "Je kiffe, good vibes", + 4: "Perso, je ne m'arrêterai pas de danser sur la piste !", + 5: "J'resterai jusqu'à 3h ou rien" + }], + "copain": ["Qu'est-ce que tu fais avec un⋅e «copain⋅ine» ?", { + 1: "Je l'insulte de sale merde", + 2: "J'lui fais faire des trucs cons et je l'affiche !", + 3: "On parlerait ensemble et on se marrerait", + 4: "On aurait des vrais gros délires", + 5: "Je meurs pour lui/elle" + }], + "vie": ["Selon toi, qu'est-ce que la vie ?", { + 1: "La vie, cette sale race !", + 2: "Un moment paisible avant la mort", + 3: "C'est difficile à définir...", + 4: "En vrai, c'est cool !", + 5: "Une gigantestque tranche de kiff ! Et tous les jours, j'en mange un morceau" + }], + "jeux": ["Quel est ton rapport avec les jeux de société ?", { + 1: "éloigné", + 2: "nonchalant", + 3: "timide", + 4: "assumé", + 5: "sexuel" + }], + "calin": ["Qu'est-ce que tu penses des câlins ?", { + 1: "Jamais je n'en fais et jamais je n'en ferai !", + 2: "J'en fais mais ça ne me plaît pas", + 3: "J'en fais rarement mais c'est toujours cool", + 4: "J'en fais tous les jours avec mes ami⋅es !", + 5: "Je pourrais en faire à n'importe qui. Pourquoi ne pas créer le club Câl[ENS] ?" + }], + "vomi": ["Quel est ton rapport au vomi ?", { + 1: "C'est compliqué…", + 2: "Jamais je ne vomis mais je nettoie quand mes potes vomissent", + 3: "Jamais je ne vomis et jamais je ne nettoie celui de quelqu'un d'autre", + 4: "Je vomis quelquefois, ça arrive, faites pas cette tête, mais je fins toujours par nettoyer !", + 5: "Je vomis à chaque soirée et ce n'est jamais moi qui nettoie" + }], + "kfet": ["Qu'est ce que la Kfet t'évoque ?", { + 1: "La Kfet, quel lieu de dépravé⋅es sérieux…", + 2: "C'est un endroit à l'hygiène plus que douteuse…", + 3: "Téma les prix des boissons et des snacks, c'est aberrant !", + 4: "En vrai, c'est cool, petit billard, petit canapé, chill !", + 5: "Banger, j'y reste jusqu'à la fin de mes jours" + }], + "fatigue": ["Comment combattre la fatigue lors de ton WEI ?", { + 1: "Le sport en journée, ça réveille", + 2: "Le sucre du coca, ça réveille", + 3: "La taurine du Red Bull, ça réveille", + 4: "L'alcool dans le sang, ça réveille", + 5: "L'écocup sur le front, ça réveille" + }], + "duree trajet": ["Quelle serait ta durée de trajet préférée ?", { + 1: "Trajet instantané, pas le temps de niaiser", + 2: "1h, histoire de faire connaissance avec quelques personnes avant de se jeter sur les boissons", + 3: "3h, on peut vraiment parler et apprendre à connaître nos voisin⋅es", + 4: "6h, histoire d'avoir le temps de faire des conneries dans le bus pour bien se marrer !", + 5: "12h, il faut bien trouver un moment pour dormir, ce seront deux gros dodos dans un bus" + }], + "scolarite": ["Comment tu vois ton cursus à l'ENS ?", { + 1: "La tranquillité et le travail", + 2: "On va s'amuser tout en bossant", + 3: "Ça va profiter et réviser au dernier moment pour les exams…", + 4: "Nous festoierons sans songer aux conséquences", + 5: "Je ne vois qu'une seule issue : la débauche" + }] +} class WEISurveyForm2023(forms.Form): """ Survey form for the year 2023. - Members choose 20 words, from which we calculate the best associated bus. + Members answer 20 questions, 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 = WEISurveyInformation2023(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) - - words = None - - buses = WEISurveyAlgorithm2023.get_buses() - informations = {bus: WEIBusInformation2023(bus) for bus in buses} - scores = sum((list(informations[bus].scores.values()) for bus in buses), []) - average_score = sum(scores) / len(scores) - - preferred_words = {bus: [word for word in WORDS - if informations[bus].scores[word] >= average_score] - for bus in buses} - while words is None or len(set(words)) != len(words): - # Ensure that there is no the same word 2 times - words = [rng.choice(words) for _ignored2, words in preferred_words.items()] - rng.shuffle(words) - words = [(w, w) for w in words] - self.fields["word"].choices = words + question = information.questions[information.step] + self.fields[question] = forms.ChoiceField( + label=WORDS[question][0], + widget=forms.RadioSelect(), + ) + answers = [(answer, WORDS[question][1][answer]) for answer in WORDS[question][1]] + self.fields[question].choices = answers class WEIBusInformation2023(WEIBusInformation): """ - For each word, the bus has a score + For each question, the bus has ordered answers """ scores: dict def __init__(self, bus): self.scores = {} - for word in WORDS: - self.scores[word] = 0.0 + for question in WORDS: + self.scores[question] = [] super().__init__(bus) @@ -93,13 +192,13 @@ class WEISurveyInformation2023(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 + questions = list(WORDS.keys()) def __init__(self, registration): - for i in range(1, 21): - setattr(self, "word" + str(i), None) + for question in WORDS: + setattr(self, str(question), None) super().__init__(registration) @@ -127,9 +226,11 @@ class WEISurvey2023(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) + for question in WORDS: + if question in form.cleaned_data: + answer = form.cleaned_data[question] + setattr(self.information, question, answer) self.save() @classmethod @@ -140,16 +241,10 @@ class WEISurvey2023(WEISurvey): """ 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() + for question in WORDS: + if not getattr(self.information, question): + return False + return True @lru_cache() def score(self, bus): @@ -158,8 +253,9 @@ class WEISurvey2023(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 + s = 0 + for question in WORDS: + s += bus_info.scores[question][str(getattr(self.information, question))] return s @lru_cache() @@ -174,7 +270,6 @@ class WEISurvey2023(WEISurvey): @classmethod def clear_cache(cls): - cls.word_mean.cache_clear() return super().clear_cache() diff --git a/apps/wei/tests/test_wei_algorithm_2023.py b/apps/wei/tests/test_wei_algorithm_2023.py index d1b8a536..714024e2 100644 --- a/apps/wei/tests/test_wei_algorithm_2023.py +++ b/apps/wei/tests/test_wei_algorithm_2023.py @@ -2,9 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later import random +from datetime import date 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.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023 from ..models import Bus, WEIClub, WEIRegistration @@ -20,9 +23,25 @@ class TestWEIAlgorithm(TestCase): """ Create some test data, with one WEI and 10 buses with random score attributions. """ + self.user = User.objects.create_superuser( + username="weiadmin", + password="admin", + email="admin@example.com", + ) + self.user.save() + self.client.force_login(self.user) + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + self.wei = WEIClub.objects.create( name="WEI 2023", email="wei2023@example.com", + parent_club_id=2, + membership_fee_paid=12500, + membership_fee_unpaid=5500, + membership_start='2023-08-26', + membership_end='2023-09-15', date_start='2023-09-16', date_end='2023-09-18', year=2023, @@ -33,8 +52,8 @@ class TestWEIAlgorithm(TestCase): bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10) self.buses.append(bus) information = WEIBusInformation2023(bus) - for word in WORDS: - information.scores[word] = random.randint(0, 101) + for question in WORDS: + information.scores[question] = {answer: random.randint(1, 5) for answer in WORDS[question][1]} information.save() bus.save() @@ -52,8 +71,8 @@ class TestWEIAlgorithm(TestCase): birth_date='2000-01-01', ) information = WEISurveyInformation2023(registration) - for j in range(1, 21): - setattr(information, f'word{j}', random.choice(WORDS)) + for question in WORDS: + setattr(information, question, random.randint(1, 5)) information.step = 20 information.save(registration) registration.save() @@ -82,8 +101,8 @@ class TestWEIAlgorithm(TestCase): birth_date='2000-01-01', ) information = WEISurveyInformation2023(registration) - for j in range(1, 21): - setattr(information, f'word{j}', random.choice(WORDS)) + for question in WORDS: + setattr(information, question, random.randint(1, 5)) information.step = 20 information.save(registration) registration.save() @@ -108,3 +127,44 @@ 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, 20 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 = WEISurvey2023(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 = WEISurvey2023(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_registration.py b/apps/wei/tests/test_wei_registration.py index eb43a296..86dd4cfd 100644 --- a/apps/wei/tests/test_wei_registration.py +++ b/apps/wei/tests/test_wei_registration.py @@ -380,7 +380,7 @@ class TestWEIRegistration(TestCase): def test_register_1a(self): """ - Test register a first year member to the WEI and complete the survey. + Test register a first year member to the WEI. """ response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk))) self.assertEqual(response.status_code, 200) @@ -402,21 +402,6 @@ class TestWEIRegistration(TestCase): self.assertTrue(qs.exists()) registration = qs.get() self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200) - for i in range(1, 21): - # Fill 1A Survey, 20 pages - response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), dict( - word="Jus de fruit", - )) - registration.refresh_from_db() - survey = CurrentSurvey(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, "word" + str(i)), "Survey page #" + str(i) + " failed") - survey = CurrentSurvey(registration) - self.assertTrue(survey.is_complete()) - survey.select_bus(self.bus) - survey.save() - self.assertIsNotNone(survey.information.get_selected_bus()) # Check that the user can't be registered twice response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict( @@ -662,7 +647,7 @@ class TestWEIRegistration(TestCase): first_name="admin", bank="Société générale", )) - self.assertRedirects(response, reverse("wei:wei_detail", 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) # Check if the membership is successfully created membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei) self.assertTrue(membership.exists()) diff --git a/apps/wei/views.py b/apps/wei/views.py index 80ff770e..4f2b7b65 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -969,7 +969,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): if not registration.soge_credit and user.note.balance + credit_amount < fee: # Users must have money before registering to the WEI. - form.add_error('bus', + form.add_error('credit_type', _("This user don't have enough money to join this club, and can't have a negative balance.")) return super().form_invalid(form) @@ -1014,7 +1014,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): def get_success_url(self): self.object.refresh_from_db() - return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk}) + return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk}) class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): @@ -1084,7 +1084,44 @@ class WEISurveyEndView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei + club = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei + context["club"] = club + + random_user = User.objects.filter(~Q(wei__wei__in=[club])).first() + + if random_user is None: + # This case occurs when all users are registered to the WEI. + # Don't worry, Pikachu never went to the WEI. + # This bug can arrive only in dev mode. + context["can_add_first_year_member"] = True + context["can_add_any_member"] = True + else: + # Check if the user has the right to create a registration of a random first year member. + empty_fy_registration = WEIRegistration( + wei=club, + user=random_user, + first_year=True, + birth_date="1970-01-01", + gender="No", + emergency_contact_name="No", + emergency_contact_phone="No", + ) + context["can_add_first_year_member"] = PermissionBackend \ + .check_perm(self.request, "wei.add_weiregistration", empty_fy_registration) + + # Check if the user has the right to create a registration of a random old member. + empty_old_registration = WEIRegistration( + wei=club, + user=User.objects.filter(~Q(wei__wei__in=[club])).first(), + first_year=False, + birth_date="1970-01-01", + gender="No", + emergency_contact_name="No", + emergency_contact_phone="No", + ) + context["can_add_any_member"] = PermissionBackend \ + .check_perm(self.request, "wei.add_weiregistration", empty_old_registration) + return context