Merge branch 'WEI_with_questions' into 'main'

Wei with questions

See merge request bde/nk20!218
This commit is contained in:
bleizi 2023-08-31 12:01:39 +02:00
commit 6bc52be707
4 changed files with 276 additions and 99 deletions

View File

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

View File

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

View File

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

View File

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