From e5f58f30ca01f5f74a3053412197a7b05d4f7903 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doderlein Date: Sun, 31 Jul 2022 12:09:21 +0000 Subject: [PATCH 01/30] Simple gitpod env update --- .gitpod.yml | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index b488382..0eb3a46 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,29 +3,33 @@ # and commit this file to your remote git repository to share the goodness with others. tasks: - - name: Apt update - init: sudo apt update - name: Apt install - init: sudo apt install --no-install-recommends -y \ + command: | + sudo apt update + sudo apt install --no-install-recommends -y \ ipython3 python3-setuptools python3-venv python3-dev \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git + gp sync-done apt - name : Install requirements - init: pip3 install -r requirements.txt + init : gp sync-await apt + command: | + pip3 install -r requirements.txt + gp sync-done pip - name : Setup env - init: cp .env_example .env - - name: Django collectstatic - init: python3 manage.py collectstatic --noinput - - name: Django compilemessages - init: python3 manage.py compilemessages - - name: Django makemigrations - init: python3 manage.py makemigrations - - name: Django migrate - init: python3 manage.py migrate - - name: Django loaddata - init: python3 manage.py loaddata initial - - name: Django create dev superuser - init: python3 manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'adminpass')" + command: cp .env_example .env + - name: Django Init + init : gp sync-await pip + command: | + python3 manage.py collectstatic --noinput + python3 manage.py compilemessages + python3 manage.py makemigrations + python3 manage.py migrate + python3 manage.py loaddata initial + python3 manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'adminpass')" + gp sync-done django + - name: Django start server + init : gp sync-await django command: python3 manage.py runserver 0.0.0.0:8000 ports: - port: 8000 From 2343eabb590f1193eb6f171dd9c1980421243d48 Mon Sep 17 00:00:00 2001 From: Marsupilami1 Date: Sun, 31 Jul 2022 14:16:24 +0200 Subject: [PATCH 02/30] remove wei --- .gitignore | 1 - apps/api/urls.py | 4 - apps/member/forms.py | 2 +- apps/member/templates/member/base.html | 2 +- apps/member/tests/test_memberships.py | 2 +- apps/member/views.py | 13 +- apps/permission/fixtures/initial.json | 5 - apps/permission/tables.py | 2 +- .../templates/permission/all_rights.html | 1 - apps/permission/views.py | 2 +- apps/registration/forms.py | 9 - apps/treasury/models.py | 16 - apps/wei/__init__.py | 4 - apps/wei/admin.py | 13 - apps/wei/api/__init__.py | 0 apps/wei/api/serializers.py | 72 - apps/wei/api/urls.py | 17 - apps/wei/api/views.py | 105 -- apps/wei/apps.py | 10 - apps/wei/forms/__init__.py | 10 - apps/wei/forms/registration.py | 189 --- apps/wei/forms/surveys/__init__.py | 12 - apps/wei/forms/surveys/base.py | 237 ---- apps/wei/forms/surveys/wei2021.py | 293 ---- apps/wei/forms/surveys/wei2022.py | 293 ---- .../commands/export_wei_registrations.py | 88 -- apps/wei/management/commands/import_scores.py | 50 - apps/wei/management/commands/wei_algorithm.py | 58 - apps/wei/migrations/0001_initial.py | 120 -- .../wei/migrations/0002_auto_20210313_1235.py | 23 - apps/wei/migrations/0003_bus_size.py | 18 - apps/wei/migrations/__init__.py | 0 apps/wei/models.py | 390 ------ apps/wei/tables.py | 340 ----- apps/wei/templates/wei/1A_list.html | 20 - apps/wei/templates/wei/attribute_bus_1A.html | 88 -- apps/wei/templates/wei/base.html | 109 -- apps/wei/templates/wei/bus_detail.html | 57 - apps/wei/templates/wei/bus_form.html | 21 - apps/wei/templates/wei/busteam_detail.html | 63 - apps/wei/templates/wei/busteam_form.html | 21 - apps/wei/templates/wei/survey.html | 28 - apps/wei/templates/wei/survey_closed.html | 22 - apps/wei/templates/wei/survey_end.html | 19 - apps/wei/templates/wei/weiclub_detail.html | 122 -- apps/wei/templates/wei/weiclub_form.html | 21 - apps/wei/templates/wei/weiclub_list.html | 73 - apps/wei/templates/wei/weilist_sample.tex | 47 - .../wei/templates/wei/weimembership_form.html | 203 --- .../wei/templates/wei/weimembership_list.html | 55 - .../wei/weiregistration_confirm_delete.html | 36 - .../templates/wei/weiregistration_form.html | 50 - .../templates/wei/weiregistration_list.html | 61 - apps/wei/tests/__init__.py | 0 apps/wei/tests/test_wei_algorithm_2021.py | 110 -- apps/wei/tests/test_wei_algorithm_2022.py | 110 -- apps/wei/tests/test_wei_registration.py | 879 ------------ apps/wei/urls.py | 45 - apps/wei/views.py | 1241 ----------------- docs/api/wei.rst | 710 ---------- docs/apps/wei.rst | 333 ----- note_kfet/settings/base.py | 1 - note_kfet/templates/base.html | 6 - note_kfet/urls.py | 1 - 64 files changed, 8 insertions(+), 6945 deletions(-) delete mode 100644 apps/wei/__init__.py delete mode 100644 apps/wei/admin.py delete mode 100644 apps/wei/api/__init__.py delete mode 100644 apps/wei/api/serializers.py delete mode 100644 apps/wei/api/urls.py delete mode 100644 apps/wei/api/views.py delete mode 100644 apps/wei/apps.py delete mode 100644 apps/wei/forms/__init__.py delete mode 100644 apps/wei/forms/registration.py delete mode 100644 apps/wei/forms/surveys/__init__.py delete mode 100644 apps/wei/forms/surveys/base.py delete mode 100644 apps/wei/forms/surveys/wei2021.py delete mode 100644 apps/wei/forms/surveys/wei2022.py delete mode 100644 apps/wei/management/commands/export_wei_registrations.py delete mode 100644 apps/wei/management/commands/import_scores.py delete mode 100644 apps/wei/management/commands/wei_algorithm.py delete mode 100644 apps/wei/migrations/0001_initial.py delete mode 100644 apps/wei/migrations/0002_auto_20210313_1235.py delete mode 100644 apps/wei/migrations/0003_bus_size.py delete mode 100644 apps/wei/migrations/__init__.py delete mode 100644 apps/wei/models.py delete mode 100644 apps/wei/tables.py delete mode 100644 apps/wei/templates/wei/1A_list.html delete mode 100644 apps/wei/templates/wei/attribute_bus_1A.html delete mode 100644 apps/wei/templates/wei/base.html delete mode 100644 apps/wei/templates/wei/bus_detail.html delete mode 100644 apps/wei/templates/wei/bus_form.html delete mode 100644 apps/wei/templates/wei/busteam_detail.html delete mode 100644 apps/wei/templates/wei/busteam_form.html delete mode 100644 apps/wei/templates/wei/survey.html delete mode 100644 apps/wei/templates/wei/survey_closed.html delete mode 100644 apps/wei/templates/wei/survey_end.html delete mode 100644 apps/wei/templates/wei/weiclub_detail.html delete mode 100644 apps/wei/templates/wei/weiclub_form.html delete mode 100644 apps/wei/templates/wei/weiclub_list.html delete mode 100644 apps/wei/templates/wei/weilist_sample.tex delete mode 100644 apps/wei/templates/wei/weimembership_form.html delete mode 100644 apps/wei/templates/wei/weimembership_list.html delete mode 100644 apps/wei/templates/wei/weiregistration_confirm_delete.html delete mode 100644 apps/wei/templates/wei/weiregistration_form.html delete mode 100644 apps/wei/templates/wei/weiregistration_list.html delete mode 100644 apps/wei/tests/__init__.py delete mode 100644 apps/wei/tests/test_wei_algorithm_2021.py delete mode 100644 apps/wei/tests/test_wei_algorithm_2022.py delete mode 100644 apps/wei/tests/test_wei_registration.py delete mode 100644 apps/wei/urls.py delete mode 100644 apps/wei/views.py delete mode 100644 docs/api/wei.rst delete mode 100644 docs/apps/wei.rst diff --git a/.gitignore b/.gitignore index caa056f..4420051 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,3 @@ ansible/host_vars/*.yaml ansible/hosts apps/member/migrations -apps/wei/migrations diff --git a/apps/api/urls.py b/apps/api/urls.py index 5d8b8b9..4f27185 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -38,10 +38,6 @@ if "logs" in settings.INSTALLED_APPS: from logs.api.urls import register_logs_urls register_logs_urls(router, 'logs') -if "wei" in settings.INSTALLED_APPS: - from wei.api.urls import register_wei_urls - register_wei_urls(router, 'wei') - app_name = 'api' # Wire up our API using automatic URL routing. diff --git a/apps/member/forms.py b/apps/member/forms.py index ffa3b4c..991aa32 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -227,7 +227,7 @@ class MembershipRolesForm(forms.ModelForm): ) roles = forms.ModelMultipleChoiceField( - queryset=Role.objects.filter(weirole=None).all(), + queryset=Role.objects.all(), label=_("Roles"), widget=CheckboxSelectMultiple(), ) diff --git a/apps/member/templates/member/base.html b/apps/member/templates/member/base.html index e1e9335..1eef79e 100644 --- a/apps/member/templates/member/base.html +++ b/apps/member/templates/member/base.html @@ -51,7 +51,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if request.path_info != user_profile_url %} {% trans 'View Profile' %} {% endif %} - {% elif club and not club.weiclub %} + {% elif club %} {% if can_add_members %} {% trans "Add member" %} diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py index a46a23e..7d41b93 100644 --- a/apps/member/tests/test_memberships.py +++ b/apps/member/tests/test_memberships.py @@ -161,7 +161,7 @@ class TestMemberships(TestCase): response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles=" + ",".join([str(role.pk) for role in - Role.objects.filter(weirole__isnull=True).all()])) + Role.objects.all()])) self.assertEqual(response.status_code, 200) def test_render_club_add_member(self): diff --git a/apps/member/views.py b/apps/member/views.py index 5605330..81d054d 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -541,11 +541,6 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - - # Don't update a WEI club through this view - if "wei" in settings.INSTALLED_APPS: - qs = qs.filter(weiclub=None) - return qs def get_success_url(self): @@ -597,7 +592,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): if "club_pk" in self.kwargs: # We create a new membership. club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\ - .get(pk=self.kwargs["club_pk"], weiclub=None) + .get(pk=self.kwargs["club_pk"]) form.fields['credit_amount'].initial = club.membership_fee_paid # Ensure that the user is member of the parent club and all its the family tree. c = club @@ -819,8 +814,7 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form = super().get_form(form_class) club = self.object.club - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) - & (Q(for_club__isnull=True) | Q(for_club=club))).all() + form.fields['roles'].queryset = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all() return form @@ -866,8 +860,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV ).get(pk=self.kwargs["pk"]) context["club"] = club - applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) - & (Q(for_club__isnull=True) | Q(for_club=club))).all() + applicable_roles = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all() context["applicable_roles"] = applicable_roles context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 771b728..149779a 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -2452,10 +2452,5 @@ 181 ] } - }, - { - "model": "wei.weirole", - "pk": 17, - "fields": {} } ] diff --git a/apps/permission/tables.py b/apps/permission/tables.py index eaec513..aea2c92 100644 --- a/apps/permission/tables.py +++ b/apps/permission/tables.py @@ -40,7 +40,7 @@ class RightsTable(tables.Table): | Q(name="Adhérent Kfet") | Q(name="Membre de club") | Q(name="Bureau de club")) - & Q(weirole__isnull=True))).all() + )).all() s = ", ".join(str(role) for role in roles) if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record): s = format_html(" Optional[Bus]: - """ - If the algorithm ran, return the prefered bus according to the survey. - In the other case, return None. - """ - return Bus.objects.get(pk=self.selected_bus_pk) if self.valid else None - - def save(self, registration) -> None: - """ - Store the data of the survey into the database, with the information of the registration. - """ - registration.information = self.__dict__ - - -class WEIBusInformation: - """ - Abstract data of the bus. - """ - - def __init__(self, bus: Bus): - self.__dict__.update(bus.information) - self.bus = bus - self.save() - - def save(self): - d = self.__dict__.copy() - d.pop("bus") - self.bus.information = d - self.bus.save() - - def free_seats(self, surveys: List["WEISurvey"] = None, quotas=None): - if not quotas: - size = self.bus.size - already_occupied = WEIMembership.objects.filter(bus=self.bus).count() - quotas = {self.bus: size - already_occupied} - - quota = quotas[self.bus] - 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 quota - valid_surveys - - def has_free_seats(self, surveys=None, quotas=None): - return self.free_seats(surveys, quotas) > 0 - - -class WEISurveyAlgorithm: - """ - Abstract algorithm that attributes a bus to each new member. - """ - - @classmethod - def get_survey_class(cls): - """ - The class of the survey associated with this algorithm. - """ - raise NotImplementedError - - @classmethod - def get_bus_information_class(cls): - """ - The class of the information associated to a bus extending WEIBusInformation. - Default: WEIBusInformation (contains nothing) - """ - return WEIBusInformation - - @classmethod - def get_registrations(cls) -> QuerySet: - """ - Queryset of all first year registrations - """ - if not hasattr(cls, '_registrations'): - cls._registrations = WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), - first_year=True).all() - - return cls._registrations - - @classmethod - def get_buses(cls) -> QuerySet: - """ - Queryset of all buses of the associated wei. - """ - if not hasattr(cls, '_buses'): - cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all() - return cls._buses - - @classmethod - def get_bus_information(cls, bus): - """ - Return the WEIBusInformation object containing the data stored in a given bus. - """ - return cls.get_bus_information_class()(bus) - - def run_algorithm(self) -> None: - """ - Once this method implemented, run the algorithm that attributes a bus to each first year member. - This method can be run in command line through ``python manage.py wei_algorithm`` - See ``wei.management.commmands.wei_algorithm`` - This method must call Survey.select_bus for each survey. - """ - raise NotImplementedError - - -class WEISurvey: - """ - Survey associated to a first year WEI registration. - The data is stored into WEISurveyInformation, this class acts as a manager. - This is an abstract class: this has to be extended each year to implement custom methods. - """ - - def __init__(self, registration: WEIRegistration): - self.registration = registration - self.information = self.get_survey_information_class()(registration) - - @classmethod - def get_year(cls) -> int: - """ - Get year of the wei concerned by the type of the survey. - """ - raise NotImplementedError - - @classmethod - def get_wei(cls) -> WEIClub: - """ - The WEI associated to this kind of survey. - """ - if not hasattr(cls, '_wei'): - cls._wei = WEIClub.objects.get(year=cls.get_year()) - - return cls._wei - - @classmethod - def get_survey_information_class(cls): - """ - The class of the data (extending WEISurveyInformation). - """ - raise NotImplementedError - - def get_form_class(self) -> Form: - """ - The form class of the survey. - This is proper to the status of the survey: the form class can evolve according to the progress of the survey. - """ - raise NotImplementedError - - def update_form(self, form) -> None: - """ - Once the form is instanciated, the information can be updated with the information of the registration - and the information of the survey. - This method is called once the form is created. - """ - pass - - def form_valid(self, form) -> None: - """ - Called when the information of the form are validated. - This method should update the information of the survey. - """ - raise NotImplementedError - - def is_complete(self) -> bool: - """ - Return True if the survey is complete. - If the survey is complete, then the button "Next" will display some text for the end of the survey. - If not, the survey is reloaded and continues. - """ - raise NotImplementedError - - def save(self) -> None: - """ - Store the information of the survey into the database. - """ - self.information.save(self.registration) - # The information is forced-saved. - # We don't want that anyone can update manually the information, so since most users don't have the - # right to save the information of a registration, we force save. - self.registration._force_save = True - self.registration.save() - - @classmethod - def get_algorithm_class(cls): - """ - Algorithm class associated to the survey. - The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member. - The association is not permanent: that's only a suggestion. - """ - raise NotImplementedError - - def select_bus(self, bus) -> None: - """ - Set the suggestion into the data of the membership. - :param bus: The bus suggested. - """ - self.information.selected_bus_pk = bus.pk - self.information.selected_bus_name = bus.name - self.information.valid = True - - def free(self) -> None: - """ - Unselect the select bus. - """ - self.information.selected_bus_pk = None - self.information.selected_bus_name = None - self.information.valid = False - - @classmethod - def clear_cache(cls): - """ - Clear stored information. - """ - if hasattr(cls, '_wei'): - del cls._wei - if hasattr(cls.get_algorithm_class(), '_registrations'): - del cls.get_algorithm_class()._registrations - if hasattr(cls.get_algorithm_class(), '_buses'): - del cls.get_algorithm_class()._buses diff --git a/apps/wei/forms/surveys/wei2021.py b/apps/wei/forms/surveys/wei2021.py deleted file mode 100644 index e515d44..0000000 --- a/apps/wei/forms/surveys/wei2021.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (C) 2018-2021 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 = [ - '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', - 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', - 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', - 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', - 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', - 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', - 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', - 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', -] - - -class WEISurveyForm2021(forms.Form): - """ - Survey form for the year 2021. - Members choose 20 words, from which we calculate the best associated bus. - """ - - word = forms.ChoiceField( - label=_("Choose a word:"), - widget=forms.RadioSelect(), - ) - - def set_registration(self, registration): - """ - Filter the bus selector with the buses of the current WEI. - """ - information = WEISurveyInformation2021(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 = WEISurveyAlgorithm2021.get_buses() - informations = {bus: WEIBusInformation2021(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 - - -class WEIBusInformation2021(WEIBusInformation): - """ - For each word, the bus has a score - """ - scores: dict - - def __init__(self, bus): - self.scores = {} - for word in WORDS: - self.scores[word] = 0.0 - super().__init__(bus) - - -class WEISurveyInformation2021(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 - - def __init__(self, registration): - for i in range(1, 21): - setattr(self, "word" + str(i), None) - super().__init__(registration) - - -class WEISurvey2021(WEISurvey): - """ - Survey for the year 2021. - """ - - @classmethod - def get_year(cls): - return 2021 - - @classmethod - def get_survey_information_class(cls): - return WEISurveyInformation2021 - - def get_form_class(self): - return WEISurveyForm2021 - - 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): - word = form.cleaned_data["word"] - self.information.step += 1 - setattr(self.information, "word" + str(self.information.step), word) - self.save() - - @classmethod - def get_algorithm_class(cls): - return WEISurveyAlgorithm2021 - - def is_complete(self) -> bool: - """ - 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() - - @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 = 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 - 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): - cls.word_mean.cache_clear() - return super().clear_cache() - - -class WEISurveyAlgorithm2021(WEISurveyAlgorithm): - """ - The algorithm class for the year 2021. - We use Gale-Shapley algorithm to attribute 1y students into buses. - """ - - @classmethod - def get_survey_class(cls): - return WEISurvey2021 - - @classmethod - def get_bus_information_class(cls): - return WEIBusInformation2021 - - 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 - WEISurvey2021.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() diff --git a/apps/wei/forms/surveys/wei2022.py b/apps/wei/forms/surveys/wei2022.py deleted file mode 100644 index db553c0..0000000 --- a/apps/wei/forms/surveys/wei2022.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (C) 2018-2022 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 = [ - '13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant', - 'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill', - 'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial', - 'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno', - 'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit', - 'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic', - 'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi', - 'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane', -] - - -class WEISurveyForm2022(forms.Form): - """ - Survey form for the year 2022. - Members choose 20 words, from which we calculate the best associated bus. - """ - - word = forms.ChoiceField( - label=_("Choose a word:"), - widget=forms.RadioSelect(), - ) - - def set_registration(self, registration): - """ - Filter the bus selector with the buses of the current WEI. - """ - information = WEISurveyInformation2022(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 = WEISurveyAlgorithm2022.get_buses() - informations = {bus: WEIBusInformation2022(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 - - -class WEIBusInformation2022(WEIBusInformation): - """ - For each word, the bus has a score - """ - scores: dict - - def __init__(self, bus): - self.scores = {} - for word in WORDS: - self.scores[word] = 0.0 - super().__init__(bus) - - -class WEISurveyInformation2022(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 - - def __init__(self, registration): - for i in range(1, 21): - setattr(self, "word" + str(i), None) - super().__init__(registration) - - -class WEISurvey2022(WEISurvey): - """ - Survey for the year 2022. - """ - - @classmethod - def get_year(cls): - return 2022 - - @classmethod - def get_survey_information_class(cls): - return WEISurveyInformation2022 - - def get_form_class(self): - return WEISurveyForm2022 - - 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): - word = form.cleaned_data["word"] - self.information.step += 1 - setattr(self.information, "word" + str(self.information.step), word) - self.save() - - @classmethod - def get_algorithm_class(cls): - return WEISurveyAlgorithm2022 - - def is_complete(self) -> bool: - """ - 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() - - @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 = 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 - 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): - cls.word_mean.cache_clear() - return super().clear_cache() - - -class WEISurveyAlgorithm2022(WEISurveyAlgorithm): - """ - The algorithm class for the year 2022. - We use Gale-Shapley algorithm to attribute 1y students into buses. - """ - - @classmethod - def get_survey_class(cls): - return WEISurvey2022 - - @classmethod - def get_bus_information_class(cls): - return WEIBusInformation2022 - - 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 - WEISurvey2022.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() diff --git a/apps/wei/management/commands/export_wei_registrations.py b/apps/wei/management/commands/export_wei_registrations.py deleted file mode 100644 index a51f1c6..0000000 --- a/apps/wei/management/commands/export_wei_registrations.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.core.management import BaseCommand, CommandError -from django.db.models import Q -from django.db.models.functions import Lower -from wei.models import WEIClub, Bus, BusTeam, WEIMembership - - -class Command(BaseCommand): - help = "Export WEI registrations." - - def add_arguments(self, parser): - parser.add_argument('--bus', '-b', choices=[bus.name for bus in Bus.objects.all()], type=str, default=None, - help='Filter by bus') - parser.add_argument('--team', '-t', choices=[team.name for team in BusTeam.objects.all()], type=str, - default=None, help='Filter by team. Type "none" if you want to select the members ' - + 'that are not in a team.') - parser.add_argument('--year', '-y', type=int, default=None, - help='Select the year of the concerned WEI. Default: last year') - parser.add_argument('--sep', type=str, default='|', - help='Select the CSV separator.') - - def handle(self, *args, **options): - year = options["year"] - if year: - try: - wei = WEIClub.objects.get(year=year) - except WEIClub.DoesNotExist: - raise CommandError("The WEI of year {:d} does not exist.".format(year,)) - else: - wei = WEIClub.objects.order_by('-year').first() - - bus = options["bus"] - if bus: - try: - bus = Bus.objects.filter(wei=wei).get(name=bus) - except Bus.DoesNotExist: - raise CommandError("The bus {} does not exist or does not belong to the WEI {}.".format(bus, wei.name,)) - - team = options["team"] - if team: - if team.lower() == "none": - team = 0 - else: - try: - team = BusTeam.objects.filter(Q(bus=bus) | Q(wei=wei)).get(name=team) - bus = team.bus - except BusTeam.DoesNotExist: - raise CommandError("The bus {} does not exist or does not belong to the bus {} neither the wei {}." - .format(team, bus.name if bus else "", wei.name,)) - - qs = WEIMembership.objects - qs = qs.filter(club=wei).order_by( - Lower('bus__name'), - Lower('team__name'), - 'user__profile__promotion', - Lower('user__last_name'), - Lower('user__first_name'), - ).distinct() - - if bus: - qs = qs.filter(bus=bus) - - if team is not None: - qs = qs.filter(team=team if team else None) - - sep = options["sep"] - - self.stdout.write("Nom|Prénom|Date de naissance|Genre|Département|Année|Section|Bus|Équipe|Rôles" - .replace(sep, sep)) - - for membership in qs.all(): - user = membership.user - registration = membership.registration - bus = membership.bus - team = membership.team - s = user.last_name - s += sep + user.first_name - s += sep + str(registration.birth_date) - s += sep + registration.get_gender_display() - s += sep + user.profile.get_department_display() - s += sep + str(user.profile.ens_year) + "A" - s += sep + user.profile.section_generated - s += sep + bus.name - s += sep + (team.name if team else "--") - s += sep + ", ".join(role.name for role in membership.roles.filter(~Q(name="Adhérent WEI")).all()) - self.stdout.write(s) diff --git a/apps/wei/management/commands/import_scores.py b/apps/wei/management/commands/import_scores.py deleted file mode 100644 index f8587cf..0000000 --- a/apps/wei/management/commands/import_scores.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -import argparse -import sys - -from django.core.management import BaseCommand -from django.db import transaction - -from ...forms import CurrentSurvey -from ...forms.surveys.wei2021 import WORDS # WARNING: this is specific to 2021 -from ...models import Bus - - -class Command(BaseCommand): - """ - This script is used to load scores for buses from a CSV file. - """ - def add_arguments(self, parser): - parser.add_argument('file', nargs='?', type=argparse.FileType('r'), default=sys.stdin, help='Input CSV file') - - @transaction.atomic - def handle(self, *args, **options): - file = options['file'] - head = file.readline().replace('\n', '') - bus_names = head.split(';') - bus_names = [name for name in bus_names if name] - buses = [] - for name in bus_names: - qs = Bus.objects.filter(name__iexact=name) - if not qs.exists(): - raise ValueError(f"Bus '{name}' does not exist") - buses.append(qs.get()) - - informations = {bus: CurrentSurvey.get_algorithm_class().get_bus_information(bus) for bus in buses} - - for line in file: - elem = line.split(';') - word = elem[0] - if word not in WORDS: - raise ValueError(f"Word {word} is not used") - - for i, bus in enumerate(buses): - info = informations[bus] - info.scores[word] = float(elem[i + 1].replace(',', '.')) - - for bus, info in informations.items(): - info.save() - bus.save() - if options['verbosity'] > 0: - self.stdout.write(f"Bus {bus.name} saved!") diff --git a/apps/wei/management/commands/wei_algorithm.py b/apps/wei/management/commands/wei_algorithm.py deleted file mode 100644 index 9e89503..0000000 --- a/apps/wei/management/commands/wei_algorithm.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -from argparse import ArgumentParser, FileType - -from django.core.management import BaseCommand -from django.db import transaction - -from ...forms import CurrentSurvey - - -class Command(BaseCommand): - help = "Attribute to each first year member a bus for the WEI" - - def add_arguments(self, parser: ArgumentParser): - parser.add_argument('--doit', '-d', action='store_true', help='Finally run the algorithm in non-dry mode.') - parser.add_argument('--output', '-o', nargs='?', type=FileType('w'), default=self.stdout, - help='Output file for the algorithm result. Default is standard output.') - - @transaction.atomic - def handle(self, *args, **options): - """ - Run the WEI algorithm to attribute a bus to each first year member. - """ - sid = transaction.savepoint() - - algorithm = CurrentSurvey.get_algorithm_class()() - - try: - from tqdm import tqdm - del tqdm - display_tqdm = True - except ImportError: - display_tqdm = False - - algorithm.run_algorithm(display_tqdm=display_tqdm) - - output = options['output'] - registrations = algorithm.get_registrations() - per_bus = {bus: [r for r in registrations if 'selected_bus_pk' in r.information - and r.information['selected_bus_pk'] == bus.pk] - for bus in algorithm.get_buses()} - for bus, members in per_bus.items(): - output.write(bus.name + "\n") - output.write("=" * len(bus.name) + "\n") - _order = -1 - for r in members: - survey = CurrentSurvey(r) - for _order, (b, _score) in enumerate(survey.ordered_buses()): - if b == bus: - break - output.write(f"{r.user.username} ({_order + 1})\n") - output.write("\n") - - if not options['doit']: - self.stderr.write(self.style.WARNING("Running in dry mode. " - "Use --doit option to really execute the algorithm.")) - transaction.savepoint_rollback(sid) - return diff --git a/apps/wei/migrations/0001_initial.py b/apps/wei/migrations/0001_initial.py deleted file mode 100644 index 0ad73fa..0000000 --- a/apps/wei/migrations/0001_initial.py +++ /dev/null @@ -1,120 +0,0 @@ -# Generated by Django 2.2.16 on 2020-09-04 21:41 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import phonenumber_field.modelfields - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('permission', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('member', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Bus', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='name')), - ('description', models.TextField(blank=True, default='', verbose_name='description')), - ('information_json', models.TextField(default='{}', help_text='Information about the survey for new members, encoded in JSON', verbose_name='survey information')), - ], - options={ - 'verbose_name': 'Bus', - 'verbose_name_plural': 'Buses', - }, - ), - migrations.CreateModel( - name='BusTeam', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='name')), - ('color', models.PositiveIntegerField(help_text='The color of the T-Shirt, stored with its number equivalent', verbose_name='color')), - ('description', models.TextField(blank=True, default='', verbose_name='description')), - ('bus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='wei.Bus', verbose_name='bus')), - ], - options={ - 'verbose_name': 'Bus team', - 'verbose_name_plural': 'Bus teams', - 'unique_together': {('bus', 'name')}, - }, - ), - migrations.CreateModel( - name='WEIClub', - fields=[ - ('club_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.Club')), - ('year', models.PositiveIntegerField(default=2020, unique=True, verbose_name='year')), - ('date_start', models.DateField(verbose_name='date start')), - ('date_end', models.DateField(verbose_name='date end')), - ], - options={ - 'verbose_name': 'WEI', - 'verbose_name_plural': 'WEI', - }, - bases=('member.club',), - ), - migrations.CreateModel( - name='WEIRole', - fields=[ - ('role_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='permission.Role')), - ], - options={ - 'verbose_name': 'WEI Role', - 'verbose_name_plural': 'WEI Roles', - }, - bases=('permission.role',), - ), - migrations.CreateModel( - name='WEIRegistration', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('soge_credit', models.BooleanField(default=False, verbose_name='Credit from Société générale')), - ('caution_check', models.BooleanField(default=False, verbose_name='Caution check given')), - ('birth_date', models.DateField(verbose_name='birth date')), - ('gender', models.CharField(choices=[('male', 'Male'), ('female', 'Female'), ('nonbinary', 'Non binary')], max_length=16, verbose_name='gender')), - ('clothing_cut', models.CharField(choices=[('male', 'Male'), ('female', 'Female')], max_length=16, verbose_name='clothing cut')), - ('clothing_size', models.CharField(choices=[('XS', 'XS'), ('S', 'S'), ('M', 'M'), ('L', 'L'), ('XL', 'XL'), ('XXL', 'XXL')], max_length=4, verbose_name='clothing size')), - ('health_issues', models.TextField(blank=True, default='', verbose_name='health issues')), - ('emergency_contact_name', models.CharField(max_length=255, verbose_name='emergency contact name')), - ('emergency_contact_phone', phonenumber_field.modelfields.PhoneNumberField(max_length=32, region=None, verbose_name='emergency contact phone')), - ('first_year', models.BooleanField(default=False, help_text='Tells if the user is new in the school.', verbose_name='first year')), - ('information_json', models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey fot the new members), encoded in JSON', verbose_name='registration information')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wei', to=settings.AUTH_USER_MODEL, verbose_name='user')), - ('wei', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='users', to='wei.WEIClub', verbose_name='WEI')), - ], - options={ - 'verbose_name': 'WEI User', - 'verbose_name_plural': 'WEI Users', - 'unique_together': {('user', 'wei')}, - }, - ), - migrations.CreateModel( - name='WEIMembership', - fields=[ - ('membership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.Membership')), - ('bus', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='wei.Bus', verbose_name='bus')), - ('registration', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='membership', to='wei.WEIRegistration', verbose_name='WEI registration')), - ('team', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='wei.BusTeam', verbose_name='team')), - ], - options={ - 'verbose_name': 'WEI membership', - 'verbose_name_plural': 'WEI memberships', - }, - bases=('member.membership',), - ), - migrations.AddField( - model_name='bus', - name='wei', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='buses', to='wei.WEIClub', verbose_name='WEI'), - ), - migrations.AlterUniqueTogether( - name='bus', - unique_together={('wei', 'name')}, - ), - ] diff --git a/apps/wei/migrations/0002_auto_20210313_1235.py b/apps/wei/migrations/0002_auto_20210313_1235.py deleted file mode 100644 index cb4dfbb..0000000 --- a/apps/wei/migrations/0002_auto_20210313_1235.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.19 on 2021-03-13 11:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('wei', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='weiclub', - name='year', - field=models.PositiveIntegerField(default=2021, unique=True, verbose_name='year'), - ), - migrations.AlterField( - model_name='weiregistration', - name='information_json', - field=models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey for the new members), encoded in JSON', verbose_name='registration information'), - ), - ] diff --git a/apps/wei/migrations/0003_bus_size.py b/apps/wei/migrations/0003_bus_size.py deleted file mode 100644 index 85c0939..0000000 --- a/apps/wei/migrations/0003_bus_size.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.19 on 2021-08-25 21:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('wei', '0002_auto_20210313_1235'), - ] - - operations = [ - migrations.AddField( - model_name='bus', - name='size', - field=models.IntegerField(default=50, verbose_name='seat count in the bus'), - ), - ] diff --git a/apps/wei/migrations/__init__.py b/apps/wei/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/wei/models.py b/apps/wei/models.py deleted file mode 100644 index 6b7272b..0000000 --- a/apps/wei/models.py +++ /dev/null @@ -1,390 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -import json -from datetime import date - -from django.conf import settings -from django.contrib.auth.models import User -from django.db import models -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ -from phonenumber_field.modelfields import PhoneNumberField -from member.models import Club, Membership -from note.models import MembershipTransaction -from permission.models import Role - - -class WEIClub(Club): - """ - The WEI is a club. Register to the WEI is equivalent than be member of the club. - """ - year = models.PositiveIntegerField( - unique=True, - default=date.today().year, - verbose_name=_("year"), - ) - - date_start = models.DateField( - verbose_name=_("date start"), - ) - - date_end = models.DateField( - verbose_name=_("date end"), - ) - - @property - def is_current_wei(self): - """ - We consider that this is the current WEI iff there is no future WEI planned. - """ - return not WEIClub.objects.filter(date_start__gt=self.date_start).exists() - - def update_membership_dates(self): - """ - We can't join the WEI next years. - """ - return - - class Meta: - verbose_name = _("WEI") - verbose_name_plural = _("WEI") - - -class Bus(models.Model): - """ - The best bus for the best WEI - """ - wei = models.ForeignKey( - WEIClub, - on_delete=models.PROTECT, - related_name="buses", - verbose_name=_("WEI"), - ) - - name = models.CharField( - max_length=255, - verbose_name=_("name"), - ) - - size = models.IntegerField( - verbose_name=_("seat count in the bus"), - default=50, - ) - - description = models.TextField( - blank=True, - default="", - verbose_name=_("description"), - ) - - information_json = models.TextField( - default="{}", - verbose_name=_("survey information"), - help_text=_("Information about the survey for new members, encoded in JSON"), - ) - - @property - def information(self): - """ - The information about the survey for new members are stored in a dictionary that can evolve following the years. - The dictionary is stored as a JSON string. - """ - return json.loads(self.information_json) - - @information.setter - def information(self, information): - """ - Store information as a JSON string - """ - self.information_json = json.dumps(information, indent=2) - - @property - def suggested_first_year(self): - registrations = WEIRegistration.objects.filter(Q(membership__isnull=True) | Q(membership__bus__isnull=True), - first_year=True, wei=self.wei) - registrations = [r for r in registrations if 'selected_bus_pk' in r.information] - return sum(1 for r in registrations if r.information['selected_bus_pk'] == self.pk) - - def __str__(self): - return self.name - - class Meta: - verbose_name = _("Bus") - verbose_name_plural = _("Buses") - unique_together = ('wei', 'name',) - - -class BusTeam(models.Model): - """ - A bus has multiple teams - """ - bus = models.ForeignKey( - Bus, - on_delete=models.CASCADE, - related_name="teams", - verbose_name=_("bus"), - ) - - name = models.CharField( - max_length=255, - verbose_name=_("name"), - ) - - color = models.PositiveIntegerField( # Use a color picker to get the hexa code - verbose_name=_("color"), - help_text=_("The color of the T-Shirt, stored with its number equivalent"), - ) - - description = models.TextField( - blank=True, - default="", - verbose_name=_("description"), - ) - - def __str__(self): - return self.name + " (" + str(self.bus) + ")" - - class Meta: - unique_together = ('bus', 'name',) - verbose_name = _("Bus team") - verbose_name_plural = _("Bus teams") - - -class WEIRole(Role): - """ - A Role for the WEI can be bus chief, team chief, free electron, ... - """ - - class Meta: - verbose_name = _("WEI Role") - verbose_name_plural = _("WEI Roles") - - -class WEIRegistration(models.Model): - """ - Store personal data that can be useful for the WEI. - """ - - user = models.ForeignKey( - User, - on_delete=models.PROTECT, - related_name="wei", - verbose_name=_("user"), - ) - - wei = models.ForeignKey( - WEIClub, - on_delete=models.PROTECT, - related_name="users", - verbose_name=_("WEI"), - ) - - soge_credit = models.BooleanField( - default=False, - verbose_name=_("Credit from Société générale"), - ) - - caution_check = models.BooleanField( - default=False, - verbose_name=_("Caution check given") - ) - - birth_date = models.DateField( - verbose_name=_("birth date"), - ) - - gender = models.CharField( - max_length=16, - choices=( - ('male', _("Male")), - ('female', _("Female")), - ('nonbinary', _("Non binary")), - ), - verbose_name=_("gender"), - ) - - clothing_cut = models.CharField( - max_length=16, - choices=( - ('male', _("Male")), - ('female', _("Female")), - ), - verbose_name=_("clothing cut"), - ) - - clothing_size = models.CharField( - max_length=4, - choices=( - ('XS', "XS"), - ('S', "S"), - ('M', "M"), - ('L', "L"), - ('XL', "XL"), - ('XXL', "XXL"), - ), - verbose_name=_("clothing size"), - ) - - health_issues = models.TextField( - blank=True, - default="", - verbose_name=_("health issues"), - ) - - emergency_contact_name = models.CharField( - max_length=255, - verbose_name=_("emergency contact name"), - ) - - emergency_contact_phone = PhoneNumberField( - max_length=32, - verbose_name=_("emergency contact phone"), - ) - - first_year = models.BooleanField( - default=False, - verbose_name=_("first year"), - help_text=_("Tells if the user is new in the school.") - ) - - information_json = models.TextField( - default="{}", - verbose_name=_("registration information"), - help_text=_("Information about the registration (buses for old members, survey for the new members), " - "encoded in JSON"), - ) - - @property - def information(self): - """ - The information about the registration (the survey for the new members, the bus for the older members, ...) - are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string. - """ - return json.loads(self.information_json) - - @information.setter - def information(self, information): - """ - Store information as a JSON string - """ - self.information_json = json.dumps(information, indent=2) - - @property - def fee(self): - bde = Club.objects.get(pk=1) - kfet = Club.objects.get(pk=2) - - kfet_member = Membership.objects.filter( - club_id=kfet.id, - user=self.user, - date_start__gte=kfet.membership_start, - ).exists() - bde_member = Membership.objects.filter( - club_id=bde.id, - user=self.user, - date_start__gte=bde.membership_start, - ).exists() - - fee = self.wei.membership_fee_paid if self.user.profile.paid \ - else self.wei.membership_fee_unpaid - if not kfet_member: - fee += kfet.membership_fee_paid if self.user.profile.paid \ - else kfet.membership_fee_unpaid - if not bde_member: - fee += bde.membership_fee_paid if self.user.profile.paid \ - else bde.membership_fee_unpaid - - return fee - - @property - def is_validated(self): - try: - return self.membership is not None - except AttributeError: - return False - - def __str__(self): - return str(self.user) - - class Meta: - unique_together = ('user', 'wei',) - verbose_name = _("WEI User") - verbose_name_plural = _("WEI Users") - - -class WEIMembership(Membership): - bus = models.ForeignKey( - Bus, - on_delete=models.PROTECT, - related_name="memberships", - null=True, - default=None, - verbose_name=_("bus"), - ) - - team = models.ForeignKey( - BusTeam, - on_delete=models.PROTECT, - related_name="memberships", - null=True, - blank=True, - default=None, - verbose_name=_("team"), - ) - - registration = models.OneToOneField( - WEIRegistration, - on_delete=models.PROTECT, - null=True, - blank=True, - default=None, - related_name="membership", - verbose_name=_("WEI registration"), - ) - - class Meta: - verbose_name = _("WEI membership") - verbose_name_plural = _("WEI memberships") - - def make_transaction(self): - """ - Create Membership transaction associated to this membership. - """ - if not self.fee or MembershipTransaction.objects.filter(membership=self).exists(): - return - - if self.fee: - transaction = MembershipTransaction( - membership=self, - source=self.user.note, - destination=self.club.note, - quantity=1, - amount=self.fee, - reason="Adhésion WEI " + self.club.name, - valid=not self.registration.soge_credit # Soge transactions are by default invalidated - ) - transaction._force_save = True - transaction.save(force_insert=True) - - if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS: - # If the soge pays, then the transaction is unvalidated in a first time, then submitted for control - # to treasurers. - transaction.refresh_from_db() - from treasury.models import SogeCredit - soge_credit, created = SogeCredit.objects.get_or_create(user=self.user) - soge_credit.refresh_from_db() - transaction.save() - soge_credit.transactions.add(transaction) - soge_credit.save() - - soge_credit.update_transactions() - soge_credit.save() - - if soge_credit.valid and \ - soge_credit.credit_transaction.total != sum(tr.total for tr in soge_credit.transactions.all()): - # The credit is already validated, but we add a new transaction (eg. for the WEI). - # Then we invalidate the transaction, update the credit transaction amount - # and re-validate the credit. - soge_credit.validate(True) - soge_credit.save() diff --git a/apps/wei/tables.py b/apps/wei/tables.py deleted file mode 100644 index 687f8f0..0000000 --- a/apps/wei/tables.py +++ /dev/null @@ -1,340 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from datetime import date - -import django_tables2 as tables -from django.db.models import Q -from django.urls import reverse_lazy -from django.utils.html import format_html -from django.utils.translation import gettext_lazy as _ -from django_tables2 import A -from note_kfet.middlewares import get_current_request -from permission.backends import PermissionBackend - -from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership - - -class WEITable(tables.Table): - """ - List all WEI. - """ - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = WEIClub - template_name = 'django_tables2/bootstrap4.html' - fields = ('name', 'year', 'date_start', 'date_end',) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - 'data-href': lambda record: reverse_lazy('wei:wei_detail', args=(record.pk,)) - } - - -class WEIRegistrationTable(tables.Table): - """ - List all WEI registrations. - """ - user = tables.LinkColumn( - 'member:user_detail', - args=[A('user__pk')], - ) - - edit = tables.LinkColumn( - 'wei:wei_update_registration', - orderable=False, - args=[A('pk')], - verbose_name=_("Edit"), - text=_("Edit"), - attrs={ - 'a': { - 'class': 'btn btn-warning', - 'data-turbolinks': 'false', - } - } - ) - - validate = tables.Column( - verbose_name=_("Validate"), - orderable=False, - accessor=A('pk'), - attrs={ - 'th': { - 'id': 'validate-membership-header' - } - } - ) - - delete = tables.LinkColumn( - 'wei:wei_delete_registration', - args=[A('pk')], - orderable=False, - verbose_name=_("delete"), - text=_("Delete"), - attrs={ - 'th': { - 'id': 'delete-membership-header' - }, - 'a': { - 'class': 'btn btn-danger', - 'data-type': 'delete-membership' - } - }, - ) - - def render_validate(self, record): - hasperm = PermissionBackend.check_perm( - get_current_request(), "wei.add_weimembership", WEIMembership( - club=record.wei, - user=record.user, - date_start=date.today(), - date_end=date.today(), - fee=0, - registration=record, - ) - ) - if not hasperm: - return format_html("") - - url = reverse_lazy('wei:validate_registration', args=(record.pk,)) - text = _('Validate') - if record.fee > record.user.note.balance and not record.soge_credit: - btn_class = 'btn-secondary' - tooltip = _("The user does not have enough money.") - elif record.first_year: - btn_class = 'btn-info' - tooltip = _("The user is in first year. You may validate the credit, the algorithm will run later.") - else: - btn_class = 'btn-success' - tooltip = _("The user has enough money, you can validate the registration.") - - return format_html(f"{text}") - - def render_delete(self, record): - hasperm = PermissionBackend.check_perm(get_current_request(), "wei.delete_weimembership", record) - return _("Delete") if hasperm else format_html("") - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = WEIRegistration - template_name = 'django_tables2/bootstrap4.html' - fields = ('user', 'user__first_name', 'user__last_name', 'first_year', 'caution_check', - 'edit', 'validate', 'delete',) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - 'data-href': lambda record: record.pk - } - - -class WEIMembershipTable(tables.Table): - user = tables.LinkColumn( - 'wei:wei_update_registration', - args=[A('registration__pk')], - ) - - year = tables.Column( - accessor=A("pk"), - verbose_name=_("Year"), - ) - - bus = tables.LinkColumn( - 'wei:manage_bus', - args=[A('bus__pk')], - ) - - team = tables.LinkColumn( - 'wei:manage_bus_team', - args=[A('team__pk')], - ) - - def render_year(self, record): - return str(record.user.profile.ens_year) + "A" - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = WEIMembership - template_name = 'django_tables2/bootstrap4.html' - fields = ('user', 'user__last_name', 'user__first_name', 'registration__gender', 'user__profile__department', - 'year', 'bus', 'team', 'registration__caution_check', ) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - } - - -class WEIRegistration1ATable(tables.Table): - user = tables.LinkColumn( - 'wei:wei_bus_1A', - args=[A('pk')], - ) - - preferred_bus = tables.Column( - verbose_name=_('preferred bus').capitalize, - accessor='pk', - orderable=False, - ) - - def render_preferred_bus(self, record): - information = record.information - return information['selected_bus_name'] if 'selected_bus_name' in information else "—" - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = WEIRegistration - template_name = 'django_tables2/bootstrap4.html' - fields = ('user', 'user__last_name', 'user__first_name', 'gender', - 'user__profile__department', 'preferred_bus', 'membership__bus', ) - row_attrs = { - 'class': lambda record: '' if 'selected_bus_pk' in record.information else 'bg-danger', - } - - -class BusTable(tables.Table): - name = tables.LinkColumn( - 'wei:manage_bus', - args=[A('pk')], - ) - - teams = tables.Column( - accessor=A("teams"), - verbose_name=_("Teams"), - attrs={ - "td": { - "class": "text-truncate", - } - } - ) - - count = tables.Column( - verbose_name=_("Members count"), - ) - - def render_teams(self, value): - return ", ".join(team.name for team in value.order_by('name').all()) - - def render_count(self, value): - return str(value) + " " + (str(_("members")) if value > 1 else str(_("member"))) - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = Bus - template_name = 'django_tables2/bootstrap4.html' - fields = ('name', 'teams', ) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - } - - -class BusTeamTable(tables.Table): - name = tables.LinkColumn( - 'wei:manage_bus_team', - args=[A('pk')], - ) - - color = tables.Column( - attrs={ - "td": { - "style": lambda record: "background-color: #{:06X}; color: #{:06X};" - .format(record.color, 0xFFFFFF - record.color, ) - } - } - ) - - def render_count(self, value): - return str(value) + " " + (str(_("members")) if value > 1 else str(_("member"))) - - count = tables.Column( - verbose_name=_("Members count"), - ) - - def render_color(self, value): - return "#{:06X}".format(value) - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - model = BusTeam - template_name = 'django_tables2/bootstrap4.html' - fields = ('name', 'color',) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - 'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, )) - } - - -class BusRepartitionTable(tables.Table): - name = tables.Column( - verbose_name=_("name").capitalize, - accessor='name', - ) - - suggested_first_year = tables.Column( - verbose_name=_("suggested first year").capitalize, - accessor='pk', - orderable=False, - ) - - validated_first_year = tables.Column( - verbose_name=_("validated first year").capitalize, - accessor='pk', - orderable=False, - ) - - validated_staff = tables.Column( - verbose_name=_("validated staff").capitalize, - accessor='pk', - orderable=False, - ) - - size = tables.Column( - verbose_name=_("seat count in the bus").capitalize, - accessor='size', - ) - - free_seats = tables.Column( - verbose_name=_("free seats").capitalize, - accessor='pk', - orderable=False, - ) - - def render_suggested_first_year(self, record): - registrations = WEIRegistration.objects.filter(Q(membership__isnull=True) | Q(membership__bus__isnull=True), - first_year=True, wei=record.wei) - registrations = [r for r in registrations if 'selected_bus_pk' in r.information] - return sum(1 for r in registrations if r.information['selected_bus_pk'] == record.pk) - - def render_validated_first_year(self, record): - return WEIRegistration.objects.filter(first_year=True, membership__bus=record).count() - - def render_validated_staff(self, record): - return WEIRegistration.objects.filter(first_year=False, membership__bus=record).count() - - def render_free_seats(self, record): - return record.size - self.render_validated_staff(record) - self.render_validated_first_year(record) - - class Meta: - attrs = { - 'class': 'table table-condensed table-striped table-hover' - } - models = Bus - template_name = 'django_tables2/bootstrap4.html' - fields = ('name', ) - row_attrs = { - 'class': 'table-row', - 'id': lambda record: "row-" + str(record.pk), - } diff --git a/apps/wei/templates/wei/1A_list.html b/apps/wei/templates/wei/1A_list.html deleted file mode 100644 index d9b8293..0000000 --- a/apps/wei/templates/wei/1A_list.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "wei/base.html" %} - -{% load i18n %} -{% load render_table from django_tables2 %} - -{% block profile_content %} -
-
-

