mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 07:49:57 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			d3e832d23a
			...
			migration-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bb9f58e497 | 
| @@ -7,25 +7,10 @@ stages: | |||||||
| variables: | variables: | ||||||
|   GIT_SUBMODULE_STRATEGY: recursive |   GIT_SUBMODULE_STRATEGY: recursive | ||||||
|  |  | ||||||
| # Debian Buster | # Ubuntu 22.04 | ||||||
| #  py37-django22: | py310-django50: | ||||||
| #   stage: test |  | ||||||
| #   image: debian:buster-backports |  | ||||||
| #   before_script: |  | ||||||
| #     - > |  | ||||||
| #         apt-get update && |  | ||||||
| #         apt-get install --no-install-recommends -t buster-backports -y |  | ||||||
| #         python3-django python3-django-crispy-forms |  | ||||||
| #         python3-django-extensions python3-django-filters python3-django-polymorphic |  | ||||||
| #         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil |  | ||||||
| #         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache |  | ||||||
| #         python3-bs4 python3-setuptools tox texlive-xetex |  | ||||||
| #   script: tox -e py37-django22 |  | ||||||
|  |  | ||||||
| # Ubuntu 20.04 |  | ||||||
| py38-django22: |  | ||||||
|   stage: test |   stage: test | ||||||
|   image: ubuntu:20.04 |   image: ubuntu:22.04 | ||||||
|   before_script: |   before_script: | ||||||
|     # Fix tzdata prompt |     # Fix tzdata prompt | ||||||
|     - ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone |     - ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone | ||||||
| @@ -37,12 +22,12 @@ py38-django22: | |||||||
|         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil |         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil | ||||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache |         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache | ||||||
|         python3-bs4 python3-setuptools tox texlive-xetex |         python3-bs4 python3-setuptools tox texlive-xetex | ||||||
|   script: tox -e py38-django22 |   script: tox -e py310-django50 | ||||||
|  |  | ||||||
| # Debian Bullseye | # Debian Bookworm | ||||||
| py39-django22: | py311-django50: | ||||||
|   stage: test |   stage: test | ||||||
|   image: debian:bullseye |   image: debian:bookworm | ||||||
|   before_script: |   before_script: | ||||||
|     - > |     - > | ||||||
|         apt-get update && |         apt-get update && | ||||||
| @@ -52,7 +37,7 @@ py39-django22: | |||||||
|         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil |         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil | ||||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache |         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache | ||||||
|         python3-bs4 python3-setuptools tox texlive-xetex |         python3-bs4 python3-setuptools tox texlive-xetex | ||||||
|   script: tox -e py39-django22 |   script: tox -e py311-django50 | ||||||
|  |  | ||||||
| linters: | linters: | ||||||
|   stage: quality-assurance |   stage: quality-assurance | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from bootstrap_datepicker_plus.widgets import DateTimePickerInput | ||||||
|  |  | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from random import shuffle | from random import shuffle | ||||||
|  |  | ||||||
| @@ -10,7 +12,7 @@ from django.utils import timezone | |||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from member.models import Club | from member.models import Club | ||||||
| from note.models import Note, NoteUser | from note.models import Note, NoteUser | ||||||
| from note_kfet.inputs import Autocomplete, DateTimePickerInput | from note_kfet.inputs import Autocomplete | ||||||
| from note_kfet.middlewares import get_current_request | from note_kfet.middlewares import get_current_request | ||||||
| from permission.backends import PermissionBackend | from permission.backends import PermissionBackend | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.conf.urls import url, include | from django.urls import include, re_path | ||||||
| from rest_framework import routers | from rest_framework import routers | ||||||
|  |  | ||||||
| from .views import UserInformationView | from .views import UserInformationView | ||||||
| @@ -47,7 +47,7 @@ app_name = 'api' | |||||||
| # Wire up our API using automatic URL routing. | # Wire up our API using automatic URL routing. | ||||||
| # Additionally, we include login URLs for the browsable API. | # Additionally, we include login URLs for the browsable API. | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     url('^', include(router.urls)), |     re_path('^', include(router.urls)), | ||||||
|     url('^me/', UserInformationView.as_view()), |     re_path('^me/', UserInformationView.as_view()), | ||||||
|     url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), |     re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
|  |  | ||||||
| import io | import io | ||||||
|  |  | ||||||
|  | from bootstrap_datepicker_plus.widgets import DatePickerInput | ||||||
| from PIL import Image, ImageSequence | from PIL import Image, ImageSequence | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @@ -13,7 +14,7 @@ from django.forms import CheckboxSelectMultiple | |||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from note.models import NoteSpecial, Alias | from note.models import NoteSpecial, Alias | ||||||
| from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput | from note_kfet.inputs import Autocomplete, AmountInput | ||||||
| from permission.models import PermissionMask, Role | from permission.models import PermissionMask, Role | ||||||
|  |  | ||||||
| from .models import Profile, Club, Membership | from .models import Profile, Club, Membership | ||||||
| @@ -32,7 +33,7 @@ class UserForm(forms.ModelForm): | |||||||
|         # Django usernames can only contain letters, numbers, @, ., +, - and _. |         # Django usernames can only contain letters, numbers, @, ., +, - and _. | ||||||
|         # We want to allow users to have uncommon and unpractical usernames: |         # We want to allow users to have uncommon and unpractical usernames: | ||||||
|         # That is their problem, and we have normalized aliases for us. |         # That is their problem, and we have normalized aliases for us. | ||||||
|         return super()._get_validation_exclusions() + ["username"] |         return super()._get_validation_exclusions() | {"username"} | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = User |         model = User | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ class TemplateLoggedInTests(TestCase): | |||||||
|         self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302) |         self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302) | ||||||
|  |  | ||||||
|     def test_logout(self): |     def test_logout(self): | ||||||
|         response = self.client.get(reverse("logout")) |         response = self.client.post(reverse("logout")) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|     def test_admin_index(self): |     def test_admin_index(self): | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ def register_note_urls(router, path): | |||||||
|     router.register(path + '/note', NotePolymorphicViewSet) |     router.register(path + '/note', NotePolymorphicViewSet) | ||||||
|     router.register(path + '/alias', AliasViewSet) |     router.register(path + '/alias', AliasViewSet) | ||||||
|     router.register(path + '/trust', TrustViewSet) |     router.register(path + '/trust', TrustViewSet) | ||||||
|     router.register(path + '/consumer', ConsumerViewSet) |     router.register(path + '/consumer', ConsumerViewSet, basename="consumer") | ||||||
|  |  | ||||||
|     router.register(path + '/transaction/category', TemplateCategoryViewSet) |     router.register(path + '/transaction/category', TemplateCategoryViewSet) | ||||||
|     router.register(path + '/transaction/transaction', TransactionViewSet) |     router.register(path + '/transaction/transaction', TransactionViewSet) | ||||||
|   | |||||||
| @@ -179,19 +179,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): | |||||||
|             # We match first an alias if it is matched without normalization, |             # We match first an alias if it is matched without normalization, | ||||||
|             # then if the normalized pattern matches a normalized alias. |             # then if the normalized pattern matches a normalized alias. | ||||||
|             queryset = queryset.filter( |             queryset = queryset.filter( | ||||||
|                 **{f'name{suffix}': alias_prefix + alias} |                 Q(**{f'name{suffix}': alias_prefix + alias}) | ||||||
|             ).union( |                 | Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) | ||||||
|                 queryset.filter( |                 | Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()}) | ||||||
|                     Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) |             ) | ||||||
|                     & ~Q(**{f'name{suffix}': alias_prefix + alias}) |  | ||||||
|                 ), |  | ||||||
|                 all=True).union( |  | ||||||
|                 queryset.filter( |  | ||||||
|                     Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()}) |  | ||||||
|                     & ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) |  | ||||||
|                     & ~Q(**{f'name{suffix}': alias_prefix + alias}) |  | ||||||
|                 ), |  | ||||||
|                 all=True) |  | ||||||
|  |  | ||||||
|         queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ |         queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ | ||||||
|             else queryset.order_by("name") |             else queryset.order_by("name") | ||||||
|   | |||||||
| @@ -1,13 +1,15 @@ | |||||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
|  | from bootstrap_datepicker_plus.widgets import DateTimePickerInput | ||||||
| from django import forms | from django import forms | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.forms import CheckboxSelectMultiple | from django.forms import CheckboxSelectMultiple | ||||||
| from django.utils.timezone import make_aware | from django.utils.timezone import make_aware | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput | from note_kfet.inputs import Autocomplete, AmountInput | ||||||
|  |  | ||||||
| from .models import TransactionTemplate, NoteClub, Alias | from .models import TransactionTemplate, NoteClub, Alias | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ def create_special_notes(apps, schema_editor): | |||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|     dependencies = [ |     dependencies = [ | ||||||
|         ('note', '0001_initial'), |         ('note', '0001_initial'), | ||||||
|  |         ('logs', '0001_initial'), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
|   | |||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | # Generated by Django 5.0.7 on 2024-07-11 09:24 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('contenttypes', '0002_remove_content_type_name'), | ||||||
|  |         ('note', '0006_trust'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='note', | ||||||
|  |             name='polymorphic_ctype', | ||||||
|  |             field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='transaction', | ||||||
|  |             name='polymorphic_ctype', | ||||||
|  |             field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|            name="{{ widget.name }}" |            name="{{ widget.name }}" | ||||||
|            {# Other attributes are loaded  #} |            {# Other attributes are loaded  #} | ||||||
|            {% for name, value in widget.attrs.items %} |            {% for name, value in widget.attrs.items %} | ||||||
|                 {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %} |                 {% if value != False %}{{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %} | ||||||
|             {% endfor %}> |             {% endfor %}> | ||||||
|     <div class="input-group-append"> |     <div class="input-group-append"> | ||||||
|         <span class="input-group-text">€</span> |         <span class="input-group-text">€</span> | ||||||
|   | |||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | # Generated by Django 5.0.7 on 2024-07-11 09:24 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('note', '0007_alter_note_polymorphic_ctype_and_more'), | ||||||
|  |         ('treasury', '0008_auto_20240322_0045'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='sogecredit', | ||||||
|  |             name='transactions', | ||||||
|  |             field=models.ManyToManyField(blank=True, related_name='+', to='note.membershiptransaction', verbose_name='membership transactions'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -1,13 +1,14 @@ | |||||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from bootstrap_datepicker_plus.widgets import DatePickerInput | ||||||
| from django import forms | from django import forms | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.forms import CheckboxSelectMultiple | from django.forms import CheckboxSelectMultiple | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from note.models import NoteSpecial, NoteUser | from note.models import NoteSpecial, NoteUser | ||||||
| from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget | from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget | ||||||
|  |  | ||||||
| from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole | from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,11 +2,11 @@ | |||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm | from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm | ||||||
| from .wei2024 import WEISurvey2024 | from .wei2023 import WEISurvey2023 | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', |     'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', | ||||||
| ] | ] | ||||||
|  |  | ||||||
| CurrentSurvey = WEISurvey2024 | CurrentSurvey = WEISurvey2023 | ||||||
|   | |||||||
| @@ -1,337 +0,0 @@ | |||||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay |  | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later |  | ||||||
|  |  | ||||||
| from functools import lru_cache |  | ||||||
|  |  | ||||||
| from django import forms |  | ||||||
| from django.db import transaction |  | ||||||
| from django.db.models import Q |  | ||||||
|  |  | ||||||
| from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation |  | ||||||
| from ...models import WEIMembership |  | ||||||
|  |  | ||||||
|  |  | ||||||
| buses_descr = [ |  | ||||||
|     [ |  | ||||||
|         "Magi[Kar]p", "#ef5568", |  | ||||||
|         """bus faible en alcool mais fort en connerie avec une partie calme pour les amateurs de sieste et de jeux de société. |  | ||||||
|         Non discriminant il accepte tout le monde y compris le plus nulle des pokémons (magicarpe !!!!!!). Malgré les |  | ||||||
|         accusations mensongères, il n'y a aucun weeb dans le Magi[Kar]p""", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "Va[car]me", "#fd7a28", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "[Kar]aïbes", "#a5cfdd", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "[Kar]di [Bus]", "#e46398", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "Sparta[bus] 🐺🐒🏉", "#ebdac2", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "Zanzo[Bus]", "#FFFF", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "Bran[Kar]", "#6da1ac", |  | ||||||
|         """Si vous ne connaissez pas le Bran[Kar], c’est comme une grande famille qui fait un apéro, qui se bourre un peu la |  | ||||||
|         gueule en discutant des heures autour d’une table remplie de bouffe et de super bons cocktails (la plupart des |  | ||||||
|         barmen/barwomen du bus sont les barmans de Shakens), sauf qu’on est un bus du Wei (vous comprendrez bien le nom de notre |  | ||||||
|         bus en voyant l’état de certain·e·s). Il nous arrive de faire quelques conneries, mais surtout de jouer au Bière-pong en |  | ||||||
|         musique !""", |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "Techno [kar]ade", "#8065a3", |  | ||||||
|         "descr" |  | ||||||
|     ], |  | ||||||
|     [ |  | ||||||
|         "[Bus]ka-P", "#7c4768", |  | ||||||
|         "descr", |  | ||||||
|     ], |  | ||||||
| ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_survey_info(id): |  | ||||||
|     s = {"recap": { |  | ||||||
|         "1": 0, |  | ||||||
|         "2": 0, |  | ||||||
|         "3": 0, |  | ||||||
|         "4": 0, |  | ||||||
|         "5": 0, |  | ||||||
|     }} |  | ||||||
|     s_ = {f"bus{id}": {f"{i}": 0 for i in range(1, 5 + 1)} for id in range(len(buses_descr))} |  | ||||||
|     s.update(s_) |  | ||||||
|     s.update({f"bus{id}": {f"{i}": i for i in range(1, 5 + 1)}}) |  | ||||||
|     return {"scores": s} |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def print_bus(id): |  | ||||||
|     return buses_descr[id][0] + "\n\n" + buses_descr[id][2] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def print_all_buses(): |  | ||||||
|     liste = [print_bus(id) for id in range(len(buses_descr))] |  | ||||||
|     return "<br><br>---------<br><br>".join(liste) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| WORDS = { |  | ||||||
|     "recap": |  | ||||||
|         [ |  | ||||||
|             """Chèr⋅e 1A, te voilà arrivé⋅e devant un choix fatidique, le choix de ton bus.......<br> |  | ||||||
|             (Musique effrayante)<br> |  | ||||||
|             Petite blagounette évidemment, chacun des bus te permettra de passer un excellent WEI ! |  | ||||||
|             Mais quitte à avoir le choix, voici la liste de tous les bus ainsi qu'une description détaillée de ces derniers ! |  | ||||||
|             Prends ton temps, observe les bien et quand tu te sens prêt⋅e, appuye sur le bouton 'Noter les bus' pour continuer |  | ||||||
|             (pas besoin d'apprendre par cœur les bus, la description du bus te sera rappeler avant de le noter !) <br><br><br>""" + print_all_buses(), |  | ||||||
|             { |  | ||||||
|                 1: "Noter les bus :", |  | ||||||
|             } |  | ||||||
|         ] |  | ||||||
| } |  | ||||||
|  |  | ||||||
| WORDS.update({ |  | ||||||
|     f"bus{id}": [print_bus(id), {i: f"Noter {i}/5" for i in range(1, 5 + 1)}] for id in range(len(buses_descr)) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WEISurveyForm2024(forms.Form): |  | ||||||
|     """ |  | ||||||
|     Survey form for the year 2024. |  | ||||||
|     Members answer 20 questions, from which we calculate the best associated bus. |  | ||||||
|     """ |  | ||||||
|     def set_registration(self, registration): |  | ||||||
|         """ |  | ||||||
|         Filter the bus selector with the buses of the current WEI. |  | ||||||
|         """ |  | ||||||
|         information = WEISurveyInformation2024(registration) |  | ||||||
|  |  | ||||||
|         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 WEIBusInformation2024(WEIBusInformation): |  | ||||||
|     """ |  | ||||||
|     For each question, the bus has ordered answers |  | ||||||
|     """ |  | ||||||
|     scores: dict |  | ||||||
|  |  | ||||||
|     def __init__(self, bus): |  | ||||||
|         self.scores = {} |  | ||||||
|         for question in WORDS: |  | ||||||
|             self.scores[question] = [] |  | ||||||
|         super().__init__(bus) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WEISurveyInformation2024(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. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     step = 0 |  | ||||||
|     questions = list(WORDS.keys()) |  | ||||||
|  |  | ||||||
|     def __init__(self, registration): |  | ||||||
|         for question in WORDS: |  | ||||||
|             setattr(self, str(question), None) |  | ||||||
|         super().__init__(registration) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WEISurvey2024(WEISurvey): |  | ||||||
|     """ |  | ||||||
|     Survey for the year 2024. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_year(cls): |  | ||||||
|         return 2024 |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_survey_information_class(cls): |  | ||||||
|         return WEISurveyInformation2024 |  | ||||||
|  |  | ||||||
|     def get_form_class(self): |  | ||||||
|         return WEISurveyForm2024 |  | ||||||
|  |  | ||||||
|     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): |  | ||||||
|         self.information.step += 1 |  | ||||||
|         for question in WORDS: |  | ||||||
|             if question in form.cleaned_data: |  | ||||||
|                 answer = form.cleaned_data[question] |  | ||||||
|                 setattr(self.information, question, answer) |  | ||||||
|         self.save() |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_algorithm_class(cls): |  | ||||||
|         return WEISurveyAlgorithm2024 |  | ||||||
|  |  | ||||||
|     def is_complete(self) -> bool: |  | ||||||
|         """ |  | ||||||
|         The survey is complete once the bus is chosen. |  | ||||||
|         """ |  | ||||||
|         for question in WORDS: |  | ||||||
|             if not getattr(self.information, question): |  | ||||||
|                 return False |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     @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 = 0 |  | ||||||
|         for question in WORDS: |  | ||||||
|             s += bus_info.scores[question][str(getattr(self.information, question))] |  | ||||||
|         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): |  | ||||||
|         return super().clear_cache() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WEISurveyAlgorithm2024(WEISurveyAlgorithm): |  | ||||||
|     """ |  | ||||||
|     The algorithm class for the year 2024. |  | ||||||
|     We use Gale-Shapley algorithm to attribute 1y students into buses. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_survey_class(cls): |  | ||||||
|         return WEISurvey2024 |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_bus_information_class(cls): |  | ||||||
|         return WEIBusInformation2024 |  | ||||||
|  |  | ||||||
|     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 |  | ||||||
|         WEISurvey2024.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() |  | ||||||
| @@ -6,6 +6,8 @@ from datetime import date, timedelta | |||||||
|  |  | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.test import TestCase | 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 ..forms.surveys.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023 | ||||||
| from ..models import Bus, WEIClub, WEIRegistration | from ..models import Bus, WEIClub, WEIRegistration | ||||||
| @@ -125,3 +127,44 @@ class TestWEIAlgorithm(TestCase): | |||||||
|             self.assertLessEqual(max_score - score, 25)  # Always less than 25 % of tolerance |             self.assertLessEqual(max_score - score, 25)  # Always less than 25 % of tolerance | ||||||
|  |  | ||||||
|         self.assertLessEqual(penalty / 100, 25)  # Tolerance of 5 % |         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()) | ||||||
|   | |||||||
| @@ -1,172 +0,0 @@ | |||||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay |  | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later |  | ||||||
|  |  | ||||||
| import random |  | ||||||
| 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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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.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 2024", |  | ||||||
|             email="wei2024@example.com", |  | ||||||
|             parent_club_id=2, |  | ||||||
|             membership_fee_paid=12500, |  | ||||||
|             membership_fee_unpaid=5500, |  | ||||||
|             membership_start='2024-01-01', |  | ||||||
|             membership_end='2024-12-31', |  | ||||||
|             date_start=date.today() + timedelta(days=2), |  | ||||||
|             date_end='2024-12-31', |  | ||||||
|             year=2024, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         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 = WEIBusInformation2024(bus) |  | ||||||
|             for question in WORDS: |  | ||||||
|                 information.scores[question] = {answer: random.randint(1, 5) for answer in WORDS[question][1]} |  | ||||||
|             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 = WEISurveyInformation2024(registration) |  | ||||||
|             for question in WORDS: |  | ||||||
|                 options = list(WORDS[question][1].keys()) |  | ||||||
|                 setattr(information, question, random.choice(options)) |  | ||||||
|             information.step = 20 |  | ||||||
|             information.save(registration) |  | ||||||
|             registration.save() |  | ||||||
|  |  | ||||||
|         # Run algorithm |  | ||||||
|         WEISurvey2024.get_algorithm_class()().run_algorithm() |  | ||||||
|  |  | ||||||
|         # Ensure that everyone has its first choice |  | ||||||
|         for r in WEIRegistration.objects.filter(wei=self.wei).all(): |  | ||||||
|             survey = WEISurvey2024(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 = WEISurveyInformation2024(registration) |  | ||||||
|             for question in WORDS: |  | ||||||
|                 options = list(WORDS[question][1].keys()) |  | ||||||
|                 setattr(information, question, random.choice(options)) |  | ||||||
|             information.step = 20 |  | ||||||
|             information.save(registration) |  | ||||||
|             registration.save() |  | ||||||
|  |  | ||||||
|         # Run algorithm |  | ||||||
|         WEISurvey2024.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 = WEISurvey2024(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 % |  | ||||||
|  |  | ||||||
|     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 = 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()) |  | ||||||
| @@ -439,7 +439,7 @@ class TestWEIRegistration(TestCase): | |||||||
|             emergency_contact_phone='+33123456789', |             emergency_contact_phone='+33123456789', | ||||||
|         )) |         )) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI." |         self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI." | ||||||
|                         in str(response.context["form"].errors)) |                         in str(response.context["form"].errors)) | ||||||
|  |  | ||||||
|         # Check that if the WEI is started, we can't register anyone |         # Check that if the WEI is started, we can't register anyone | ||||||
| @@ -635,7 +635,7 @@ class TestWEIRegistration(TestCase): | |||||||
|         )) |         )) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertFalse(response.context["form"].is_valid()) |         self.assertFalse(response.context["form"].is_valid()) | ||||||
|         self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors)) |         self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors)) | ||||||
|  |  | ||||||
|         response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( |         response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( | ||||||
|             roles=[WEIRole.objects.get(name="GC WEI").id], |             roles=[WEIRole.objects.get(name="GC WEI").id], | ||||||
| @@ -767,7 +767,7 @@ class TestDefaultWEISurvey(TestCase): | |||||||
|         WEISurvey.update_form(None, None) |         WEISurvey.update_form(None, None) | ||||||
|  |  | ||||||
|         self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) |         self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) | ||||||
|         self.assertEqual(CurrentSurvey.get_year(), 2024) |         self.assertEqual(CurrentSurvey.get_year(), 2023) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestWeiAPI(TestAPI): | class TestWeiAPI(TestAPI): | ||||||
|   | |||||||
| @@ -25,19 +25,13 @@ admin_site.register(Site, SiteAdmin) | |||||||
|  |  | ||||||
| # Add external apps model | # Add external apps model | ||||||
| if "oauth2_provider" in settings.INSTALLED_APPS: | if "oauth2_provider" in settings.INSTALLED_APPS: | ||||||
|     from oauth2_provider.admin import Application, ApplicationAdmin, Grant, \ |     from oauth2_provider.admin import ApplicationAdmin, GrantAdmin, AccessTokenAdmin, RefreshTokenAdmin | ||||||
|         GrantAdmin, AccessToken, AccessTokenAdmin, RefreshToken, RefreshTokenAdmin |     from oauth2_provider.models import Application, Grant, AccessToken, RefreshToken | ||||||
|     admin_site.register(Application, ApplicationAdmin) |     admin_site.register(Application, ApplicationAdmin) | ||||||
|     admin_site.register(Grant, GrantAdmin) |     admin_site.register(Grant, GrantAdmin) | ||||||
|     admin_site.register(AccessToken, AccessTokenAdmin) |     admin_site.register(AccessToken, AccessTokenAdmin) | ||||||
|     admin_site.register(RefreshToken, RefreshTokenAdmin) |     admin_site.register(RefreshToken, RefreshTokenAdmin) | ||||||
|  |  | ||||||
| if "django_htcpcp_tea" in settings.INSTALLED_APPS: |  | ||||||
|     from django_htcpcp_tea.admin import * |  | ||||||
|     from django_htcpcp_tea.models import * |  | ||||||
|     admin_site.register(Pot, PotAdmin) |  | ||||||
|     admin_site.register(TeaType, TeaTypeAdmin) |  | ||||||
|     admin_site.register(Addition, AdditionAdmin) |  | ||||||
|  |  | ||||||
| if "mailer" in settings.INSTALLED_APPS: | if "mailer" in settings.INSTALLED_APPS: | ||||||
|     from mailer.admin import * |     from mailer.admin import * | ||||||
| @@ -50,9 +44,3 @@ if "rest_framework" in settings.INSTALLED_APPS: | |||||||
|     from rest_framework.authtoken.admin import * |     from rest_framework.authtoken.admin import * | ||||||
|     from rest_framework.authtoken.models import * |     from rest_framework.authtoken.models import * | ||||||
|     admin_site.register(Token, TokenAdmin) |     admin_site.register(Token, TokenAdmin) | ||||||
|  |  | ||||||
| if "cas_server" in settings.INSTALLED_APPS: |  | ||||||
|     from cas_server.admin import * |  | ||||||
|     from cas_server.models import * |  | ||||||
|     admin_site.register(ServicePattern, ServicePatternAdmin) |  | ||||||
|     admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin) |  | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ INSTALLED_APPS = [ | |||||||
|     'bootstrap_datepicker_plus', |     'bootstrap_datepicker_plus', | ||||||
|     'colorfield', |     'colorfield', | ||||||
|     'crispy_forms', |     'crispy_forms', | ||||||
|     'django_htcpcp_tea', |     'crispy_bootstrap4', | ||||||
|     'django_tables2', |     'django_tables2', | ||||||
|     'mailer', |     'mailer', | ||||||
|     'phonenumber_field', |     'phonenumber_field', | ||||||
| @@ -90,7 +90,6 @@ MIDDLEWARE = [ | |||||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', |     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||||
|     'django.middleware.locale.LocaleMiddleware', |     'django.middleware.locale.LocaleMiddleware', | ||||||
|     'django.contrib.sites.middleware.CurrentSiteMiddleware', |     'django.contrib.sites.middleware.CurrentSiteMiddleware', | ||||||
|     'django_htcpcp_tea.middleware.HTCPCPTeaMiddleware', |  | ||||||
|     'note_kfet.middlewares.SessionMiddleware', |     'note_kfet.middlewares.SessionMiddleware', | ||||||
|     'note_kfet.middlewares.LoginByIPMiddleware', |     'note_kfet.middlewares.LoginByIPMiddleware', | ||||||
|     'note_kfet.middlewares.TurbolinksMiddleware', |     'note_kfet.middlewares.TurbolinksMiddleware', | ||||||
| @@ -295,3 +294,6 @@ PHONENUMBER_DEFAULT_REGION = 'FR' | |||||||
|  |  | ||||||
| # We add custom information to CAS, in order to give a normalized name to other services | # We add custom information to CAS, in order to give a normalized name to other services | ||||||
| CAS_AUTH_CLASS = 'member.auth.CustomAuthUser' | CAS_AUTH_CLASS = 'member.auth.CustomAuthUser' | ||||||
|  |  | ||||||
|  | # Default field for primary key | ||||||
|  | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|    {% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %} |    {% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %} | ||||||
|    name="{{ widget.name }}_name" autocomplete="off" |    name="{{ widget.name }}_name" autocomplete="off" | ||||||
|     {% for name, value in widget.attrs.items %} |     {% for name, value in widget.attrs.items %} | ||||||
|         {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %} |         {% if value != False %}{{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     aria-describedby="{{widget.attrs.id}}_tooltip"> |     aria-describedby="{{widget.attrs.id}}_tooltip"> | ||||||
|     {% if widget.resetable %} |     {% if widget.resetable %} | ||||||
|   | |||||||
| @@ -126,9 +126,12 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|                                 <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> |                                 <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> | ||||||
|                                     <i class="fa fa-user"></i> {% trans "My account" %} |                                     <i class="fa fa-user"></i> {% trans "My account" %} | ||||||
|                                 </a> |                                 </a> | ||||||
|                                 <a class="dropdown-item" href="{% url 'logout' %}"> |                                 <form method="post" action="{% url 'logout' %}"> | ||||||
|  |                                   {% csrf_token %} | ||||||
|  |                                   <button class="dropdown-item" type="submit"> | ||||||
|                                     <i class="fa fa-sign-out"></i> {% trans "Log out" %} |                                     <i class="fa fa-sign-out"></i> {% trans "Log out" %} | ||||||
|                                 </a> |                                   </button> | ||||||
|  |                                 </form> | ||||||
|                             </div> |                             </div> | ||||||
|                         </li> |                         </li> | ||||||
|                     {% else %} |                     {% else %} | ||||||
|   | |||||||
| @@ -30,9 +30,6 @@ urlpatterns = [ | |||||||
|     path('accounts/', include('django.contrib.auth.urls')), |     path('accounts/', include('django.contrib.auth.urls')), | ||||||
|     path('api/', include('api.urls')), |     path('api/', include('api.urls')), | ||||||
|     path('permission/', include('permission.urls')), |     path('permission/', include('permission.urls')), | ||||||
|  |  | ||||||
|     # Make coffee |  | ||||||
|     path('coffee/', include('django_htcpcp_tea.urls')), |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| # During development, serve static and media files | # During development, serve static and media files | ||||||
| @@ -46,11 +43,6 @@ if "oauth2_provider" in settings.INSTALLED_APPS: | |||||||
|         path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')) |         path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| if "cas_server" in settings.INSTALLED_APPS: |  | ||||||
|     urlpatterns.append( |  | ||||||
|         path('cas/', include('cas_server.urls', namespace='cas_server')) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
| if "debug_toolbar" in settings.INSTALLED_APPS: | if "debug_toolbar" in settings.INSTALLED_APPS: | ||||||
|     import debug_toolbar |     import debug_toolbar | ||||||
|     urlpatterns = [ |     urlpatterns = [ | ||||||
|   | |||||||
| @@ -1,19 +1,17 @@ | |||||||
| beautifulsoup4~=4.7.1 | beautifulsoup4~=4.12.3 | ||||||
| Django~=2.2.15 | crispy-bootstrap4~=2024.1 | ||||||
| django-bootstrap-datepicker-plus~=3.0.5 | Django~=5.0.7 | ||||||
| django-cas-server~=1.2.0 | django-bootstrap-datepicker-plus~=5.0.5 | ||||||
| django-colorfield~=0.3.2 | django-colorfield~=0.11.0 | ||||||
| django-crispy-forms~=1.7.2 | django-crispy-forms~=2.2 | ||||||
| django-extensions>=2.1.4 | django-extensions~=3.2.3 | ||||||
| django-filter~=2.1 | django-filter~=24.2 | ||||||
| django-htcpcp-tea~=0.3.1 | django-mailer~=2.3.2 | ||||||
| django-mailer~=2.0.1 | django-oauth-toolkit~=2.4.0 | ||||||
| django-oauth-toolkit~=1.3.3 | django-phonenumber-field~=8.0.0 | ||||||
| django-phonenumber-field~=5.0.0 | django-polymorphic~=3.1.0 | ||||||
| django-polymorphic>=2.0.3,<3.0.0 | django-rest-polymorphic~=0.1.10 | ||||||
| djangorestframework>=3.9.0,<3.13.0 | django-tables2~=2.7.0 | ||||||
| django-rest-polymorphic~=0.1.9 | djangorestframework~=3.15.2 | ||||||
| django-tables2~=2.3.1 | phonenumbers~=8.13.40 | ||||||
| python-memcached~=1.59 |  | ||||||
| phonenumbers~=8.9.10 |  | ||||||
| Pillow>=5.4.1 | Pillow>=5.4.1 | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,13 +1,10 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = | envlist = | ||||||
|     # Debian Buster Python |  | ||||||
|     py37-django22 |  | ||||||
|  |  | ||||||
|     # Ubuntu 20.04 Python |     # Ubuntu 20.04 Python | ||||||
|     py38-django22 |     py310-django50 | ||||||
|  |  | ||||||
|     # Debian Bullseye Python |     # Debian Bullseye Python | ||||||
|     py39-django22 |     py311-django50 | ||||||
|  |  | ||||||
|     linters |     linters | ||||||
| skipsdist = True | skipsdist = True | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user