mirror of https://gitlab.crans.org/bde/nk20
Merge branch 'wei' into 'beta'
[WEI] Correction de l'algorithme et tests unitaires See merge request bde/nk20!173
This commit is contained in:
commit
7b809ff3a6
|
@ -53,7 +53,8 @@ class WEIBusInformation:
|
||||||
def free_seats(self, surveys: List["WEISurvey"] = None):
|
def free_seats(self, surveys: List["WEISurvey"] = None):
|
||||||
size = self.bus.size
|
size = self.bus.size
|
||||||
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
||||||
valid_surveys = sum(1 for survey in surveys if survey.information.valid) if surveys else 0
|
valid_surveys = sum(1 for survey in surveys if survey.information.valid
|
||||||
|
and survey.information.get_selected_bus() == self.bus) if surveys else 0
|
||||||
return size - already_occupied - valid_surveys
|
return size - already_occupied - valid_surveys
|
||||||
|
|
||||||
def has_free_seats(self, surveys=None):
|
def has_free_seats(self, surveys=None):
|
||||||
|
|
|
@ -16,7 +16,7 @@ WORDS = [
|
||||||
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
|
'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',
|
'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',
|
'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',
|
'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',
|
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -45,9 +45,9 @@ class WEISurveyForm2021(forms.Form):
|
||||||
rng = Random(information.seed)
|
rng = Random(information.seed)
|
||||||
|
|
||||||
words = []
|
words = []
|
||||||
for _ in range(information.step + 1):
|
for _ignored in range(information.step + 1):
|
||||||
# Generate N times words
|
# Generate N times words
|
||||||
words = [rng.choice(WORDS) for _ in range(10)]
|
words = [rng.choice(WORDS) for _ignored2 in range(10)]
|
||||||
words = [(w, w) for w in words]
|
words = [(w, w) for w in words]
|
||||||
if self.data:
|
if self.data:
|
||||||
self.fields["word"].choices = [(w, w) for w in WORDS]
|
self.fields["word"].choices = [(w, w) for w in WORDS]
|
||||||
|
@ -162,7 +162,7 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
||||||
while free_surveys: # Some students are not affected
|
while free_surveys: # Some students are not affected
|
||||||
survey = free_surveys[0]
|
survey = free_surveys[0]
|
||||||
buses = survey.ordered_buses() # Preferences of the student
|
buses = survey.ordered_buses() # Preferences of the student
|
||||||
for bus, _ in buses:
|
for bus, _ignored in buses:
|
||||||
if self.get_bus_information(bus).has_free_seats(surveys):
|
if self.get_bus_information(bus).has_free_seats(surveys):
|
||||||
# Selected bus has free places. Put student in the bus
|
# Selected bus has free places. Put student in the bus
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
|
@ -190,6 +190,9 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
||||||
# If it does not exist, choose the next bus.
|
# If it does not exist, choose the next bus.
|
||||||
least_preferred_survey.free()
|
least_preferred_survey.free()
|
||||||
least_preferred_survey.save()
|
least_preferred_survey.save()
|
||||||
|
free_surveys.append(least_preferred_survey)
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
survey.save()
|
survey.save()
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"User {survey.registration.user} has no free seat")
|
||||||
|
|
|
@ -5,7 +5,7 @@ from argparse import ArgumentParser, FileType
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from wei.forms import CurrentSurvey
|
from ...forms import CurrentSurvey
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Copyright (C) 2018-2021 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.wei2021 import WEIBusInformation2021, WEISurvey2021, WORDS, WEISurveyInformation2021
|
||||||
|
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 2021",
|
||||||
|
email="wei2021@example.com",
|
||||||
|
date_start='2021-09-17',
|
||||||
|
date_end='2021-09-19',
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = WEIBusInformation2021(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 = WEISurveyInformation2021(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
|
||||||
|
WEISurvey2021.get_algorithm_class()().run_algorithm()
|
||||||
|
|
||||||
|
# Ensure that everyone has its first choice
|
||||||
|
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
||||||
|
survey = WEISurvey2021(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 = WEISurveyInformation2021(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
|
||||||
|
WEISurvey2021.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 = WEISurvey2021(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 %
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
from unittest import skip
|
|
||||||
|
|
||||||
from api.tests import TestAPI
|
from api.tests import TestAPI
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -813,10 +812,6 @@ class TestWEISurveyAlgorithm(TestCase):
|
||||||
)
|
)
|
||||||
CurrentSurvey(self.registration).save()
|
CurrentSurvey(self.registration).save()
|
||||||
|
|
||||||
@skip # FIXME Write good unit tests
|
|
||||||
def test_survey_algorithm(self):
|
|
||||||
CurrentSurvey.get_algorithm_class()().run_algorithm()
|
|
||||||
|
|
||||||
|
|
||||||
class TestWeiAPI(TestAPI):
|
class TestWeiAPI(TestAPI):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|
Loading…
Reference in New Issue