{% trans "Attribute first year members into buses" %}

-
- -
- {% render_table bus_repartition_table %} -
- {% trans "Start attribution!" %} -
- {% render_table table %} -
-
-{% endblock %} diff --git a/apps/wei/templates/wei/attribute_bus_1A.html b/apps/wei/templates/wei/attribute_bus_1A.html deleted file mode 100644 index 3305981..0000000 --- a/apps/wei/templates/wei/attribute_bus_1A.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends "wei/base.html" %} - -{% load i18n %} - -{% block profile_content %} -
-
-

{% trans "Bus attribution" %}

-
- -
-
-
{% trans 'user'|capfirst %}
-
{{ object.user }}
- -
{% trans 'last name'|capfirst %}
-
{{ object.user.last_name }}
- -
{% trans 'first name'|capfirst %}
-
{{ object.user.first_name }}
- -
{% trans 'gender'|capfirst %}
-
{{ object.get_gender_display }}
- -
{% trans 'department'|capfirst %}
-
{{ object.user.profile.get_department_display }}
- -
{% trans 'health issues'|capfirst %}
-
{{ object.health_issues|default:"—" }}
- -
{% trans 'suggested bus'|capfirst %}
-
{{ survey.information.selected_bus_name }}
-
- -
-
- -
-
-
- {% for key, value in survey.registration.information.items %} -
{{ key }}
-
{{ value }}
- {% endfor %} -
-
-
- -
- - {% for bus, score in survey.ordered_buses %} - - {% endfor %} - - {% trans "Back to main list" %} -
-
-{% endblock %} - -{% block extrajavascript %} - -{% endblock %} diff --git a/apps/wei/templates/wei/base.html b/apps/wei/templates/wei/base.html deleted file mode 100644 index 43d6179..0000000 --- a/apps/wei/templates/wei/base.html +++ /dev/null @@ -1,109 +0,0 @@ -{% extends "base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n pretty_money perms %} - -{# Use a fluid-width container #} -{% block containertype %}container-fluid{% endblock %} - -{% block content %} -
-
- {% block profile_info %} - {% if club %} -
-

- {{ club.name }} -

-
- - - -
-
-
-
{% trans 'name'|capfirst %}
-
{{ club.name }}
- - {% if club.require_memberships %} -
{% trans 'date start'|capfirst %}
-
{{ club.date_start }}
- -
{% trans 'date end'|capfirst %}
-
{{ club.date_end }}
- -
{% trans 'year'|capfirst %}
-
{{ club.year }}
- - {% if club.membership_fee_paid == club.membership_fee_unpaid %} -
{% trans 'membership fee'|capfirst %}
-
{{ club.membership_fee_paid|pretty_money }}
- {% else %} - {% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %} -
{% trans 'WEI fee (paid students)'|capfirst %}
-
{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }} -
- {% endwith %} - - {% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %} -
{% trans 'WEI fee (unpaid students)'|capfirst %}
-
{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }} -
- {% endwith %} - {% endif %} - {% endif %} - - {% if "note.view_note"|has_perm:club.note %} -
{% trans 'balance'|capfirst %}
-
{{ club.note.balance | pretty_money }}
- {% endif %} - - {% if "note.change_alias"|has_perm:club.note.alias.first %} -
{% trans 'aliases'|capfirst %}
-
{{ club.note.alias.all|join:", " }}
- {% endif %} - -
{% trans 'email'|capfirst %}
-
{{ club.email }}
-
-
- -
- {% endif %} - {% endblock %} -
-
- {% block profile_content %}{% endblock %} -
-
-{% endblock %} diff --git a/apps/wei/templates/wei/bus_detail.html b/apps/wei/templates/wei/bus_detail.html deleted file mode 100644 index c8f3ce2..0000000 --- a/apps/wei/templates/wei/bus_detail.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block profile_content %} -
-
-

{{ object.name }}

-
- -
- {{ object.description }} -
- - -
- -
- -{% if teams.data %} -
- - {% render_table teams %} -
- -
-{% endif %} - -{% if memberships.data %} -
- - {% render_table memberships %} -
- -
- - - - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/bus_form.html b/apps/wei/templates/wei/bus_form.html deleted file mode 100644 index c62fec4..0000000 --- a/apps/wei/templates/wei/bus_form.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load crispy_forms_tags %} -{% load i18n %} - -{% block profile_content %} -
-

- {{ title }} -

-
-
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/busteam_detail.html b/apps/wei/templates/wei/busteam_detail.html deleted file mode 100644 index 27348d0..0000000 --- a/apps/wei/templates/wei/busteam_detail.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block profile_content %} -
-
-

{{ bus.name }}

-
- -
- {{ bus.description }} -
- - -
- -
- -
-
-

{{ object.name }}

-
- -
- {{ object.description }} -
- - -
- -
- -{% if memberships.data or True %} -
- - {% render_table memberships %} -
- -
- - - - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/busteam_form.html b/apps/wei/templates/wei/busteam_form.html deleted file mode 100644 index c62fec4..0000000 --- a/apps/wei/templates/wei/busteam_form.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load crispy_forms_tags %} -{% load i18n %} - -{% block profile_content %} -
-

- {{ title }} -

-
-
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/survey.html b/apps/wei/templates/wei/survey.html deleted file mode 100644 index 9eabab5..0000000 --- a/apps/wei/templates/wei/survey.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load crispy_forms_tags %} - -{% block profile_content %} -
-
-

{% trans "Survey WEI" %}

-
-
-
-
{% trans 'user'|capfirst %}
-
{{ object.user }}
-
- -
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-{% endblock %} diff --git a/apps/wei/templates/wei/survey_closed.html b/apps/wei/templates/wei/survey_closed.html deleted file mode 100644 index aac9e83..0000000 --- a/apps/wei/templates/wei/survey_closed.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load crispy_forms_tags %} - -{% block profile_content %} -
-
-

{% trans "Survey WEI" %}

-
-
-

- {% trans "The inscription for this WEI are now closed." %} -

-
- -
-{% endblock %} diff --git a/apps/wei/templates/wei/survey_end.html b/apps/wei/templates/wei/survey_end.html deleted file mode 100644 index 3152f6e..0000000 --- a/apps/wei/templates/wei/survey_end.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load crispy_forms_tags %} - -{% block profile_content %} -
-
-

{% trans "Survey WEI" %}

-
-
-

- {% trans "The survey is now ended. Your answers have been saved." %} -

-
-
-{% endblock %} diff --git a/apps/wei/templates/wei/weiclub_detail.html b/apps/wei/templates/wei/weiclub_detail.html deleted file mode 100644 index cd4b5ef..0000000 --- a/apps/wei/templates/wei/weiclub_detail.html +++ /dev/null @@ -1,122 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load render_table from django_tables2 %} -{% load i18n perms %} - -{% block profile_content %} -
-
-

Week-End d'Intégration

-
-
-

- Le WEI (Week-End d’Intégration), ou 3 jours d’immersion dans les profondeurs du - monde post-préparatoire. -

-

- Que serait une école sans son week-end d’intégration ? Quelques semaines après la - rentrée, on embarque tous et toutes à bord de bus à thèmes pour quelques jours - inoubliables dans une destination inconnue. L’objectif de ce week-end : permettre aux - nouvel·les arrivant·es de se lâcher après 2 ans de dur labeur (voire 3 pour les plus - chanceux), de découvrir l’ambiance familiale de l’ENS ainsi que de nouer des liens avec - ceux·elles qu’ils côtoieront par la suite. Dose de chants et de fun garantie ! -

-
- {% if club.is_current_wei %} - - {% endif %} -
- -{% if buses.data %} -
-
- - {% trans "Buses" %} - -
- {% render_table buses %} -
-{% endif %} - -{% if member_list.data %} -
- - {% render_table member_list %} -
-{% endif %} - -{% if history_list.data %} -
- -
- {% render_table history_list %} -
-
-{% endif %} - -{% if pre_registrations.data %} -
- -
- {% render_table pre_registrations %} -
-
-{% endif %} - - {% if can_validate_1a %} - {% trans "Attribute buses" %} - {% endif %} -{% endblock %} - -{% block extrajavascript %} - -{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/weiclub_form.html b/apps/wei/templates/wei/weiclub_form.html deleted file mode 100644 index c62fec4..0000000 --- a/apps/wei/templates/wei/weiclub_form.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load crispy_forms_tags %} -{% load i18n %} - -{% block profile_content %} -
-

- {{ title }} -

-
-
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/weiclub_list.html b/apps/wei/templates/wei/weiclub_list.html deleted file mode 100644 index 1202a66..0000000 --- a/apps/wei/templates/wei/weiclub_list.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block content %} -
-
- - {% if can_create_wei %} -
- {% trans "Create WEI" %} - {% endif %} -
-
-
-
-
-
-
{% trans "WEI listing" %}
-
-
- {% render_table table %} -
-
-
-
- -{% endblock %} -{% block extrajavascript %} - -{% endblock %} diff --git a/apps/wei/templates/wei/weilist_sample.tex b/apps/wei/templates/wei/weilist_sample.tex deleted file mode 100644 index 820df64..0000000 --- a/apps/wei/templates/wei/weilist_sample.tex +++ /dev/null @@ -1,47 +0,0 @@ -\documentclass[a4paper,landscape,10pt]{article} - -\usepackage{fontspec} -\usepackage[margin=1.5cm]{geometry} -\usepackage{longtable} - -\begin{document} -\begin{center} -\huge{Liste des inscrits « {{ wei.name }} »} - -{% if bus %} -\LARGE{Bus {{ bus.name|safe }}} - - -{% if team %} -\Large{Équipe {{ team.name|safe }}} -{% endif %} -{% endif %} -\end{center} - -\begin{center} -\footnotesize -\begin{longtable}{ccccccccc} -\textbf{Nom} & \textbf{Prénom} & \textbf{Date de naissance} & \textbf{Genre} & \textbf{Section} - & \textbf{Bus} & \textbf{Équipe} & \textbf{Rôles} \\ -{% for membership in memberships %} -{{ membership.user.last_name|safe }} & {{ membership.user.first_name|safe }} & {{ membership.registration.birth_date|safe }} -& {{ membership.registration.get_gender_display|safe }} & {{ membership.user.profile.section_generated|safe }} & {{ membership.bus.name|safe }} -& {% if membership.team %}{{ membership.team.name|safe }}{% else %}--{% endif %} & {{ membership.roles.first|safe }} \\ -{% endfor %} -\end{longtable} -\end{center} - -\footnotesize -Section = Année à l'ENS + code du département - -\begin{center} -\begin{longtable}{ccccccccc} -\textbf{Code} & A0 & A1 & A2 & A'2 & A''2 & A3 & B1234 & B1 \\ -\textbf{Département} & Informatique & Maths & Physique & Physique appliquée & Chimie & Biologie & SAPHIRE & Mécanique \\ -\hline -\textbf{Code} & B2 & B3 & B4 & C & D2 & D3 & E & EXT \\ -\textbf{Département} & Génie civil & Génie mécanique & EEA & Design & Éco-gestion & Sciences sociales & Anglais & Extérieur -\end{longtable} -\end{center} - -\end{document} diff --git a/apps/wei/templates/wei/weimembership_form.html b/apps/wei/templates/wei/weimembership_form.html deleted file mode 100644 index 7d1059b..0000000 --- a/apps/wei/templates/wei/weimembership_form.html +++ /dev/null @@ -1,203 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n crispy_forms_tags pretty_money perms %} - -{% block profile_content %} -
-
-

{% trans "Review registration" %}

-
-
-
-
{% trans 'name'|capfirst %}, {% trans 'first name' %}
-
{{ registration.user.last_name }} {{ registration.user.first_name }}
- -
{% trans 'username'|capfirst %}
-
{{ registration.user.username }}
- -
{% trans 'email'|capfirst %}
-
{{ registration.user.email }}
- - {% if not registration.user.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:registration.user.profile %} -
-
- {% trans "This user doesn't have confirmed his/her e-mail address." %} - {% trans "Click here to resend a validation link." %} -
-
- {% endif %} - -
{% trans 'department'|capfirst %}
-
{{ registration.user.profile.department }}
- -
{% trans 'ENS year'|capfirst %}
-
{{ registration.user.profile.ens_year }}
- -
{% trans 'section'|capfirst %}
-
{{ registration.user.profile.section }}
- -
{% trans 'address'|capfirst %}
-
{{ registration.user.profile.address }}
- -
{% trans 'phone number'|capfirst %}
-
{{ registration.user.profile.phone_number }}
- -
{% trans 'paid'|capfirst %}
-
{{ registration.user.profile.paid|yesno }}
- -
- -
{% trans 'first year'|capfirst %}
-
{{ registration.first_year|yesno }}
- -
{% trans 'gender'|capfirst %}
-
{{ registration.get_gender_display }}
- -
{% trans 'clothing cut'|capfirst %}
-
{{ registration.clothing_cut }}
- -
{% trans 'clothing size'|capfirst %}
-
{{ registration.clothing_size }}
- -
{% trans 'birth date'|capfirst %}
-
{{ registration.birth_date }}
- -
{% trans 'health issues'|capfirst %}
-
{{ registration.health_issues }}
- -
{% trans 'emergency contact name'|capfirst %}
-
{{ registration.emergency_contact_name }}
- -
{% trans 'emergency contact phone'|capfirst %}
-
{{ registration.emergency_contact_phone }}
- -
{% trans 'Payment from Société générale' %}
-
{{ registration.soge_credit|yesno }}
- - {% if registration.first_year %} -
{% trans 'Suggested bus from the survey:' %}
- {% if registration.information.valid or True %} -
{{ suggested_bus }}
- -
-
{% trans 'Raw survey information' %}
-
- - {% with information=registration.information %} - {% for key, value in information.items %} -
{{ key }}
-
{{ value }}
- {% endfor %} - {% endwith %} - {% else %} -
{% trans "The algorithm didn't run." %}
- {% endif %} - {% else %} -
{% trans 'caution check given'|capfirst %}
-
{{ registration.caution_check|yesno }}
- - {% with information=registration.information %} -
{% trans 'preferred bus'|capfirst %}
-
{{ information.preferred_bus_name|join:', ' }}
- -
{% trans 'preferred team'|capfirst %}
-
{{ information.preferred_team_name|join:', ' }}
- -
{% trans 'preferred roles'|capfirst %}
-
{{ information.preferred_roles_name|join:', ' }}
- {% endwith %} - {% endif %} -
-
- -
- -
- -
-
-
-

{% trans "Validate registration" %}

-
- {% if registration.is_validated %} -
- {% trans "The registration is already validated and can't be unvalidated." %} - {% trans "The user joined the bus" %} {{ registration.membership.bus }} - {% if registration.membership.team %}{% trans "in the team" %} {{ registration.membership.team }}, - {% else %}{% trans "in no team (staff)" %},{% endif %} {% trans "with the following roles:" %} {{ registration.membership.roles.all|join:", " }} -
- {% else %} - {% if registration.soge_credit %} -
- {% blocktrans trimmed %} - The WEI will be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet. - The membership transaction will be created but will be invalid. You will have to validate it once the bank - validated the creation of the account, or to change the payment method. - {% endblocktrans %} -
- {% else %} - {% if registration.user.note.balance < fee %} -
- {% with pretty_fee=fee|pretty_money %} - {% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %} - The note don't have enough money ({{ balance }}, {{ pretty_fee }} required). - The registration may fail if you don't credit the note now. - {% endblocktrans %} - {% endwith %} -
- {% else %} -
- {% blocktrans trimmed with pretty_fee=fee|pretty_money %} - The note has enough money ({{ pretty_fee }} required), the registration is possible. - {% endblocktrans %} -
- {% endif %} - {% endif %} - - {% if not registration.caution_check and not registration.first_year %} -
- {% trans "The user didn't give her/his caution check." %} -
- {% endif %} - - {% if not kfet_member %} -
- {% url 'registration:future_user_detail' pk=registration.user.pk as future_user_detail %} - {% url 'member:club_detail' pk=club.parent_club.parent_club.pk as club_detail %} - {% blocktrans trimmed %} - This user is not a member of the Kfet club for the coming year. The membership will be - processed automatically, the WEI registration includes the membership fee. - {% endblocktrans %} -
- {% endif %} - -
- {% csrf_token %} - {{ form|crispy }} -
- - {% endif %} -
-
-{% endblock %} - -{% block extrajavascript %} - -{% endblock %} diff --git a/apps/wei/templates/wei/weimembership_list.html b/apps/wei/templates/wei/weimembership_list.html deleted file mode 100644 index fe9506b..0000000 --- a/apps/wei/templates/wei/weimembership_list.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load render_table from django_tables2 %} - -{% block profile_content %} -
-
- -
- -
- {% if table.data %} - {% render_table table %} - {% else %} -
- {% trans "There is no membership found with this pattern." %} -
- {% endif %} -
-
- - -
-{% endblock %} - -{% block extrajavascript %} - -{% endblock %} diff --git a/apps/wei/templates/wei/weiregistration_confirm_delete.html b/apps/wei/templates/wei/weiregistration_confirm_delete.html deleted file mode 100644 index f1d2e88..0000000 --- a/apps/wei/templates/wei/weiregistration_confirm_delete.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load crispy_forms_tags %} - -{% block profile_content %} -
-
-

{% trans "Delete registration" %}

-
- {% if object.is_validated %} -
-
- {% blocktrans %}This registration is already validated and can't be deleted.{% endblocktrans %} -
-
- {% else %} -
-
- {% with user=object.user wei_name=object.wei.name %} - {% blocktrans %}Are you sure you want to delete the registration of {{ user }} for the WEI {{ wei_name }}? This action can't be undone.{% endblocktrans %} - {% endwith %} -
-
- - {% endif %} -
-{% endblock %} diff --git a/apps/wei/templates/wei/weiregistration_form.html b/apps/wei/templates/wei/weiregistration_form.html deleted file mode 100644 index fae85e0..0000000 --- a/apps/wei/templates/wei/weiregistration_form.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load crispy_forms_tags %} - -{% block profile_content %} -
-

- {{ title }} -

-
-
- {% csrf_token %} - {{ form|crispy }} - {{ membership_form|crispy }} - -
-
-
-{% endblock %} - -{% block extrajavascript %} -{% if not object.membership %} - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/apps/wei/templates/wei/weiregistration_list.html b/apps/wei/templates/wei/weiregistration_list.html deleted file mode 100644 index c0c1e32..0000000 --- a/apps/wei/templates/wei/weiregistration_list.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "wei/base.html" %} -{% comment %} -SPDX-License-Identifier: GPL-3.0-or-later -{% endcomment %} -{% load i18n %} -{% load render_table from django_tables2 %} - -{% block profile_content %} -
-
- -
- -
- {% if table.data %} - {% render_table table %} - {% else %} -
- {% trans "There is no pre-registration found with this pattern." %} -
- {% endif %} -
-
- - -
-{% endblock %} - -{% block extrajavascript %} - -{% endblock %} diff --git a/apps/wei/tests/__init__.py b/apps/wei/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/wei/tests/test_wei_algorithm_2021.py b/apps/wei/tests/test_wei_algorithm_2021.py deleted file mode 100644 index 5320712..0000000 --- a/apps/wei/tests/test_wei_algorithm_2021.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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', - year=2021, - ) - - 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 % diff --git a/apps/wei/tests/test_wei_algorithm_2022.py b/apps/wei/tests/test_wei_algorithm_2022.py deleted file mode 100644 index 2d358db..0000000 --- a/apps/wei/tests/test_wei_algorithm_2022.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (C) 2018-2022 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.wei2022 import WEIBusInformation2022, WEISurvey2022, WORDS, WEISurveyInformation2022 -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 2022", - email="wei2022@example.com", - date_start='2022-09-16', - date_end='2022-09-18', - year=2022, - ) - - 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 = WEIBusInformation2022(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 = WEISurveyInformation2022(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 - WEISurvey2022.get_algorithm_class()().run_algorithm() - - # Ensure that everyone has its first choice - for r in WEIRegistration.objects.filter(wei=self.wei).all(): - survey = WEISurvey2022(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 = WEISurveyInformation2022(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 - WEISurvey2022.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 = WEISurvey2022(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 % diff --git a/apps/wei/tests/test_wei_registration.py b/apps/wei/tests/test_wei_registration.py deleted file mode 100644 index ef285f4..0000000 --- a/apps/wei/tests/test_wei_registration.py +++ /dev/null @@ -1,879 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -import subprocess -from datetime import timedelta, date - -from api.tests import TestAPI -from django.conf import settings -from django.contrib.auth.models import User -from django.db.models import Q -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone -from member.models import Membership, Club -from note.models import NoteClub, SpecialTransaction, NoteUser -from treasury.models import SogeCredit - -from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \ - WEIRoleViewSet -from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey -from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership - - -class TestWEIList(TestCase): - fixtures = ('initial',) - - def setUp(self): - self.user = User.objects.create_superuser( - username="weiadmin", - password="admin", - email="admin@example.com", - ) - self.client.force_login(self.user) - sess = self.client.session - sess["permission_mask"] = 42 - sess.save() - - def test_current_wei_detail(self): - """ - Test that when no WEI is created, the WEI button redirect to the WEI list - """ - response = self.client.get(reverse("wei:current_wei_detail")) - self.assertRedirects(response, reverse("wei:wei_list"), 302, 200) - - -class TestWEIRegistration(TestCase): - """ - Test the whole WEI app - """ - fixtures = ('initial',) - - def setUp(self): - """ - Setup the database with initial data - Create a new user, a new WEI, bus, team, registration - """ - 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.year = timezone.now().year - self.wei = WEIClub.objects.create( - name="Test WEI", - email="gc.wei@example.com", - parent_club_id=2, - membership_fee_paid=12500, - membership_fee_unpaid=5500, - membership_start=date(self.year, 1, 1), - membership_end=date(self.year, 12, 31), - year=self.year, - date_start=date.today() + timedelta(days=2), - date_end=date(self.year, 12, 31), - ) - NoteClub.objects.create(club=self.wei) - self.bus = Bus.objects.create( - name="Test Bus", - wei=self.wei, - description="Test Bus", - ) - - # Setup the bus - bus_info = CurrentSurvey.get_algorithm_class().get_bus_information(self.bus) - bus_info.scores["Jus de fruit"] = 70 - bus_info.save() - self.bus.save() - - self.team = BusTeam.objects.create( - name="Test Team", - bus=self.bus, - color=0xFFFFFF, - description="Test Team", - ) - self.registration = WEIRegistration.objects.create( - user_id=self.user.id, - wei_id=self.wei.id, - soge_credit=True, - caution_check=True, - birth_date=date(2000, 1, 1), - gender="nonbinary", - clothing_cut="male", - clothing_size="XL", - health_issues="I am a bot", - emergency_contact_name="Pikachu", - emergency_contact_phone="+33123456789", - first_year=False, - ) - - def test_create_wei(self): - """ - Test creating a new WEI club. - """ - response = self.client.post(reverse("wei:wei_create"), dict( - name="Create WEI Test", - email="gc.wei@example.com", - membership_fee_paid=12500, - membership_fee_unpaid=5500, - membership_start=str(self.year + 1) + "-08-01", - membership_end=str(self.year + 1) + "-09-30", - year=self.year + 1, - date_start=str(self.year + 1) + "-09-01", - date_end=str(self.year + 1) + "-09-03", - )) - qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1) - self.assertTrue(qs.exists()) - wei = qs.get() - self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=wei.pk)), 302, 200) - - def test_wei_detail(self): - """ - Test display the information about the default WEI. - """ - response = self.client.get(reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - def test_current_wei_detail(self): - """ - Test display the information about the current WEI. - """ - response = self.client.get(reverse("wei:current_wei_detail")) - self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_update_wei(self): - """ - Test update the information about the default WEI. - """ - response = self.client.post(reverse("wei:wei_update", kwargs=dict(pk=self.wei.pk)), dict( - name="Update WEI Test", - year=2000, - email="wei-updated@example.com", - membership_fee_paid=0, - membership_fee_unpaid=0, - membership_start="2000-08-01", - membership_end="2000-09-30", - date_start="2000-09-01", - date_end="2000-09-03", - )) - qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id) - self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200) - self.assertTrue(qs.exists()) - - # Check that if the WEI is started, we can't update a wei - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:wei_update", kwargs=dict(pk=self.wei.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_wei_closed(self): - """ - Test display the page when a WEI is closed. - """ - response = self.client.get(reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - def test_wei_list(self): - """ - Test display the list of all WEI. - """ - response = self.client.get(reverse("wei:wei_list")) - self.assertEqual(response.status_code, 200) - - def test_add_bus(self): - """ - Test create a new bus. - """ - response = self.client.get(reverse("wei:add_bus", kwargs=dict(pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.post(reverse("wei:add_bus", kwargs=dict(pk=self.wei.pk)), dict( - wei=self.wei.id, - name="Create Bus Test", - size=50, - description="This bus was created.", - information_json="{}", - )) - qs = Bus.objects.filter(name="Create Bus Test") - self.assertTrue(qs.exists()) - bus = qs.get() - CurrentSurvey.get_algorithm_class().get_bus_information(bus).save() - self.assertRedirects(response, reverse("wei:manage_bus", kwargs=dict(pk=bus.pk)), 302, 200) - - # Check that if the WEI is started, we can't create a bus - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:add_bus", kwargs=dict(pk=self.wei.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_detail_bus(self): - """ - Test display the information about a bus. - """ - response = self.client.get(reverse("wei:manage_bus", kwargs=dict(pk=self.bus.pk))) - self.assertEqual(response.status_code, 200) - - def test_update_bus(self): - """ - Test update a bus. - """ - response = self.client.get(reverse("wei:update_bus", kwargs=dict(pk=self.bus.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.post(reverse("wei:update_bus", kwargs=dict(pk=self.bus.pk)), dict( - name="Update Bus Test", - size=40, - description="This bus was updated.", - information_json="{}", - )) - qs = Bus.objects.filter(name="Update Bus Test", id=self.bus.id) - self.assertRedirects(response, reverse("wei:manage_bus", kwargs=dict(pk=self.bus.pk)), 302, 200) - self.assertTrue(qs.exists()) - - # Check that if the WEI is started, we can't update a bus - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:update_bus", kwargs=dict(pk=self.bus.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_add_team(self): - """ - Test create a new team. - """ - response = self.client.get(reverse("wei:add_team", kwargs=dict(pk=self.bus.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.post(reverse("wei:add_team", kwargs=dict(pk=self.bus.pk)), dict( - bus=self.bus.id, - name="Create Team Test", - color="#2A", - description="This team was created.", - )) - qs = BusTeam.objects.filter(name="Create Team Test", color=42) - self.assertTrue(qs.exists()) - team = qs.get() - self.assertRedirects(response, reverse("wei:manage_bus_team", kwargs=dict(pk=team.pk)), 302, 200) - - # Check that if the WEI is started, we can't create a team - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:add_team", kwargs=dict(pk=self.bus.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_detail_team(self): - """ - Test display the detail about a team. - """ - response = self.client.get(reverse("wei:manage_bus_team", kwargs=dict(pk=self.team.pk))) - self.assertEqual(response.status_code, 200) - - def test_update_team(self): - """ - Test update a team. - """ - response = self.client.get(reverse("wei:update_bus_team", kwargs=dict(pk=self.team.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.post(reverse("wei:update_bus_team", kwargs=dict(pk=self.team.pk)), dict( - name="Update Team Test", - color="#A6AA", - description="This team was updated.", - )) - qs = BusTeam.objects.filter(name="Update Team Test", color=42666, id=self.team.id) - self.assertRedirects(response, reverse("wei:manage_bus_team", kwargs=dict(pk=self.team.pk)), 302, 200) - self.assertTrue(qs.exists()) - - # Check that if the WEI is started, we can't update a team - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:update_bus_team", kwargs=dict(pk=self.team.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_register_2a(self): - """ - Test register a new 2A+ to the WEI. - """ - response = self.client.get(reverse("wei:wei_register_2A", 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) - - # Try with an invalid form - response = self.client.post(reverse("wei:wei_register_2A", 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', - bus=[], - team=[], - roles=[], - )) - self.assertEqual(response.status_code, 200) - self.assertFalse(response.context["membership_form"].is_valid()) - - response = self.client.post(reverse("wei:wei_register_2A", 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', - bus=[self.bus.id], - team=[self.team.id], - roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], - )) - qs = WEIRegistration.objects.filter(user_id=user.id) - self.assertTrue(qs.exists()) - self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=qs.get().pk)), 302, 302) - - # Check that the user can't be registered twice - response = self.client.post(reverse("wei:wei_register_2A", 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', - bus=[self.bus.id], - team=[self.team.id], - roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], - )) - self.assertEqual(response.status_code, 200) - self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors)) - - # Test the render of the page to register ourself if we have already opened a Société générale account - SogeCredit.objects.create(user=self.user, credit_transaction=SpecialTransaction.objects.create( - source_id=4, # Bank transfer - destination=self.user.note, - quantity=1, - amount=0, - reason="Test", - first_name="toto", - last_name="toto", - bank="Société générale", - )) - response = self.client.get(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - # Check that if the WEI is started, we can't register anyone - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - 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 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( - 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', - )) - self.assertEqual(response.status_code, 200) - self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors)) - - # Check that the user can't be registered twice as a first year member - second_wei = WEIClub.objects.create( - name="Second WEI", - year=self.year + 1, - date_start=str(self.year + 1) + "-01-01", - date_end=str(self.year + 1) + "-12-31", - membership_start=str(self.year) + "-01-01", - membership_end=str(self.year + 1) + "-12-31", - ) - response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=second_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', - )) - 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." - in str(response.context["form"].errors)) - - # Check that if the WEI is started, we can't register anyone - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - response = self.client.get(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_register_myself(self): - """ - Try to register myself to the WEI, and check redirections. - """ - response = self.client.get(reverse('wei:wei_register_1A_myself', args=(self.wei.pk,))) - self.assertRedirects(response, reverse('wei:wei_update_registration', args=(self.registration.pk,))) - - response = self.client.get(reverse('wei:wei_register_2A_myself', args=(self.wei.pk,))) - self.assertRedirects(response, reverse('wei:wei_update_registration', args=(self.registration.pk,))) - - self.registration.delete() - - response = self.client.get(reverse('wei:wei_register_1A_myself', args=(self.wei.pk,))) - self.assertEqual(response.status_code, 200) - - response = self.client.get(reverse('wei:wei_register_2A_myself', args=(self.wei.pk,))) - self.assertEqual(response.status_code, 200) - - def test_wei_survey_ended(self): - """ - Test display the end page of a survey. - """ - response = self.client.get(reverse("wei:wei_survey_end", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 200) - - def test_update_registration(self): - """ - Test update a registration. - """ - self.registration.information = dict( - preferred_bus_pk=[], - preferred_team_pk=[], - preferred_roles_pk=[] - ) - self.registration.save() - - response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.post( - reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)), - dict( - user=self.user.id, - soge_credit=False, - birth_date='2020-01-01', - gender='female', - clothing_cut='male', - clothing_size='M', - health_issues='I am really a bot', - emergency_contact_name='Note Kfet 2020', - emergency_contact_phone='+33600000000', - bus=[self.bus.id], - team=[self.team.id], - roles=[role.id for role in WEIRole.objects.filter(name="Adhérent WEI").all()], - information_json=self.registration.information_json, - ) - ) - qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M") - self.assertTrue(qs.exists()) - self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200) - - # Check the page when the registration is already validated - membership = WEIMembership( - user=self.user, - club=self.wei, - registration=self.registration, - bus=self.bus, - team=self.team, - ) - membership._soge = True - membership._force_renew_parent = True - membership.save() - soge_credit = SogeCredit.objects.get(user=self.user) - soge_credit.credit_transaction = SpecialTransaction.objects.create( - source_id=4, # Bank transfer - destination=self.user.note, - quantity=1, - amount=0, - reason="Test", - first_name="toto", - last_name="toto", - bank="Société générale", - ) - soge_credit.save() - - sess = self.client.session - sess["permission_mask"] = 0 - sess.save() - response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 403) - sess["permission_mask"] = 42 - sess.save() - - response = self.client.post( - reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)), - dict( - user=self.user.id, - soge_credit=False, - birth_date='2015-01-01', - gender='male', - clothing_cut='female', - clothing_size='L', - health_issues='I am really a bot', - emergency_contact_name='Note Kfet 2020', - emergency_contact_phone='+33600000000', - bus=[self.bus.id], - team=[self.team.id], - roles=[role.id for role in WEIRole.objects.filter(name="Adhérent WEI").all()], - information_json=self.registration.information_json, - ) - ) - qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L") - self.assertTrue(qs.exists()) - self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200) - - # Test invalid form - response = self.client.post( - reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)), - dict( - user=self.user.id, - soge_credit=False, - birth_date='2015-01-01', - gender='male', - clothing_cut='female', - clothing_size='L', - health_issues='I am really a bot', - emergency_contact_name='Note Kfet 2020', - emergency_contact_phone='+33600000000', - bus=[], - team=[], - roles=[], - information_json=self.registration.information_json, - ) - ) - self.assertFalse(response.context["membership_form"].is_valid()) - - # Check that if the WEI is started, we can't update a registration - self.wei.date_start = date(2000, 1, 1) - self.wei.update_membership_dates() - self.wei.save() - response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_delete_registration(self): - """ - Test delete a WEI registration. - """ - response = self.client.get(reverse("wei:wei_delete_registration", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.delete(reverse("wei:wei_delete_registration", kwargs=dict(pk=self.registration.pk))) - self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_validate_membership(self): - """ - Test validate a membership. - """ - response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 200) - - self.registration.first_year = True - self.registration.save() - - response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk))) - self.assertEqual(response.status_code, 200) - - self.registration.first_year = False - self.registration.save() - - # Check that a team must belong to the bus - second_bus = Bus.objects.create(wei=self.wei, name="Second bus") - second_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42) - response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( - roles=[WEIRole.objects.get(name="GC WEI").id], - bus=self.bus.pk, - team=second_team.pk, - credit_type=4, # Bank transfer - credit_amount=420, - last_name="admin", - first_name="admin", - bank="Société générale", - )) - self.assertEqual(response.status_code, 200) - self.assertFalse(response.context["form"].is_valid()) - 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( - roles=[WEIRole.objects.get(name="GC WEI").id], - bus=self.bus.pk, - team=self.team.pk, - credit_type=4, # Bank transfer - credit_amount=420, - last_name="admin", - 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) - # Check if the membership is successfully created - membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei) - self.assertTrue(membership.exists()) - membership = membership.get() - # Check if the user is member of the Kfet club and the BDE - kfet_membership = Membership.objects.filter(user_id=self.user.id, club__name="Kfet") - self.assertTrue(kfet_membership.exists()) - kfet_membership = kfet_membership.get() - bde_membership = Membership.objects.filter(user_id=self.user.id, club__name="BDE") - self.assertTrue(bde_membership.exists()) - bde_membership = bde_membership.get() - - if "treasury" in settings.INSTALLED_APPS: - # The registration is made with the Société générale. Ensure that all is fine - from treasury.models import SogeCredit - soge_credit = SogeCredit.objects.filter(user_id=self.user.id) - self.assertTrue(soge_credit.exists()) - soge_credit = soge_credit.get() - self.assertTrue(membership.transaction in soge_credit.transactions.all()) - self.assertTrue(kfet_membership.transaction in soge_credit.transactions.all()) - self.assertTrue(bde_membership.transaction in soge_credit.transactions.all()) - self.assertFalse(membership.transaction.valid) - self.assertFalse(kfet_membership.transaction.valid) - self.assertFalse(bde_membership.transaction.valid) - - # Check that if the WEI is started, we can't update a wei - self.wei.date_start = date(2000, 1, 1) - self.wei.save() - response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk))) - self.assertRedirects(response, reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)), 302, 200) - - def test_registrations_list(self): - """ - Test display the registration list, with or without a research - """ - response = self.client.get(reverse("wei:wei_registrations", kwargs=dict(pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.get(reverse("wei:wei_registrations", kwargs=dict(pk=self.wei.pk)) + "?search=.") - self.assertEqual(response.status_code, 200) - - def test_memberships_list(self): - """ - Test display the memberships list, with or without a research - """ - response = self.client.get(reverse("wei:wei_memberships", kwargs=dict(pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - - response = self.client.get(reverse("wei:wei_memberships", kwargs=dict(pk=self.wei.pk)) + "?search=.") - self.assertEqual(response.status_code, 200) - - def is_latex_installed(self): - """ - Check if LaTeX is installed in the machine. Don't check pages that generate a PDF file if LaTeX is not - installed, like in Gitlab. - """ - with open("/dev/null", "wb") as devnull: - return subprocess.call( - ["/usr/bin/which", "xelatex"], - stdout=devnull, - stderr=devnull, - ) == 0 - - def test_memberships_pdf_list(self): - """ - Test display the membership list as a PDF file - """ - if self.is_latex_installed(): - response = self.client.get(reverse("wei:wei_memberships_pdf", kwargs=dict(wei_pk=self.wei.pk))) - self.assertEqual(response.status_code, 200) - self.assertEqual(response["content-type"], "application/pdf") - - def test_bus_memberships_pdf_list(self): - """ - Test display the membership list of a bus as a PDF file - """ - if self.is_latex_installed(): - response = self.client.get(reverse("wei:wei_memberships_bus_pdf", kwargs=dict(wei_pk=self.wei.pk, - bus_pk=self.bus.pk))) - self.assertEqual(response.status_code, 200) - self.assertEqual(response["content-type"], "application/pdf") - - def test_team_memberships_pdf_list(self): - """ - Test display the membership list of a bus team as a PDF file - """ - if self.is_latex_installed(): - response = self.client.get(reverse("wei:wei_memberships_team_pdf", kwargs=dict(wei_pk=self.wei.pk, - bus_pk=self.bus.pk, - team_pk=self.team.pk))) - self.assertEqual(response.status_code, 200) - self.assertEqual(response["content-type"], "application/pdf") - - -class TestDefaultWEISurvey(TestCase): - """ - Doesn't test anything, just cover the default Survey classes. - """ - def check_not_implemented(self, fun: callable, *args, **kwargs): - self.assertRaises(NotImplementedError, fun, *args, **kwargs) - - def test_survey_classes(self): - WEISurveyAlgorithm.get_bus_information_class() - self.check_not_implemented(WEISurveyAlgorithm.get_survey_class) - self.check_not_implemented(WEISurveyAlgorithm.get_registrations) - self.check_not_implemented(WEISurveyAlgorithm.get_buses) - self.check_not_implemented(WEISurveyAlgorithm().run_algorithm) - - self.check_not_implemented(WEISurvey, registration=None) - self.check_not_implemented(WEISurvey.get_wei) - self.check_not_implemented(WEISurvey.get_survey_information_class) - self.check_not_implemented(WEISurvey.get_algorithm_class) - self.check_not_implemented(WEISurvey.get_form_class, None) - self.check_not_implemented(WEISurvey.form_valid, None, None) - self.check_not_implemented(WEISurvey.is_complete, None) - # noinspection PyTypeChecker - WEISurvey.update_form(None, None) - - self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) - self.assertEqual(CurrentSurvey.get_year(), 2022) - - -class TestWeiAPI(TestAPI): - def setUp(self) -> None: - super().setUp() - - self.year = timezone.now().year - self.wei = WEIClub.objects.create( - name="Test WEI", - email="gc.wei@example.com", - parent_club_id=2, - membership_fee_paid=12500, - membership_fee_unpaid=5500, - membership_start=date(self.year, 1, 1), - membership_end=date(self.year, 12, 31), - membership_duration=396, - year=self.year, - date_start=date.today() + timedelta(days=2), - date_end=date(self.year, 12, 31), - ) - NoteClub.objects.create(club=self.wei) - self.bus = Bus.objects.create( - name="Test Bus", - wei=self.wei, - description="Test Bus", - ) - self.team = BusTeam.objects.create( - name="Test Team", - bus=self.bus, - color=0xFFFFFF, - description="Test Team", - ) - self.registration = WEIRegistration.objects.create( - user_id=self.user.id, - wei_id=self.wei.id, - soge_credit=True, - caution_check=True, - birth_date=date(2000, 1, 1), - gender="nonbinary", - clothing_cut="male", - clothing_size="XL", - health_issues="I am a bot", - emergency_contact_name="Pikachu", - emergency_contact_phone="+33123456789", - first_year=False, - ) - Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) - Membership.objects.create(user=self.user, club=Club.objects.get(name="Kfet")) - self.membership = WEIMembership.objects.create( - user=self.user, - club=self.wei, - fee=125, - bus=self.bus, - team=self.team, - registration=self.registration, - ) - self.membership.roles.add(WEIRole.objects.last()) - self.membership.save() - - def test_weiclub_api(self): - """ - Load WEI API page and test all filters and permissions - """ - self.check_viewset(WEIClubViewSet, "/api/wei/club/") - - def test_wei_bus_api(self): - """ - Load Bus API page and test all filters and permissions - """ - self.check_viewset(BusViewSet, "/api/wei/bus/") - - def test_wei_team_api(self): - """ - Load BusTeam API page and test all filters and permissions - """ - self.check_viewset(BusTeamViewSet, "/api/wei/team/") - - def test_weirole_api(self): - """ - Load WEIRole API page and test all filters and permissions - """ - self.check_viewset(WEIRoleViewSet, "/api/wei/role/") - - def test_weiregistration_api(self): - """ - Load WEIRegistration API page and test all filters and permissions - """ - self.check_viewset(WEIRegistrationViewSet, "/api/wei/registration/") - - def test_weimembership_api(self): - """ - Load WEIMembership API page and test all filters and permissions - """ - self.check_viewset(WEIMembershipViewSet, "/api/wei/membership/") diff --git a/apps/wei/urls.py b/apps/wei/urls.py deleted file mode 100644 index fb49721..0000000 --- a/apps/wei/urls.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.urls import path - -from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \ - WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \ - BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \ - WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \ - WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView - -app_name = 'wei' -urlpatterns = [ - path('detail/', CurrentWEIDetailView.as_view(), name="current_wei_detail"), - path('list/', WEIListView.as_view(), name="wei_list"), - path('create/', WEICreateView.as_view(), name="wei_create"), - path('detail//', WEIDetailView.as_view(), name="wei_detail"), - path('update//', WEIUpdateView.as_view(), name="wei_update"), - path('detail//registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"), - path('detail//memberships/', WEIMembershipsView.as_view(), name="wei_memberships"), - path('detail//memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"), - path('detail//memberships/pdf//', MemberListRenderView.as_view(), - name="wei_memberships_bus_pdf"), - path('detail//memberships/pdf///', MemberListRenderView.as_view(), - name="wei_memberships_team_pdf"), - path('bus-1A/list//', WEI1AListView.as_view(), name="wei_1A_list"), - path('add-bus//', BusCreateView.as_view(), name="add_bus"), - path('manage-bus//', BusManageView.as_view(), name="manage_bus"), - path('update-bus//', BusUpdateView.as_view(), name="update_bus"), - path('add-bus-team//', BusTeamCreateView.as_view(), name="add_team"), - path('manage-bus-team//', BusTeamManageView.as_view(), name="manage_bus_team"), - path('update-bus-team//', BusTeamUpdateView.as_view(), name="update_bus_team"), - path('register//1A/', WEIRegister1AView.as_view(), name="wei_register_1A"), - path('register//2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"), - path('register//1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"), - path('register//2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"), - path('edit-registration//', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"), - path('delete-registration//', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"), - path('validate//', WEIValidateRegistrationView.as_view(), name="validate_registration"), - path('survey//', WEISurveyView.as_view(), name="wei_survey"), - path('survey//end/', WEISurveyEndView.as_view(), name="wei_survey_end"), - path('detail//closed/', WEIClosedView.as_view(), name="wei_closed"), - path('bus-1A//', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"), - path('bus-1A/next//', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"), -] diff --git a/apps/wei/views.py b/apps/wei/views.py deleted file mode 100644 index 80ff770..0000000 --- a/apps/wei/views.py +++ /dev/null @@ -1,1241 +0,0 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -import os -import shutil -import subprocess -from datetime import date, timedelta -from tempfile import mkdtemp - -from django.conf import settings -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied -from django.db import transaction -from django.db.models import Q, Count -from django.db.models.functions.text import Lower -from django.http import HttpResponse, Http404 -from django.shortcuts import redirect -from django.template.loader import render_to_string -from django.urls import reverse_lazy -from django.views import View -from django.views.generic import DetailView, UpdateView, RedirectView, TemplateView -from django.utils.translation import gettext_lazy as _ -from django.views.generic.edit import BaseFormView, DeleteView -from django_tables2 import SingleTableView -from member.models import Membership, Club -from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial -from note.tables import HistoryTable -from note_kfet.settings import BASE_DIR -from permission.backends import PermissionBackend -from permission.views import ProtectQuerysetMixin, ProtectedCreateView - -from .forms.registration import WEIChooseBusForm -from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole -from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \ - WEIMembershipForm, CurrentSurvey -from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \ - WEIRegistration1ATable, WEIMembershipTable - - -class CurrentWEIDetailView(LoginRequiredMixin, RedirectView): - def get_redirect_url(self, *args, **kwargs): - wei = WEIClub.objects.filter(membership_start__lte=date.today()).order_by('date_start') - if wei.exists(): - wei = wei.last() - return reverse_lazy('wei:wei_detail', args=(wei.pk,)) - else: - return reverse_lazy('wei:wei_list') - - -class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): - """ - List existing WEI - """ - model = WEIClub - table_class = WEITable - ordering = '-year' - extra_context = {"title": _("Search WEI")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["can_create_wei"] = PermissionBackend.check_perm(self.request, "wei.add_weiclub", WEIClub( - name="", - email="weiclub@example.com", - year=0, - date_start=date.today(), - date_end=date.today(), - )) - return context - - -class WEICreateView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Create WEI - """ - - model = WEIClub - form_class = WEIForm - extra_context = {"title": _("Create WEI")} - - def get_sample_object(self): - return WEIClub( - name="", - email="weiclub@example.com", - year=0, - date_start=date.today(), - date_end=date.today(), - ) - - @transaction.atomic - def form_valid(self, form): - form.instance.requires_membership = True - form.instance.parent_club = Club.objects.get(name="Kfet") - ret = super().form_valid(form) - NoteClub.objects.create(club=form.instance) - return ret - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk}) - - -class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): - """ - View WEI information - """ - model = WEIClub - context_object_name = "club" - extra_context = {"title": _("WEI Detail")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - club = context["club"] - - club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \ - .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \ - .order_by('-created_at', '-id') - history_table = HistoryTable(club_transactions, prefix="history-") - history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) - context['history_list'] = history_table - - club_member = WEIMembership.objects.filter( - club=club, - date_end__gte=date.today(), - ).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view")) - membership_table = WEIMembershipTable(data=club_member, prefix="membership-") - membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) - context['member_list'] = membership_table - - pre_registrations = WEIRegistration.objects.filter( - PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter( - membership=None, - wei=club - ) - pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-") - pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('pre-registration-page', 1)) - context['pre_registrations'] = pre_registrations_table - - my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user) - if my_registration.exists(): - my_registration = my_registration.get() - else: - my_registration = None - context["my_registration"] = my_registration - - buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \ - .filter(wei=self.object).annotate(count=Count("memberships")).order_by("name") - bus_table = BusTable(data=buses, prefix="bus-") - context['buses'] = bus_table - - 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) - - empty_bus = Bus( - wei=club, - name="", - ) - context["can_add_bus"] = PermissionBackend.check_perm(self.request, "wei.add_bus", empty_bus) - - context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists() - - qs = WEIMembership.objects.filter(club=club, registration__first_year=True, bus__isnull=True) - context["can_validate_1a"] = PermissionBackend.check_perm( - self.request, "wei.change_weimembership_bus", qs.first()) if qs.exists() else False - - return context - - -class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): - """ - List all WEI memberships - """ - model = WEIMembership - table_class = WEIMembershipTable - extra_context = {"title": _("View members of the WEI")} - - def dispatch(self, request, *args, **kwargs): - self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def get_queryset(self, **kwargs): - qs = super().get_queryset(**kwargs).filter(club=self.club).distinct() - - pattern = self.request.GET.get("search", "") - - if not pattern: - return qs.none() - - qs = qs.filter( - Q(user__first_name__iregex=pattern) - | Q(user__last_name__iregex=pattern) - | Q(user__note__alias__name__iregex="^" + pattern) - | Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) - | Q(bus__name__iregex=pattern) - | Q(team__name__iregex=pattern) - ) - - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.club - context["title"] = _("Find WEI Membership") - return context - - -class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): - """ - List all non-validated WEI registrations. - """ - model = WEIRegistration - table_class = WEIRegistrationTable - extra_context = {"title": _("View registrations to the WEI")} - - def dispatch(self, request, *args, **kwargs): - self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def get_queryset(self, **kwargs): - qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() - - pattern = self.request.GET.get("search", "") - - if pattern: - qs = qs.filter( - Q(user__first_name__iregex=pattern) - | Q(user__last_name__iregex=pattern) - | Q(user__note__alias__name__iregex="^" + pattern) - | Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) - ) - - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.club - context["title"] = _("Find WEI Registration") - return context - - -class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): - """ - Update the information of the WEI. - """ - model = WEIClub - context_object_name = "club" - form_class = WEIForm - extra_context = {"title": _("Update the WEI")} - - def dispatch(self, request, *args, **kwargs): - wei = self.get_object() - today = date.today() - # We can't update a past WEI - # But we can update it while it is not officially opened - if today > wei.date_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk}) - - -class BusCreateView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Create Bus - """ - model = Bus - form_class = BusForm - extra_context = {"title": _("Create new bus")} - - def get_sample_object(self): - wei = WEIClub.objects.get(pk=self.kwargs["pk"]) - return Bus( - wei=wei, - name="", - ) - - def dispatch(self, request, *args, **kwargs): - wei = WEIClub.objects.get(pk=self.kwargs["pk"]) - today = date.today() - # We can't add a bus once the WEI is started - if today >= wei.date_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"]) - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["wei"].initial = WEIClub.objects.get(pk=self.kwargs["pk"]) - return form - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk}) - - -class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): - """ - Update Bus - """ - model = Bus - form_class = BusForm - extra_context = {"title": _("Update bus")} - - def dispatch(self, request, *args, **kwargs): - wei = self.get_object().wei - today = date.today() - # We can't update a bus once the WEI is started - if today >= wei.date_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.wei - context["information"] = CurrentSurvey.get_algorithm_class().get_bus_information(self.object) - self.object.save() - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["wei"].disabled = True - return form - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk}) - - -class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): - """ - Manage Bus - """ - model = Bus - extra_context = {"title": _("Manage bus")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.wei - - bus = self.object - teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request, BusTeam, "view")) \ - .filter(bus=bus).annotate(count=Count("memberships")).order_by("name") - teams_table = BusTeamTable(data=teams, prefix="team-") - context["teams"] = teams_table - - memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( - self.request, WEIMembership, "view")).filter(bus=bus) - memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") - memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) - context["memberships"] = memberships_table - - return context - - -class BusTeamCreateView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Create BusTeam - """ - model = BusTeam - form_class = BusTeamForm - extra_context = {"title": _("Create new team")} - - def get_sample_object(self): - bus = Bus.objects.get(pk=self.kwargs["pk"]) - return BusTeam( - name="", - bus=bus, - color=0, - ) - - def dispatch(self, request, *args, **kwargs): - wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"]) - today = date.today() - # We can't add a team once the WEI is started - if today >= wei.date_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - bus = Bus.objects.get(pk=self.kwargs["pk"]) - context["club"] = bus.wei - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["bus"].initial = Bus.objects.get(pk=self.kwargs["pk"]) - return form - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) - - -class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): - """ - Update Bus team - """ - model = BusTeam - form_class = BusTeamForm - extra_context = {"title": _("Update team")} - - def dispatch(self, request, *args, **kwargs): - wei = self.get_object().bus.wei - today = date.today() - # We can't update a bus once the WEI is started - if today >= wei.date_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.bus.wei - context["bus"] = self.object.bus - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["bus"].disabled = True - return form - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) - - -class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): - """ - Manage Bus team - """ - model = BusTeam - extra_context = {"title": _("Manage WEI team")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["bus"] = self.object.bus - context["club"] = self.object.bus.wei - - memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( - self.request, WEIMembership, "view")).filter(team=self.object) - memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") - memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) - context["memberships"] = memberships_table - - return context - - -class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Register a new user to the WEI - """ - model = WEIRegistration - form_class = WEIRegistrationForm - extra_context = {"title": _("Register first year student to the WEI")} - - def get_sample_object(self): - wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - if "myself" in self.request.path: - user = self.request.user - else: - # To avoid unique validation issues, we use an account that can't join the WEI. - # In development mode, the note account may not exist, we use a random user (may fail) - user = User.objects.get(username="note") \ - if User.objects.filter(username="note").exists() else User.objects.first() - return WEIRegistration( - wei=wei, - user=user, - first_year=True, - birth_date="1970-01-01", - gender="No", - emergency_contact_name="No", - emergency_contact_phone="No", - ) - - def dispatch(self, request, *args, **kwargs): - wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - today = date.today() - # We can't register someone once the WEI is started and before the membership start date - if today >= wei.date_start or today < wei.membership_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - # Don't register twice - if 'myself' in self.request.path and not self.request.user.is_anonymous \ - and WEIRegistration.objects.filter(wei=wei, user=self.request.user).exists(): - obj = WEIRegistration.objects.get(wei=wei, user=self.request.user) - return redirect(reverse_lazy('wei:wei_update_registration', args=(obj.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['title'] = _("Register 1A") - context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - if "myself" in self.request.path: - context["form"].fields["user"].disabled = True - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["user"].initial = self.request.user - del form.fields["first_year"] - del form.fields["caution_check"] - del form.fields["information_json"] - return form - - @transaction.atomic - def form_valid(self, form): - form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - form.instance.first_year = True - - if not form.instance.pk: - # Check if the user is not already registered to the WEI - if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists(): - form.add_error('user', _("This user is already registered to this WEI.")) - return self.form_invalid(form) - - # Check if the user can be in her/his first year (yeah, no cheat) - if WEIRegistration.objects.filter(user=form.instance.user).exists(): - form.add_error('user', _("This user can't be in her/his first year since he/she has already" - " participated to a WEI.")) - return self.form_invalid(form) - - if 'treasury' in settings.INSTALLED_APPS: - from treasury.models import SogeCredit - form.instance.soge_credit = \ - form.instance.soge_credit \ - or SogeCredit.objects.filter(user=form.instance.user, credit_transaction__valid=False).exists() - - return super().form_valid(form) - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) - - -class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Register an old user to the WEI - """ - model = WEIRegistration - form_class = WEIRegistrationForm - extra_context = {"title": _("Register old student to the WEI")} - - def get_sample_object(self): - wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - if "myself" in self.request.path: - user = self.request.user - else: - # To avoid unique validation issues, we use an account that can't join the WEI. - # In development mode, the note account may not exist, we use a random user (may fail) - user = User.objects.get(username="note") \ - if User.objects.filter(username="note").exists() else User.objects.first() - return WEIRegistration( - wei=wei, - user=user, - first_year=True, - birth_date="1970-01-01", - gender="No", - emergency_contact_name="No", - emergency_contact_phone="No", - ) - - def dispatch(self, request, *args, **kwargs): - wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - today = date.today() - # We can't register someone once the WEI is started and before the membership start date - if today >= wei.date_start or today < wei.membership_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - # Don't register twice - if 'myself' in self.request.path and not self.request.user.is_anonymous \ - and WEIRegistration.objects.filter(wei=wei, user=self.request.user).exists(): - obj = WEIRegistration.objects.get(wei=wei, user=self.request.user) - return redirect(reverse_lazy('wei:wei_update_registration', args=(obj.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['title'] = _("Register 2A+") - context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - - if "myself" in self.request.path: - context["form"].fields["user"].disabled = True - - choose_bus_form = WEIChooseBusForm(self.request.POST if self.request.POST else None) - choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]).order_by('name') - choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\ - .order_by('bus__name', 'name') - context['membership_form'] = choose_bus_form - - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["user"].initial = self.request.user - if "myself" in self.request.path and self.request.user.profile.soge: - form.fields["soge_credit"].disabled = True - form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") - - del form.fields["caution_check"] - del form.fields["first_year"] - del form.fields["information_json"] - - return form - - @transaction.atomic - def form_valid(self, form): - form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - form.instance.first_year = False - - if not form.instance.pk: - # Check if the user is not already registered to the WEI - if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists(): - form.add_error('user', _("This user is already registered to this WEI.")) - return self.form_invalid(form) - - choose_bus_form = WEIChooseBusForm(self.request.POST) - if not choose_bus_form.is_valid(): - return self.form_invalid(form) - - information = form.instance.information - information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]] - information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]] - information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]] - information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]] - information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] - information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] - form.instance.information = information - form.instance.save() - - if 'treasury' in settings.INSTALLED_APPS: - from treasury.models import SogeCredit - form.instance.soge_credit = \ - form.instance.soge_credit \ - or SogeCredit.objects.filter(user=form.instance.user, credit_transaction__valid=False).exists() - - return super().form_valid(form) - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) - - -class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): - """ - Update a registration for the WEI - """ - model = WEIRegistration - form_class = WEIRegistrationForm - extra_context = {"title": _("Update WEI Registration")} - - def dispatch(self, request, *args, **kwargs): - wei = self.get_object().wei - today = date.today() - # We can't update a registration once the WEI is started and before the membership start date - if today >= wei.date_start or today < wei.membership_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.wei - - if self.object.is_validated: - membership_form = self.get_membership_form(instance=self.object.membership, - data=self.request.POST) - context["membership_form"] = membership_form - elif not self.object.first_year and PermissionBackend.check_perm( - self.request, "wei.change_weiregistration_information_json", self.object): - information = self.object.information - d = dict( - bus=Bus.objects.filter(pk__in=information["preferred_bus_pk"]).all(), - team=BusTeam.objects.filter(pk__in=information["preferred_team_pk"]).all(), - roles=WEIRole.objects.filter(pk__in=information["preferred_roles_pk"]).all(), - ) if 'preferred_bus_pk' in information else dict() - choose_bus_form = WEIChooseBusForm( - self.request.POST if self.request.POST else d - ) - choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]) - choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"]) - context["membership_form"] = choose_bus_form - - if not self.object.soge_credit and self.object.user.profile.soge: - form = context["form"] - form.fields["soge_credit"].disabled = True - form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") - - return context - - def get_form(self, form_class=None): - form = super().get_form(form_class) - form.fields["user"].disabled = True - # The auto-json-format may cause issues with the default field remove - if not PermissionBackend.check_perm(self.request, 'wei.change_weiregistration_information_json', self.object): - del form.fields["information_json"] - return form - - def get_membership_form(self, data=None, instance=None): - membership_form = WEIMembershipForm(data if data else None, instance=instance) - del membership_form.fields["credit_type"] - del membership_form.fields["credit_amount"] - del membership_form.fields["first_name"] - del membership_form.fields["last_name"] - del membership_form.fields["bank"] - for field_name, _field in list(membership_form.fields.items()): - if not PermissionBackend.check_perm( - self.request, "wei.change_weimembership_" + field_name, self.object.membership): - del membership_form.fields[field_name] - return membership_form - - @transaction.atomic - def form_valid(self, form): - # If the membership is already validated, then we update the bus and the team (and the roles) - if form.instance.is_validated: - membership_form = self.get_membership_form(self.request.POST, form.instance.membership) - if not membership_form.is_valid(): - return self.form_invalid(form) - membership_form.save() - # If it is not validated and if this is an old member, then we update the choices - elif not form.instance.first_year and PermissionBackend.check_perm( - self.request, "wei.change_weiregistration_information_json", self.object): - choose_bus_form = WEIChooseBusForm(self.request.POST) - if not choose_bus_form.is_valid(): - return self.form_invalid(form) - information = form.instance.information - information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]] - information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]] - information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]] - information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]] - information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] - information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] - form.instance.information = information - form.instance.save() - - return super().form_valid(form) - - def get_success_url(self): - self.object.refresh_from_db() - if self.object.first_year: - survey = CurrentSurvey(self.object) - if not survey.is_complete(): - return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) - if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership( - club=self.object.wei, - user=self.object.user, - date_start=date.today(), - date_end=date.today(), - fee=0, - registration=self.object, - )): - return reverse_lazy("wei:validate_registration", kwargs={"pk": self.object.pk}) - return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk}) - - -class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): - """ - Delete a non-validated WEI registration - """ - model = WEIRegistration - extra_context = {"title": _("Delete WEI registration")} - - def dispatch(self, request, *args, **kwargs): - object = self.get_object() - wei = object.wei - today = date.today() - # We can't delete a registration of a past WEI - if today > wei.membership_end: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - - if not PermissionBackend.check_perm(self.request, "wei.delete_weiregistration", object): - raise PermissionDenied(_("You don't have the right to delete this WEI registration.")) - - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.wei - return context - - def get_success_url(self): - return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,)) - - -class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): - """ - Validate WEI Registration - """ - model = WEIMembership - extra_context = {"title": _("Validate WEI registration")} - - def get_sample_object(self): - registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) - return WEIMembership( - club=registration.wei, - user=registration.user, - date_start=date.today(), - date_end=date.today() + timedelta(days=1), - fee=0, - registration=registration, - ) - - def dispatch(self, request, *args, **kwargs): - wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei - today = date.today() - # We can't validate anyone once the WEI is started and before the membership start date - if today >= wei.date_start or today < wei.membership_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) - context["registration"] = registration - survey = CurrentSurvey(registration) - if survey.information.valid: - context["suggested_bus"] = survey.information.get_selected_bus() - context["club"] = registration.wei - - kfet = registration.wei.parent_club - bde = kfet.parent_club - - context["kfet_member"] = Membership.objects.filter( - club__name=kfet.name, - user=registration.user, - date_start__gte=kfet.membership_start, - ).exists() - context["bde_member"] = Membership.objects.filter( - club__name=bde.name, - user=registration.user, - date_start__gte=bde.membership_start, - ).exists() - - context["fee"] = registration.fee - - form = context["form"] - if registration.soge_credit: - form.fields["credit_amount"].initial = registration.fee - else: - form.fields["credit_amount"].initial = max(0, registration.fee - registration.user.note.balance) - - return context - - def get_form_class(self): - registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) - if registration.first_year and 'sleected_bus_pk' not in registration.information: - return WEIMembership1AForm - return WEIMembershipForm - - def get_form(self, form_class=None): - form = super().get_form(form_class) - registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) - form.fields["last_name"].initial = registration.user.last_name - form.fields["first_name"].initial = registration.user.first_name - - if registration.soge_credit: - form.fields["credit_type"].disabled = True - form.fields["credit_type"].initial = NoteSpecial.objects.get(special_type="Virement bancaire") - form.fields["credit_amount"].disabled = True - form.fields["last_name"].disabled = True - form.fields["first_name"].disabled = True - form.fields["bank"].disabled = True - form.fields["bank"].initial = "Société générale" - - if 'bus' in form.fields: - # For 2A+ and hardcoded 1A - form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk) - if registration.first_year: - # Use the results of the survey to fill initial data - # A first year has no other role than "1A" - del form.fields["roles"] - survey = CurrentSurvey(registration) - if survey.information.valid: - form.fields["bus"].initial = survey.information.get_selected_bus() - else: - # Use the choice of the member to fill initial data - information = registration.information - if "preferred_bus_pk" in information and len(information["preferred_bus_pk"]) == 1: - form["bus"].initial = Bus.objects.get(pk=information["preferred_bus_pk"][0]) - if "preferred_team_pk" in information and len(information["preferred_team_pk"]) == 1: - form["team"].initial = BusTeam.objects.get(pk=information["preferred_team_pk"][0]) - if "preferred_roles_pk" in information: - form["roles"].initial = WEIRole.objects.filter( - Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI") - ).all() - return form - - @transaction.atomic - def form_valid(self, form): - """ - Create membership, check that all is good, make transactions - """ - registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) - club = registration.wei - user = registration.user - - membership = form.instance - membership.user = user - membership.club = club - membership.date_start = min(date.today(), club.date_start) - membership.registration = registration - # Force the membership of the clubs BDE and Kfet - membership._force_renew_parent = True - - fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid - - kfet = club.parent_club - bde = kfet.parent_club - - kfet_member = Membership.objects.filter( - club__name=kfet.name, - user=registration.user, - date_start__gte=kfet.membership_start, - ).exists() - bde_member = Membership.objects.filter( - club__name=bde.name, - user=registration.user, - date_start__gte=bde.membership_start, - ).exists() - - if not kfet_member: - fee += kfet.membership_fee_paid if registration.user.profile.paid else kfet.membership_fee_unpaid - if not bde_member: - fee += bde.membership_fee_paid if registration.user.profile.paid else bde.membership_fee_unpaid - - credit_type = form.cleaned_data["credit_type"] - credit_amount = form.cleaned_data["credit_amount"] - last_name = form.cleaned_data["last_name"] - first_name = form.cleaned_data["first_name"] - bank = form.cleaned_data["bank"] - - if credit_type is None or registration.soge_credit: - credit_amount = 0 - - 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', - _("This user don't have enough money to join this club, and can't have a negative balance.")) - return super().form_invalid(form) - - if credit_amount: - if not last_name: - form.add_error('last_name', _("This field is required.")) - return super().form_invalid(form) - - if not first_name: - form.add_error('first_name', _("This field is required.")) - return super().form_invalid(form) - - # Credit note before adding the membership - SpecialTransaction.objects.create( - source=credit_type, - destination=registration.user.note, - amount=credit_amount, - reason="Crédit " + str(credit_type) + " (WEI)", - last_name=last_name, - first_name=first_name, - bank=bank, - ) - - # Now, all is fine, the membership can be created. - - if registration.soge_credit: - form.instance._soge = True - - if registration.first_year: - membership = form.instance - # If the user is not a member of the club Kfet, then the membership is created. - membership.save() - membership.refresh_from_db() - membership.roles.set(WEIRole.objects.filter(name="1A").all()) - membership.save() - - membership.save() - membership.refresh_from_db() - membership.roles.add(WEIRole.objects.get(name="Adhérent WEI")) - - return super().form_valid(form) - - def get_success_url(self): - self.object.refresh_from_db() - return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk}) - - -class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): - """ - Display the survey for the WEI for first year members. - Warning: this page is accessible for anyone that is connected, the view doesn't extend ProtectQuerySetMixin. - """ - model = WEIRegistration - template_name = "wei/survey.html" - survey = None - extra_context = {"title": _("Survey WEI")} - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.object = obj - - wei = obj.wei - today = date.today() - # We can't access to the WEI survey once the WEI is started and before the membership start date - if today >= wei.date_start or today < wei.membership_start: - return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) - - if not self.survey: - self.survey = CurrentSurvey(obj) - # If the survey is complete, then display the end page. - if self.survey.is_complete(): - return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) - # Non first year members don't have a survey - if not obj.first_year: - return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_form_class(self): - """ - Get the survey form. It may depend on the current state of the survey. - """ - return self.survey.get_form_class() - - def get_form(self, form_class=None): - """ - Update the form with the data of the survey. - """ - form = super().get_form(form_class) - self.survey.update_form(form) - return form - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = self.object.wei - return context - - @transaction.atomic - def form_valid(self, form): - """ - Update the survey with the data of the form. - """ - self.survey.form_valid(form) - return super().form_valid(form) - - def get_success_url(self): - return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,)) - - -class WEISurveyEndView(LoginRequiredMixin, TemplateView): - template_name = "wei/survey_end.html" - extra_context = {"title": _("Survey WEI")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei - return context - - -class WEIClosedView(LoginRequiredMixin, TemplateView): - template_name = "wei/survey_closed.html" - extra_context = {"title": _("Survey WEI")} - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"]) - return context - - -class MemberListRenderView(LoginRequiredMixin, View): - """ - Render Invoice as a generated PDF with the given information and a LaTeX template - """ - - def get_queryset(self, **kwargs): - qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view")) - qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by( - Lower('bus__name'), - Lower('team__name'), - 'user__profile__promotion', - Lower('user__last_name'), - Lower('user__first_name'), - 'id', - ) - - if "bus_pk" in self.kwargs: - qs = qs.filter(bus__pk=self.kwargs["bus_pk"]) - - if "team_pk" in self.kwargs: - qs = qs.filter(team__pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None) - - return qs.distinct() - - def get(self, request, **kwargs): - qs = self.get_queryset() - - wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) - bus = team = None - if "bus_pk" in self.kwargs: - bus = Bus.objects.get(pk=self.kwargs["bus_pk"]) - if "team_pk" in self.kwargs: - team = BusTeam.objects.filter(pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None) - if team.exists(): - team = team.get() - bus = team.bus - else: - team = dict(name="Staff") - - # Fill the template with the information - tex = render_to_string("wei/weilist_sample.tex", dict(memberships=qs.all(), wei=wei, bus=bus, team=team)) - - try: - os.mkdir(BASE_DIR + "/tmp") - except FileExistsError: - pass - # We render the file in a temporary directory - tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/") - - try: - with open("{}/wei-list.tex".format(tmp_dir), "wb") as f: - f.write(tex.encode("UTF-8")) - del tex - - with open(os.devnull, "wb") as devnull: - error = subprocess.Popen( - ["/usr/bin/xelatex", "-interaction=nonstopmode", "{}/wei-list.tex".format(tmp_dir)], - cwd=tmp_dir, - stderr=devnull, - stdout=devnull, - ).wait() - - if error: - with open("{}/wei-list.log".format(tmp_dir), "r") as f: - log = f.read() - raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")\n\n" + log) - - # Display the generated pdf as a HTTP Response - with open("{}/wei-list.pdf".format(tmp_dir), 'rb') as f: - pdf = f.read() - response = HttpResponse(pdf, content_type="application/pdf") - response['Content-Disposition'] = "inline;filename=Liste%20des%20participants%20au%20WEI.pdf" - except IOError as e: - raise e - finally: - # Delete all temporary files - shutil.rmtree(tmp_dir) - - return response - - -class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView): - model = WEIRegistration - template_name = "wei/1A_list.html" - table_class = WEIRegistration1ATable - extra_context = {"title": _("Attribute buses to first year members")} - - def dispatch(self, request, *args, **kwargs): - self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def get_queryset(self, filter_permissions=True, **kwargs): - qs = super().get_queryset(filter_permissions, **kwargs) - qs = qs.filter(first_year=True, membership__isnull=False) - qs = qs.order_by('-membership__bus') - return qs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['club'] = self.club - context['bus_repartition_table'] = BusRepartitionTable( - Bus.objects.filter(wei=self.club, size__gt=0) - .filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) - .all()) - return context - - -class WEIAttributeBus1AView(ProtectQuerysetMixin, DetailView): - model = WEIRegistration - template_name = "wei/attribute_bus_1A.html" - extra_context = {"title": _("Attribute bus")} - - def get_queryset(self, filter_permissions=True, **kwargs): - qs = super().get_queryset(filter_permissions, **kwargs) - qs = qs.filter(first_year=True) - return qs - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - if 'selected_bus_pk' not in obj.information: - return redirect(reverse_lazy('wei:wei_survey', args=(obj.pk,))) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['club'] = self.object.wei - context['survey'] = CurrentSurvey(self.object) - return context - - -class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView): - def get_redirect_url(self, *args, **kwargs): - wei = WEIClub.objects.filter(pk=self.kwargs['pk']) - if not wei.exists(): - raise Http404 - wei = wei.get() - qs = WEIRegistration.objects.filter(wei=wei, membership__isnull=False, membership__bus__isnull=True) - qs = qs.filter(information_json__contains='selected_bus_pk') # not perfect, but works... - if qs.exists(): - return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk, )) - return reverse_lazy('wei:wei_1A_list', args=(wei.pk, )) diff --git a/docs/api/wei.rst b/docs/api/wei.rst deleted file mode 100644 index 8c92c02..0000000 --- a/docs/api/wei.rst +++ /dev/null @@ -1,710 +0,0 @@ -API WEI -======= - -Wei ---- - -**Chemin :** `/api/wei/club/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Wei Club List", - "description": "REST API View set.\nThe djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/club/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "name": { - "type": "string", - "required": true, - "read_only": false, - "label": "Nom", - "max_length": 255 - }, - "email": { - "type": "email", - "required": true, - "read_only": false, - "label": "Courriel", - "max_length": 254 - }, - "require_memberships": { - "type": "boolean", - "required": false, - "read_only": false, - "label": "N\u00e9cessite des adh\u00e9sions", - "help_text": "D\u00e9cochez si ce club n'utilise pas d'adh\u00e9sions." - }, - "membership_fee_paid": { - "type": "integer", - "required": false, - "read_only": false, - "label": "Cotisation pour adh\u00e9rer (normalien \u00e9l\u00e8ve)", - "min_value": 0, - "max_value": 2147483647 - }, - "membership_fee_unpaid": { - "type": "integer", - "required": false, - "read_only": false, - "label": "Cotisation pour adh\u00e9rer (normalien \u00e9tudiant)", - "min_value": 0, - "max_value": 2147483647 - }, - "membership_duration": { - "type": "integer", - "required": false, - "read_only": false, - "label": "Dur\u00e9e de l'adh\u00e9sion", - "help_text": "La dur\u00e9e maximale (en jours) d'une adh\u00e9sion (NULL = infinie).", - "min_value": 0, - "max_value": 2147483647 - }, - "membership_start": { - "type": "date", - "required": false, - "read_only": false, - "label": "D\u00e9but de l'adh\u00e9sion", - "help_text": "Date \u00e0 partir de laquelle les adh\u00e9rents peuvent renouveler leur adh\u00e9sion." - }, - "membership_end": { - "type": "date", - "required": false, - "read_only": false, - "label": "Fin de l'adh\u00e9sion", - "help_text": "Date maximale d'une fin d'adh\u00e9sion, apr\u00e8s laquelle les adh\u00e9rents doivent la renouveler." - }, - "year": { - "type": "integer", - "required": false, - "read_only": false, - "label": "Ann\u00e9e", - "min_value": 0, - "max_value": 2147483647 - }, - "date_start": { - "type": "date", - "required": true, - "read_only": false, - "label": "D\u00e9but" - }, - "date_end": { - "type": "date", - "required": true, - "read_only": false, - "label": "Fin" - }, - "parent_club": { - "type": "field", - "required": false, - "read_only": false, - "label": "Club parent" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``name`` -* ``year`` -* ``date_start`` -* ``date_end`` -* ``email`` -* ``note__alias__name`` -* ``note__alias__normalized_name`` -* ``parent_club`` -* ``parent_club__name`` -* ``require_memberships`` -* ``membership_fee_paid`` -* ``membership_fee_unpaid`` -* ``membership_duration`` -* ``membership_start`` -* ``membership_end`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``name`` (expression régulière) -* ``email`` (expression régulière) -* ``note__alias__name`` (expression régulière) -* ``note__alias__normalized_name`` (expression régulière) - -Bus ---- - -**Chemin :** `/api/wei/bus/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Bus List", - "description": "REST API View set.\nThe djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/bus/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "name": { - "type": "string", - "required": true, - "read_only": false, - "label": "Nom", - "max_length": 255 - }, - "description": { - "type": "string", - "required": false, - "read_only": false, - "label": "Description" - }, - "information_json": { - "type": "string", - "required": false, - "read_only": false, - "label": "Informations sur le questionnaire", - "help_text": "Informations sur le sondage pour les nouveaux membres, encod\u00e9es en JSON" - }, - "wei": { - "type": "field", - "required": true, - "read_only": false, - "label": "WEI" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``name`` -* ``wei`` -* ``description`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``name`` (expression régulière) -* ``wei__name`` (expression régulière) -* ``description`` (expression régulière) - -Équipe de bus -------------- - -**Chemin :** `/api/wei/team/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Bus Team List", - "description": "REST API View set.\nThe djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/team/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "name": { - "type": "string", - "required": true, - "read_only": false, - "label": "Nom", - "max_length": 255 - }, - "color": { - "type": "integer", - "required": true, - "read_only": false, - "label": "Couleur", - "help_text": "La couleur du T-Shirt, stock\u00e9 sous la forme de son \u00e9quivalent num\u00e9rique", - "min_value": 0, - "max_value": 2147483647 - }, - "description": { - "type": "string", - "required": false, - "read_only": false, - "label": "Description" - }, - "bus": { - "type": "field", - "required": true, - "read_only": false, - "label": "Bus" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``name`` -* ``bus`` -* ``color`` -* ``description`` -* ``bus__wei`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``name`` (expression régulière) -* ``bus__name`` (expression régulière) -* ``bus__wei__name`` (expression régulière) -* ``description`` (expression régulière) - -Rôle au wei ------------ - -**Chemin :** `/api/wei/role/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Wei Role List", - "description": "REST API View set.\nThe djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/role/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "name": { - "type": "string", - "required": true, - "read_only": false, - "label": "Nom", - "max_length": 255 - }, - "for_club": { - "type": "field", - "required": false, - "read_only": false, - "label": "S'applique au club" - }, - "permissions": { - "type": "field", - "required": true, - "read_only": false, - "label": "Permissions" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``name`` -* ``permissions`` -* ``memberships`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``name`` (expression régulière) - -Participant au wei ------------------- - -**Chemin :** `/api/wei/registration/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Wei Registration List", - "description": "REST API View set.\nThe djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/registration/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "soge_credit": { - "type": "boolean", - "required": false, - "read_only": false, - "label": "Cr\u00e9dit de la Soci\u00e9t\u00e9 g\u00e9n\u00e9rale" - }, - "caution_check": { - "type": "boolean", - "required": false, - "read_only": false, - "label": "Ch\u00e8que de caution donn\u00e9" - }, - "birth_date": { - "type": "date", - "required": true, - "read_only": false, - "label": "Date de naissance" - }, - "gender": { - "type": "choice", - "required": true, - "read_only": false, - "label": "Genre", - "choices": [ - { - "value": "male", - "display_name": "Homme" - }, - { - "value": "female", - "display_name": "Femme" - }, - { - "value": "nonbinary", - "display_name": "Non-binaire" - } - ] - }, - "clothing_cut": { - "type": "choice", - "required": true, - "read_only": false, - "label": "Coupe de v\u00eatement", - "choices": [ - { - "value": "male", - "display_name": "Homme" - }, - { - "value": "female", - "display_name": "Femme" - } - ] - }, - "clothing_size": { - "type": "choice", - "required": true, - "read_only": false, - "label": "Taille de v\u00eatement", - "choices": [ - { - "value": "XS", - "display_name": "XS" - }, - { - "value": "S", - "display_name": "S" - }, - { - "value": "M", - "display_name": "M" - }, - { - "value": "L", - "display_name": "L" - }, - { - "value": "XL", - "display_name": "XL" - }, - { - "value": "XXL", - "display_name": "XXL" - } - ] - }, - "health_issues": { - "type": "string", - "required": false, - "read_only": false, - "label": "Probl\u00e8mes de sant\u00e9" - }, - "emergency_contact_name": { - "type": "string", - "required": true, - "read_only": false, - "label": "Nom du contact en cas d'urgence", - "max_length": 255 - }, - "emergency_contact_phone": { - "type": "string", - "required": true, - "read_only": false, - "label": "T\u00e9l\u00e9phone du contact en cas d'urgence", - "max_length": 32 - }, - "first_year": { - "type": "boolean", - "required": false, - "read_only": false, - "label": "Premi\u00e8re ann\u00e9e", - "help_text": "Indique si l'utilisateur est nouveau dans l'\u00e9cole." - }, - "information_json": { - "type": "string", - "required": false, - "read_only": false, - "label": "Informations sur l'inscription", - "help_text": "Informations sur l'inscription (bus pour les 2A+, questionnaire pour les 1A), encod\u00e9es en JSON" - }, - "user": { - "type": "field", - "required": true, - "read_only": false, - "label": "Utilisateur" - }, - "wei": { - "type": "field", - "required": true, - "read_only": false, - "label": "WEI" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``user`` -* ``user__username`` -* ``user__first_name`` -* ``user__last_name`` -* ``user__email`` -* ``user__note__alias__name`` -* ``user__note__alias__normalized_name`` -* ``wei`` -* ``wei__name`` -* ``wei__email`` -* ``wei__year`` -* ``soge_credit`` -* ``caution_check`` -* ``birth_date`` -* ``gender`` -* ``clothing_cut`` -* ``clothing_size`` -* ``first_year`` -* ``emergency_contact_name`` -* ``emergency_contact_phone`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``user__username`` (expression régulière) -* ``user__first_name`` (expression régulière) -* ``user__last_name`` (expression régulière) -* ``user__email`` (expression régulière) -* ``user__note__alias__name`` (expression régulière) -* ``user__note__alias__normalized_name`` (expression régulière) -* ``wei__name`` (expression régulière) -* ``wei__email`` (expression régulière) -* ``health_issues`` (expression régulière) -* ``emergency_contact_name`` (expression régulière) -* ``emergency_contact_phone`` (expression régulière) - -Adhésion au wei ---------------- - -**Chemin :** `/api/wei/membership/ `_ - -Options -~~~~~~~ - -.. code:: json - - { - "name": "Wei Membership List", - "description": "REST API View set.\nThe djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,\nthen render it on /api/wei/membership/", - "renders": [ - "application/json", - "text/html" - ], - "parses": [ - "application/json", - "application/x-www-form-urlencoded", - "multipart/form-data" - ], - "actions": { - "POST": { - "id": { - "type": "integer", - "required": false, - "read_only": true, - "label": "ID" - }, - "date_start": { - "type": "date", - "required": false, - "read_only": false, - "label": "L'adh\u00e9sion commence le" - }, - "date_end": { - "type": "date", - "required": false, - "read_only": false, - "label": "L'adh\u00e9sion finit le" - }, - "fee": { - "type": "integer", - "required": true, - "read_only": false, - "label": "Cotisation", - "min_value": 0, - "max_value": 2147483647 - }, - "user": { - "type": "field", - "required": true, - "read_only": false, - "label": "Utilisateur" - }, - "club": { - "type": "field", - "required": true, - "read_only": false, - "label": "Club" - }, - "bus": { - "type": "field", - "required": false, - "read_only": false, - "label": "Bus" - }, - "team": { - "type": "field", - "required": false, - "read_only": false, - "label": "\u00c9quipe" - }, - "registration": { - "type": "field", - "required": false, - "read_only": false, - "label": "Inscription au WEI" - }, - "roles": { - "type": "field", - "required": true, - "read_only": false, - "label": "R\u00f4les" - } - } - } - } - -Filtres Django -~~~~~~~~~~~~~~ - -* ``club__name`` -* ``club__email`` -* ``club__note__alias__name`` -* ``club__note__alias__normalized_name`` -* ``user__username`` -* ``user__last_name`` -* ``user__first_name`` -* ``user__email`` -* ``user__note__alias__name`` -* ``user__note__alias__normalized_name`` -* ``date_start`` -* ``date_end`` -* ``fee`` -* ``roles`` -* ``bus`` -* ``bus__name`` -* ``team`` -* ``team__name`` -* ``registration`` - -Tris possible -~~~~~~~~~~~~~ - -* ``id`` -* ``date_start`` -* ``date_end`` - -Filtres de recherche -~~~~~~~~~~~~~~~~~~~~ - -* ``club__name`` (expression régulière) -* ``club__email`` (expression régulière) -* ``club__note__alias__name`` (expression régulière) -* ``club__note__alias__normalized_name`` (expression régulière) -* ``user__username`` (expression régulière) -* ``user__last_name`` (expression régulière) -* ``user__first_name`` (expression régulière) -* ``user__email`` (expression régulière) -* ``user__note__alias__name`` (expression régulière) -* ``user__note__alias__normalized_name`` (expression régulière) -* ``roles__name`` (expression régulière) -* ``bus__name`` (expression régulière) -* ``team__name`` (expression régulière) - diff --git a/docs/apps/wei.rst b/docs/apps/wei.rst deleted file mode 100644 index 51d3375..0000000 --- a/docs/apps/wei.rst +++ /dev/null @@ -1,333 +0,0 @@ -WEI -=== - -Cette application gère toute la phase d'inscription au WEI. - -Modèles -------- - -WEIClub -~~~~~~~ - -Le modèle ``WEIClub`` hérite de ``Club`` et contient toutes les informations d'un WEI. - -* ``year`` : ``PositiveIntegerField`` unique, année du WEI. -* ``date_start`` : ``DateField``, date de début du WEI. -* ``date_end`` : ``DateField``, date de fin du WEI. - -Champs hérités de ``Club`` de l'application ``member`` : - -* ``parent_club`` : ``ForeignKey(Club)``. Ce champ vaut toujours ``Kfet`` dans le cas d'un WEI : on doit être membre du - club Kfet pour participer au WEI. -* ``email`` : ``EmailField``, adresse e-mail sur laquelle contacter les gérants du WEI. -* ``membership_start`` : ``DateField``, date à partir de laquelle il est possible de s'inscrire au WEI. -* ``membership_end`` : ``DateField``, date de fin d'adhésion possible au WEI. -* ``membership_duration`` : ``PositiveIntegerField``, inutilisé dans le cas d'un WEI, vaut ``None``. -* ``membership_fee_paid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un élève normalien - (donc rémunéré) puisse adhérer. -* ``membership_fee_unpaid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un étudiant - normalien (donc non rémunéré) puisse adhérer. -* ``name`` : ``CharField``, nom du WEI. -* ``require_memberships`` : ``BooleanField``, vaut toujours ``True`` pour le WEI. - -Bus -~~~ - -Contient les informations sur un bus allant au WEI. - -* ``wei`` : ``ForeignKey(WEIClub)``, WEI auquel ce bus est rattaché. -* ``name`` : ``CharField``, nom du bus. Le champ est unique pour le WEI attaché. -* ``description`` : ``TextField``, description textuelle de l'ambiance du bus. -* ``information_json`` : ``TextField``, diverses informations non publiques qui permettent d'indiquer divers paramètres - pouvant varier selon les années permettant l'attribution des bus aux 1A. - -Il est souhaitable de créer chaque année un bus "Staff" (non accessible aux 1A bien évidemment) pour les GC WEI qui ne -monteraient pas dans un bus. - -BusTeam -~~~~~~~ - -Contient les informations d'une équipe WEI. - -* ``wei`` : ``ForeignKey(WEIClub)``, WEI auquel cette équipe est rattachée. -* ``name`` : ``CharField``, nom de l'équipe. -* ``color`` : ``PositiveIntegerField``, entier entre 0 et 16777215 = 0xFFFFFF représentant la couleur du T-Shirt. - La donnée se rentre en hexadécimal via un sélecteur de couleur. Cette information est purement cosmétique et n'est - utilisée nulle part. -* ``description`` : ``TextField``, description de l'équipe. - -WEIRole -~~~~~~~ - -Ce modèle hérité de ``Role``, ne contient qu'un champ ``name`` (``CharField``), le nom du rôle. Ce modèle ne permet -que de dissocier les rôles propres au WEI des rôles s'appliquant pour n'importe quel club. - -WEIRegistration -~~~~~~~~~~~~~~~ - -Inscription au WEI, contenant les informations avant validation. Ce modèle est créé dès lors que quelqu'un se pré-inscrit au WEI. - -* ``user`` : ``ForeignKey(User)``, utilisateur qui s'est pré-inscrit. Ce champ est unique avec ``wei``. -* ``wei`` : ``ForeignKey(WEIClub)``, le WEI auquel l'utilisateur s'est pré-inscrit. Ce champ est unique avec ``user``. -* ``soge_credit`` : ``BooleanField``, indique si l'utilisateur a déclaré vouloir ouvrir un compte à la Société générale. -* ``caution_check`` : ``BooleanField``, indique si l'utilisateur (en 2ème année ou plus) a bien remis son chèque de - caution auprès de la trésorerie. -* ``birth_date`` : ``DateField``, date de naissance de l'utilisateur. -* ``gender`` : ``CharField`` parmi ``male`` (Homme), ``female`` (Femme), ``non binary`` (Non binaire), genre de la personne. -* ``health_issues`` : ``TextField``, problèmes de santé déclarés par l'utilisateur. -* ``emergency_contact_name`` : ``CharField``, nom du contact en cas d'urgence. -* ``emergency_contact_phone`` : ``CharField``, numéro de téléphone du contact en cas d'urgence. -* ``ml_events_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des - événements du BDE (1A uniquement) -* ``ml_art_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des - actualités du BDA (1A uniquement) -* ``ml_sport_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des - actualités du BDS (1A uniquement) -* ``first_year`` : ``BooleanField``, indique si l'inscription est d'un 1A ou non. Non modifiable par n'importe qui. -* ``information_json`` : ``TextField`` non modifiable manuellement par n'importe qui stockant les informations du - questionnaire d'inscription au WEI pour les 1A, et stocke les demandes faites par un 2A+ concerant bus, équipes et rôles. - On utilise un ``TextField`` contenant des données au format JSON pour permettre de la modularité au fil des années, - sans avoir à tout casser à chaque fois. - -WEIMembership -~~~~~~~~~~~~~ - -Ce modèle hérite de ``Membership`` et contient les informations d'une adhésion au WEI. - -* ``bus`` : ``ForeignKey(Bus)``, bus dans lequel se trouve l'utilisateur. -* ``team`` : ``ForeignKey(BusTeam)`` pouvant être nulle (pour les chefs de bus et électrons libres), équipe dans laquelle - se trouve l'utilisateur. -* ``registration`` : ``OneToOneField(WEIRegistration)``, informations de la pré-inscription. - -Champs hérités du modèle ``Membership`` : - -* ``club`` : ``ForeignKey(Club)``, club lié à l'adhésion. Doit être un ``WEIClub``. -* ``user`` : ``ForeignKey(User)``, utilisateur adhéré. -* ``date_start`` : ``DateField``, date de début d'adhésion. -* ``date_end`` : ``DateField``, date de fin d'adhésion. -* ``fee`` : ``PositiveIntegerField``, montant de la cotisation payée. -* ``roles`` : ``ManyToManyField(Role)``, liste des rôles endossés par l'adhérent. Les rôles doivent être des ``WEIRole``. - -Graphe des modèles -~~~~~~~~~~~~~~~~~~ - -Pour une meilleure compréhension, le graphe des modèles de l'application ``member`` ont été ajoutés au schéma. - -.. image:: ../_static/img/graphs/wei.svg - :width: 960 - :alt: Graphe des modèles de l'application WEI - -Fonctionnement --------------- - -Création d'un WEI -~~~~~~~~~~~~~~~~~ - -Seul un respo info peut créer un WEI. Pour cela, se rendre dans l'onglet WEI, puis "Liste des WEI" et enfin -"Créer un WEI". Diverses informations sont demandées, comme le nom du WEI, l'adresse mail de contact, l'année du WEI -(doit être unique), les dates de début et de fin, et les dates pendant lesquelles les utilisateurs peuvent s'inscrire. - -Don des droits à un GC WEI -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Le GC WEI peut gérer tout ce qui a un rapport avec le WEI. Il ne peut cependant pas créer le WEI, ce privilège est -réservé au respo info. Pour avoir ses droits, le GC WEI doit s'inscrire au WEI avec le rôle GC WEI, et donc payer -en premier sa cotisation. C'est donc au respo info de créer l'adhésion du GC WEI. Voir ci-dessous pour l'inscription au WEI. - -S'inscrire au WEI -~~~~~~~~~~~~~~~~~ - -N'importe quel utilisateur peut s'auto-inscrire au WEI, lorsque les dates d'adhésion le permettent. Ceux qui se sont -déjà inscrits peuvent également inscrire un 1A. Seuls les GC WEI et les respo info peuvent inscrire un autre 2A+. - -À tout moment, tant que le WEI n'est pas passé, l'inscription peut être modifiée, même après validation. - -Inscription d'un 2A+ -^^^^^^^^^^^^^^^^^^^^ - -Comme indiqué, les 2A+ sont assez autonomes dans leur inscription au WEI. Ils remplissent le questionnaire et sont -ensuite pré-inscrits. Le questionnaire se compose de plusieurs champs (voir WEIRegistration) : - -* Est-ce que l'utilisateur a déclaré avoir ouvert un compte à la Société générale ? (Option disponible uniquemement - si cela n'a pas été fait une année avant) -* Date de naissance -* Genre (Homme/Femme/Non-binaire) -* Problèmes de santé -* Nom du contact en cas d'urgence -* Numéro du contact en cas d'urgence -* Bus préférés (choix multiple, utile pour les électrons libres) -* Équipes préférées (choix multiple éventuellement vide, vide pour les chefs de bus/staff) -* Rôles souhaités - -Les trois derniers champs n'ont aucun caractère définitif et sont simplement là en suggestion pour le GC WEI qui -validera l'inscription. C'est utile si on hésite entre plusieurs bus. - -L'inscription est ensuite créée, le GC WEI devra ensuite la valider (voir plus bas). - -Inscription d'un 1A -^^^^^^^^^^^^^^^^^^^ - -N'importe quelle personne déjà inscrite au WEI peut inscrire un 1A. Le formulaire 1A est assez peu différent du formulaire 2A+ : - -* Est-ce que l'utilisateur a déclaré avoir ouvert un compte à la Société générale ? -* Date de naissance -* Genre (Homme/Femme/Non-binaire) -* Problèmes de santé -* Nom du contact en cas d'urgence -* Numéro du contact en cas d'urgence -* S'inscrire à la ML événements -* S'inscrire à la ML BDA -* S'inscrire à la ML BDS - -Le 1A ne peut donc pas choisir de son bus et de son équipe, et peut s'inscrire aux listes de diffusion. -Il y a néanmoins une différence majeure : une fois le formulaire rempli, un questionnaire se lance. -Ce questionnaire peut varier au fil des années (voir section Questionnaire), et contient divers formulaires de collecte -de données qui serviront à déterminer quel est le meilleur bus pour ce nouvel utilisateur. - -Questionnaire 1A -^^^^^^^^^^^^^^^^ - -Le questionnaire 1A permet de poser des questions aux 1A lors de leur inscription au WEI afin de déterminer quel serait -le meilleur bus pour eux. Un algorithme attribue ensuite à chaque 1A le bus sélectionné. - -Afin de permettre de la modularité et de s'adapter aux changements au fil des années, il n'y a pas de modèle dédié au -sondage. On sauvegarde alors les données du sondage sous la forme d'un dictionnaire enregistré au format JSON -dans le champ ``information_json`` du modèle ``WEIRegistration``. Ce champ est modifiable manuellement uniquement par -les respos info et les GC WEI. - -Je veux changer d'algorithme de répartition, que faire ? -"""""""""""""""""""""""""""""""""""""""""""""""""""""""" - -Cette section est plus technique et s'adresse surtout aux respos info en cours de mandat. - -Première règle : on ne supprime rien (sauf si vraiment c'est du mauvais boulot). En prenant exemple sur des fichiers déjà existant tels que ``apps/wei/forms/surveys/wei2020.py``, créer un nouveau fichier ``apps/wei/forms/surveys/wei20XY.py``. Ce fichier doit inclure les éléments suivants : - -WEISurvey -""""""""" - -Une classe héritant de ``wei.forms.surveys.base.WEISurvey``, comportant les éléments suivants : - -* Une fonction ``get_year(cls)`` indiquant l'année du WEI liée au sondage -* Une fonction ``get_survey_information_class(cls)`` indiquant la classe héritant de - ``wei.forms.surveys.base.WEISurveyInformation`` contenant les données du sondage (voir plus bas) -* Une fonction ``get_algorithm_class(cls)`` indiquant la classe héritant de - ``wei.forms.surveys.base.WEISurveyAlgorithm`` contenant l'algorithme de répartition (voir plus bas) -* Une fonction ``get_form_class(self)`` qui indique la classe du formulaire Django à remplir. Cette classe peut dépendre - de l'état actuel du sondage. -* Une fonction ``update_form(self, form)``, optionnelle, appelée lorsqu'un formulaire dont la classe est spécifiée via - la fonction ``get_form_class``, et permet d'opérer sur le formulaire si besoin. -* Une fonction ``form_valid(self, form)`` qui indique quoi faire lorsque le formulaire est rempli. Cette fonction peut - bien sûr dépendre de l'état actuel du sondage. -* Une fonction ``is_complete(self)`` devant renvoyer un booléen indiquant si le sondage est complet ou non. - -Naturellement, il est implicite qu'une fonction ayant pour premier argument ``cls`` doit être annotée par ``@classmethod``. -Nativement, la classe ``WEISurvey`` comprend les informations suivantes : - -* ``registration``, le modèle ``WEIRegistration`` de l'utilisateur qui remplit le questionnaire -* ``information``, instance de ``WEISurveyInformation``, contient les données du questionnaire en cours de remplissage. -* ``get_wei(cls)``, renvoie le WEI correspondant à l'année du sondage. -* ``save(self)``, enregistre les informations du sondage dans l'objet ``registration`` associé, qui est ensuite - enregistré en base de données. -* ``select_bus(self, bus)``, choisit le bus ``bus`` comme bus préféré. Cela à pour effet de remplir les champs - ``selected_bus_pk`` et ``selected_bus_name`` par les identifiant et nom du bus, et ``valid`` à ``True``. - -Pour information, ``WEISurvey.__init__`` prend comme paramètre l'inscription ``registration``, et récupère les -informations du sondage converties en dictionnaire Python puis en objet ``WEISurveyInformation``. - -WEISurveyInformation -"""""""""""""""""""" - -Une classe héritant de ``wei.forms.surveys.base.WEISurveyInformation``, comportant les informations brutes du sondage. -Le champ ``information_json`` de ``WEIRegistration`` est traduit en dictionnaire Python depuis JSON, puis les différents -champs de WEISurveyInformation sont mis à jour depuis ce dictionnaire. Il n'y a rien de supplémentaire à ajouter, tout -est déjà géré. - -Ainsi, plutôt que de modifier laborieusement le champ JSON, préférez utiliser cette classe. Attention : pour des soucis -de traduction facile, merci de n'utiliser que des objets primitifs (nombres, chaînes de caractère, booléens, listes, -dictionnaires simples). Les instances de modèle sont à proscrire, préférez stocker l'identifiant et créer une fonction -qui récupère l'instance à partir de l'identifiant. - -Attention, 3 noms sont réservés : ``selected_bus_pk``, ``selected_bus_name`` et ``valid``, qui représentent la sortie -de l'algorithme de répartition. - -À noter que l'interface de validation des inscriptions affiche les données brutes du sondage. - -WEIBusInformation -""""""""""""""""" - -Une classe héritant de ``wei.forms.surveys.base.WEIBusInformation``, qui contient les informations sur un bus, -de la même manière que ``WEISurveyInformation`` contient les informations d'un sondage. Le fonctionnement est le même : -on récupère le champ ``information_json`` du modèle ``Bus`` qu'on convertit en dictionnaire puis en objet Python. -Cet objet est en lecture uniquement, on modifie à la main les paramètres d'un bus. - -Le champ ``bus`` est fourni. - -WEISurveyAlgorithm -"""""""""""""""""" - -Une classe héritant de ``wei.forms.surveys.base.WEISurveyAlgorithm``, qui contient 3 fonctions : - -* ``get_survey_class(cls)``, qui renvoie la classe du ``WEISurvey`` associée à l'algorithme. -* ``get_bus_information_class(cls)`` qui renvoie la classe du ``WEIBusInformation`` décrivant les informations d' -* ``run_algorithm(self)``, la fonction importante. Cette fonction n'est supposée n'être exécutée qu'une seule fois - par WEI, et a pour cahier des charges de prendre chaque sondage d'un 1A et d'appeler la fonction ``WEISurvey.select_bus(bus)``, - en décidant convenablement de quel bus le membre doit prendre. C'est bien sûr la fonction la plus complexe à mettre en oeuvre. - Tout est permis tant qu'à la fin tout le monde a bien son bus. - -Trois fonctions sont implémentées nativement : - -* ``get_registrations(cls)``, renvoie un ``QuerySSet`` vers l'ensemble des inscriptions au WEI concerné des 1A. -* ``get_buses(cls)``, renvoie l'ensemble des bus du WEI concerné. -* ``get_bus_information(cls, bus)``, renvoie l'objet ``WEIBusInformation`` instancié avec les informations fournies - par le champ ``information_json`` de ``bus``. - - -La dernière chose à faire est dans le fichier ``apps/wei/forms/surveys/__init__.py``, où la classe ``CurrentSurvey`` -est à mettre à jour. Il n'y a rien d'autre à changer, tout le reste est normalement géré pour qu'il n'y ait pas nécessité -d'y toucher. - -Le lancement de l'algorithme se fait en ligne de commande, via la commande ``python manage.py wei_algorithm``. Elle a -pour unique effet d'appeler la fonction ``run_algorithm`` décrite plus tôt. Une fois cela fait, vous aurez noté qu'il -n'a pas été évoqué d'adhésion. L'adhésion est ensuite manuelle, l'algorithme ne fournit qu'une suggestion. - -Cette structure, complexe mais raisonnable, permet de gérer plus ou moins proprement la répartition des 1A, -en limitant très fortement le hard code. Ami nouveau développeur, merci de bien penser à la propreté du code :) -En particulier, on évitera de mentionner dans le code le nom des bus, et profiter du champ ``information_json`` -présent dans le modèle ``Bus``. - -Valider les inscriptions -~~~~~~~~~~~~~~~~~~~~~~~~ - -Cette partie est moins technique. - -Une fois la pré-inscription faite, elle doit être validée par le BDE, afin de procéder au paiement. Le GC WEI a accès à -la liste des inscriptions non validées, soit sur la page de détails du WEI, soit sur un tableau plus large avec filtre. -Une inscription non validée peut soit être validée, soit supprimée (la suppression est irréversible). - -Lorsque le GC WEI veut valider une inscription, il a accès au récapitulatif de l'inscription ainsi qu'aux informations -personnelles de l'utilisateur. Il lui est proposé de les modifier si besoin (du moins les informations liées au WEI, -pas les informations personnelles). Il a enfin accès aux résultats du sondage et la sortie de l'algorithme s'il s'agit -d'un 1A, aux préférences d'un 2A+. Avant de valider, le GC WEI doit sélectionner un bus, éventuellement une équipe -et un rôle. Si c'est un 1A et que l'algorithme a tourné, ou si c'est un 2A+ qui n'a fait qu'un seul choix de bus, -d'équipe, de rôles, les champs sont automatiquement pré-remplis. - -Quelques restrictions cependant : - -* Si c'est un 2A+, le chèque de caution doit être déclaré déposé -* Si l'inscription se fait via la Société générale, un message expliquant la situation apparaît : la transaction de - paiement sera créée mais invalidée, les trésoriers devront confirmer plus tard sur leur interface que le compte - à la Société générale a bien été créé avant de valider la transaction (voir `Trésorerie `_ section - Crédit de la Société générale). -* Dans le cas contraire, l'utilisateur doit avoir le solde nécessaire sur sa note avant de pouvoir adhérer. -* L'utilisateur doit enfin être membre du club Kfet. Un lien est présent pour le faire adhérer ou réadhérer selon le cas. - -Si tout est bon, le GC WEI peut valider. L'utilisateur a bien payé son WEI, et son interface est un peu plus grande. -Il peut toujours changer ses paramètres au besoin. Un 1A ne voit rien de plus avant la fin du WEI. - -Un adhérent WEI non 1A a accès à la liste des bus, des équipes et de leur descriptions. Les chefs de bus peuvent gérer -les bus et leurs équipes. Les chefs d'équipe peuvent gérer leurs équipes. Cela inclut avoir accès à la liste des membres -de ce bus / de cette équipe. - -Un export au format PDF de la liste des membres *visibles* est disponible pour chacun. - -Bon WEI à tous ! diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 549399d..2a99908 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -76,7 +76,6 @@ INSTALLED_APPS = [ 'registration', 'scripts', 'treasury', - 'wei', ] MIDDLEWARE = [ diff --git a/note_kfet/templates/base.html b/note_kfet/templates/base.html index 0466360..ee99016 100644 --- a/note_kfet/templates/base.html +++ b/note_kfet/templates/base.html @@ -96,12 +96,6 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Treasury' %} {% endif %} - {% if "wei.weiclub"|not_empty_model_list %} - - {% endif %} {% if request.user.is_authenticated %}