remove wei

This commit is contained in:
Marsupilami1 2022-07-31 14:16:24 +02:00
parent 94f5788922
commit 2343eabb59
64 changed files with 8 additions and 6945 deletions

1
.gitignore vendored
View file

@ -55,4 +55,3 @@ ansible/host_vars/*.yaml
ansible/hosts ansible/hosts
apps/member/migrations apps/member/migrations
apps/wei/migrations

View file

@ -38,10 +38,6 @@ if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls from logs.api.urls import register_logs_urls
register_logs_urls(router, 'logs') 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' app_name = 'api'
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.

View file

@ -227,7 +227,7 @@ class MembershipRolesForm(forms.ModelForm):
) )
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=Role.objects.filter(weirole=None).all(), queryset=Role.objects.all(),
label=_("Roles"), label=_("Roles"),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
) )

View file

@ -51,7 +51,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if request.path_info != user_profile_url %} {% if request.path_info != user_profile_url %}
<a class="btn btn-sm btn-primary" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-sm btn-primary" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a>
{% endif %} {% endif %}
{% elif club and not club.weiclub %} {% elif club %}
{% if can_add_members %} {% if can_add_members %}
<a class="btn btn-sm btn-success" href="{% url 'member:club_add_member' club_pk=club.pk %}" <a class="btn btn-sm btn-success" href="{% url 'member:club_add_member' club_pk=club.pk %}"
data-turbolinks="false"> {% trans "Add member" %}</a> data-turbolinks="false"> {% trans "Add member" %}</a>

View file

@ -161,7 +161,7 @@ class TestMemberships(TestCase):
response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles=" response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles="
+ ",".join([str(role.pk) for role in + ",".join([str(role.pk) for role in
Role.objects.filter(weirole__isnull=True).all()])) Role.objects.all()]))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_render_club_add_member(self): def test_render_club_add_member(self):

View file

@ -541,11 +541,6 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset(**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 return qs
def get_success_url(self): def get_success_url(self):
@ -597,7 +592,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if "club_pk" in self.kwargs: # We create a new membership. if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\ 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 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. # Ensure that the user is member of the parent club and all its the family tree.
c = club c = club
@ -819,8 +814,7 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form = super().get_form(form_class) form = super().get_form(form_class)
club = self.object.club club = self.object.club
form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) form.fields['roles'].queryset = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all()
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
return form return form
@ -866,8 +860,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
).get(pk=self.kwargs["pk"]) ).get(pk=self.kwargs["pk"])
context["club"] = club context["club"] = club
applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) applicable_roles = Role.objects.filter((Q(for_club__isnull=True) | Q(for_club=club))).all()
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
context["applicable_roles"] = applicable_roles context["applicable_roles"] = applicable_roles
context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'

View file

@ -2452,10 +2452,5 @@
181 181
] ]
} }
},
{
"model": "wei.weirole",
"pk": 17,
"fields": {}
} }
] ]

View file

@ -40,7 +40,7 @@ class RightsTable(tables.Table):
| Q(name="Adhérent Kfet") | Q(name="Adhérent Kfet")
| Q(name="Membre de club") | Q(name="Membre de club")
| Q(name="Bureau de club")) | Q(name="Bureau de club"))
& Q(weirole__isnull=True))).all() )).all()
s = ", ".join(str(role) for role in roles) s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record): if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk})) s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))

View file

@ -63,7 +63,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
data-target="#collapse{{ role|slugify }}" data-target="#collapse{{ role|slugify }}"
aria-expanded="true" aria-controls="collapse{{ role|slugify }}"> aria-expanded="true" aria-controls="collapse{{ role|slugify }}">
{{ role }} {{ role }}
{% if role.weirole %}(<em>Pour le WEI</em>){% endif %}
{% if role.for_club %}(<em>Pour le club {{ role.for_club }} uniquement</em>){% endif %} {% if role.for_club %}(<em>Pour le club {{ role.for_club }} uniquement</em>){% endif %}
{% if role.clubs %} {% if role.clubs %}
<small><span class="badge badge-success">{% trans "Owned" %} : <small><span class="badge badge-success">{% trans "Owned" %} :

View file

@ -135,7 +135,7 @@ class RightsView(TemplateView):
| Q(name="Adhérent Kfet") | Q(name="Adhérent Kfet")
| Q(name="Membre de club") | Q(name="Membre de club")
| Q(name="Bureau de club")) | Q(name="Bureau de club"))
& Q(weirole__isnull=True))))\ )))\
.order_by("club__name", "user__last_name")\ .order_by("club__name", "user__last_name")\
.distinct().all() .distinct().all()
context["special_memberships_table"] = RightsTable(special_memberships, prefix="clubs-") context["special_memberships_table"] = RightsTable(special_memberships, prefix="clubs-")

View file

@ -54,15 +54,6 @@ class DeclareSogeAccountOpenedForm(forms.Form):
) )
class WEISignupForm(forms.Form):
wei_registration = forms.BooleanField(
label=_("Register to the WEI"),
required=False,
help_text=_("Check this case if you want to register to the WEI. If you hesitate, you will be able to register"
" later, after validating your account in the Kfet."),
)
class ValidationForm(forms.Form): class ValidationForm(forms.Form):
""" """
Validate the inscription of the new users and pay memberships. Validate the inscription of the new users and pay memberships.

View file

@ -308,12 +308,6 @@ class SogeCredit(models.Model):
if self.valid: if self.valid:
return self.credit_transaction.total return self.credit_transaction.total
amount = sum(transaction.total for transaction in self.transactions.all()) amount = sum(transaction.total for transaction in self.transactions.all())
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIMembership
if not WEIMembership.objects.filter(club__weiclub__year=datetime.date.today().year, user=self.user)\
.exists():
# 80 € for people that don't go to WEI
amount += 8000
return amount return amount
def update_transactions(self): def update_transactions(self):
@ -341,16 +335,6 @@ class SogeCredit(models.Model):
if m.transaction not in self.transactions.all(): if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction) self.transactions.add(m.transaction)
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIClub
wei = WEIClub.objects.order_by('-year').first()
wei_qs = Membership.objects.filter(user=self.user, club=wei, date_start__gte=wei.membership_start)
if wei_qs.exists():
m = wei_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction)
for tr in self.transactions.all(): for tr in self.transactions.all():
tr.valid = False tr.valid = False
tr.save() tr.save()

View file

@ -1,4 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'wei.apps.WeiConfig'

View file

@ -1,13 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from note_kfet.admin import admin_site
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
admin_site.register(WEIClub)
admin_site.register(WEIRegistration)
admin_site.register(WEIMembership)
admin_site.register(WEIRole)
admin_site.register(Bus)
admin_site.register(BusTeam)

View file

@ -1,72 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
class WEIClubSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Clubs.
The djangorestframework plugin will analyse the model `WEIClub` and parse all fields in the API.
"""
class Meta:
model = WEIClub
fields = '__all__'
class BusSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Bus.
The djangorestframework plugin will analyse the model `Bus` and parse all fields in the API.
"""
class Meta:
model = Bus
fields = '__all__'
class BusTeamSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Bus teams.
The djangorestframework plugin will analyse the model `BusTeam` and parse all fields in the API.
"""
class Meta:
model = BusTeam
fields = '__all__'
class WEIRoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI roles.
The djangorestframework plugin will analyse the model `WEIRole` and parse all fields in the API.
"""
class Meta:
model = WEIRole
fields = '__all__'
class WEIRegistrationSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI registrations.
The djangorestframework plugin will analyse the model `WEIRegistration` and parse all fields in the API.
"""
class Meta:
model = WEIRegistration
fields = '__all__'
class WEIMembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for WEI memberships.
The djangorestframework plugin will analyse the model `WEIMembership` and parse all fields in the API.
"""
class Meta:
model = WEIMembership
fields = '__all__'

View file

@ -1,17 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import WEIClubViewSet, BusViewSet, BusTeamViewSet, WEIRoleViewSet, WEIRegistrationViewSet, \
WEIMembershipViewSet
def register_wei_urls(router, path):
"""
Configure router for Member REST API.
"""
router.register(path + '/club', WEIClubViewSet)
router.register(path + '/bus', BusViewSet)
router.register(path + '/team', BusTeamViewSet)
router.register(path + '/role', WEIRoleViewSet)
router.register(path + '/registration', WEIRegistrationViewSet)
router.register(path + '/membership', WEIMembershipViewSet)

View file

@ -1,105 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
WEIRegistrationSerializer, WEIMembershipSerializer
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
class WEIClubViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/club/
"""
queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['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', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
class BusViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/bus/
"""
queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'wei', 'description', ]
search_fields = ['$name', '$wei__name', '$description', ]
class BusTeamViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/team/
"""
queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
class WEIRoleViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/role/
"""
queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ]
search_fields = ['$name', ]
class WEIRegistrationViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
then render it on /api/wei/registration/
"""
queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['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', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', '$wei__name',
'$wei__email', '$health_issues', '$emergency_contact_name', '$emergency_contact_phone', ]
class WEIMembershipViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/membership/
"""
queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['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', ]
ordering_fields = ['id', 'date_start', 'date_end', ]
search_fields = ['$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', '$roles__name', '$bus__name', '$team__name', ]

View file

@ -1,10 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class WeiConfig(AppConfig):
name = 'wei'
verbose_name = _('WEI')

View file

@ -1,10 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .registration import WEIForm, WEIRegistrationForm, WEIMembership1AForm, WEIMembershipForm, BusForm, BusTeamForm
from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey
__all__ = [
'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]

View file

@ -1,189 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
class WEIForm(forms.ModelForm):
class Meta:
model = WEIClub
exclude = ('parent_club', 'require_memberships', 'membership_duration', )
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
"membership_start": DatePickerInput(),
"membership_end": DatePickerInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
}
class WEIRegistrationForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if 'user' in cleaned_data:
if not NoteUser.objects.filter(user=cleaned_data['user']).exists():
self.add_error('user', _("The selected user is not validated. Please validate its account first"))
return cleaned_data
class Meta:
model = WEIRegistration
exclude = ('wei', )
widgets = {
"user": Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
),
"birth_date": DatePickerInput(options={'minDate': '1900-01-01',
'maxDate': '2100-01-01'}),
}
class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects,
label=_("bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
+ " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(),
)
team = forms.ModelMultipleChoiceField(
queryset=BusTeam.objects,
label=_("Team"),
required=False,
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
widget=CheckboxSelectMultiple(),
)
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent WEI").all(),
widget=CheckboxSelectMultiple(),
)
class WEIMembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects,
label=_("WEI Roles"),
widget=CheckboxSelectMultiple(),
)
credit_type = forms.ModelChoiceField(
queryset=NoteSpecial.objects.all(),
label=_("Credit type"),
empty_label=_("No credit"),
required=False,
)
credit_amount = forms.IntegerField(
label=_("Credit amount"),
widget=AmountInput(),
initial=0,
required=False,
)
last_name = forms.CharField(
label=_("Last name"),
required=False,
)
first_name = forms.CharField(
label=_("First name"),
required=False,
)
bank = forms.CharField(
label=_("Bank"),
required=False,
)
def clean(self):
cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \
and cleaned_data["team"].bus != cleaned_data["bus"]:
self.add_error('bus', _("This team doesn't belong to the given bus."))
return cleaned_data
class Meta:
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
}
class WEIMembership1AForm(WEIMembershipForm):
"""
Used to confirm registrations of first year members without choosing a bus now.
"""
roles = None
def clean(self):
return super(forms.ModelForm, self).clean()
class Meta:
model = WEIMembership
fields = ('credit_type', 'credit_amount', 'last_name', 'first_name', 'bank',)
class BusForm(forms.ModelForm):
class Meta:
model = Bus
fields = '__all__'
widgets = {
"wei": Autocomplete(
WEIClub,
attrs={
'api_url': '/api/wei/club/',
'placeholder': 'WEI ...',
},
),
}
class BusTeamForm(forms.ModelForm):
class Meta:
model = BusTeam
fields = '__all__'
widgets = {
"bus": Autocomplete(
Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
},
),
"color": ColorWidget(),
}

View file

@ -1,12 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from .wei2022 import WEISurvey2022
__all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]
CurrentSurvey = WEISurvey2022

View file

@ -1,237 +0,0 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional, List
from django.db.models import QuerySet
from django.forms import Form
from ...models import WEIClub, WEIRegistration, Bus, WEIMembership
class WEISurveyInformation:
"""
Abstract data of the survey.
"""
valid = False
selected_bus_pk = None
selected_bus_name = None
def __init__(self, registration):
self.__dict__.update(registration.information)
def get_selected_bus(self) -> 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

View file

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

View file

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

View file

@ -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 "<None>", 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)

View file

@ -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!")

View file

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

View file

@ -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')},
),
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

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

View file

@ -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("<span class='no-perm'></span>")
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"<a class=\"btn {btn_class}\" data-type='validate-membership' data-toggle=\"tooltip\" "
f"title=\"{tooltip}\" href=\"{url}\">{text}</a>")
def render_delete(self, record):
hasperm = PermissionBackend.check_perm(get_current_request(), "wei.delete_weimembership", record)
return _("Delete") if hasperm else format_html("<span class='no-perm'></span>")
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),
}

View file

@ -1,20 +0,0 @@
{% extends "wei/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block profile_content %}
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Attribute first year members into buses" %}</h3>
</div>
<div class="card-body">
{% render_table bus_repartition_table %}
<hr>
<a href="{% url 'wei:wei_bus_1A_next' pk=club.pk %}" class="btn btn-block btn-success">{% trans "Start attribution!" %}</a>
<hr>
{% render_table table %}
</div>
</div>
{% endblock %}

View file

@ -1,88 +0,0 @@
{% extends "wei/base.html" %}
{% load i18n %}
{% block profile_content %}
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Bus attribution" %}</h3>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user }}</dd>
<dt class="col-xl-6">{% trans 'last name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.last_name }}</dd>
<dt class="col-xl-6">{% trans 'first name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.first_name }}</dd>
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.get_gender_display }}</dd>
<dt class="col-xl-6">{% trans 'department'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.profile.get_department_display }}</dd>
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.health_issues|default:"—" }}</dd>
<dt class="col-xl-6">{% trans 'suggested bus'|capfirst %}</dt>
<dd class="col-xl-6">{{ survey.information.selected_bus_name }}</dd>
</dl>
<div class="card">
<div class="card-header">
<button class="btn btn-link" data-toggle="collapse" data-target="#raw-survey">{% trans "View raw survey information" %}</button>
</div>
<div class="collapse" id="raw-survey">
<dl class="row">
{% for key, value in survey.registration.information.items %}
<dt class="col-xl-6">{{ key }}</dt>
<dd class="col-xl-6">{{ value }}</dd>
{% endfor %}
</dl>
</div>
</div>
<hr>
{% for bus, score in survey.ordered_buses %}
<button class="btn btn-{% if bus.pk == survey.information.selected_bus_pk %}success{% else %}light{% endif %}" onclick="choose_bus({{ bus.pk }})">
{{ bus }} ({{ score|floatformat:2 }}) : {{ bus.memberships.count }}+{{ bus.suggested_first_year }} / {{ bus.size }}
</button>
{% endfor %}
<a href="{% url 'wei:wei_1A_list' pk=object.wei.pk %}" class="btn btn-block btn-info">{% trans "Back to main list" %}</a>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function choose_bus(bus_id) {
let valid_buses = [{% for bus, score in survey.ordered_buses %}{{ bus.pk }}, {% endfor %}];
if (valid_buses.indexOf(bus_id) === -1) {
console.log("Invalid chosen bus")
return
}
$.ajax({
url: "/api/wei/membership/{{ object.membership.id }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
bus: bus_id,
}
}).done(function () {
window.location = "{% url 'wei:wei_bus_1A_next' pk=object.wei.pk %}"
}).fail(function (xhr) {
errMsg(xhr.responseJSON)
})
}
</script>
{% endblock %}

View file

@ -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 %}
<div class="row mt-4">
<div class="col-xl-4">
{% block profile_info %}
{% if club %}
<div class="card bg-light">
<h4 class="card-header text-center">
{{ club.name }}
</h4>
<div class="card-top text-center">
<a href="{% url 'member:club_update_pic' club.pk %}">
<img src="{{ club.note.display_image.url }}" class="img-thumbnail mt-2">
</a>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.name }}</dd>
{% if club.require_memberships %}
<dt class="col-xl-6">{% trans 'date start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.date_start }}</dd>
<dt class="col-xl-6">{% trans 'date end'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.date_end }}</dd>
<dt class="col-xl-6">{% trans 'year'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.year }}</dd>
{% if club.membership_fee_paid == club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %}
{% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %}
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
<i class="fa fa-question-circle"
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
{% endwith %}
{% endif %}
{% endif %}
{% if "note.view_note"|has_perm:club.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
{% endif %}
{% if "note.change_alias"|has_perm:club.note.alias.first %}
<dt class="col-xl-4"><a
href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-8 text-truncate">{{ club.note.alias.all|join:", " }}</dd>
{% endif %}
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-8"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
</dl>
</div>
<div class="card-footer text-center">
{% if True %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_list' %}"> {% trans "WEI list" %}</a>
{% endif %}
{% if club.is_current_wei %}
{% if can_add_first_year_member %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_register_1A' wei_pk=club.pk %}"
data-turbolinks="false"> {% trans "Register 1A" %}</a>
{% endif %}
{% if can_add_any_member %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_register_2A' wei_pk=club.pk %}"
data-turbolinks="false"> {% trans "Register 2A+" %}</a>
{% endif %}
{% if "wei.change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_update' pk=club.pk %}"
data-turbolinks="false"> {% trans "Edit" %}</a>
{% endif %}
{% if can_add_bus %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_bus' pk=club.pk %}"
data-turbolinks="false"> {% trans "Add bus" %}</a>
{% endif %}
{% url 'wei:wei_detail' club.pk as club_detail_url %}
{%if request.path_info != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View WEI' %}</a>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
</div>
<div class="col-xl-8">
{% block profile_content %}{% endblock %}
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-header text-center">
<h4>{{ object.name }}</h4>
</div>
<div class="card-body">
{{ object.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>
</div>
<hr>
{% if teams.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>
{% render_table teams %}
</div>
<hr>
{% endif %}
{% if memberships.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Members" %}
</a>
</div>
{% render_table memberships %}
</div>
<hr>
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
</a>
{% endif %}
{% endblock %}

View file

@ -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 %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-header text-center">
<h4>{{ bus.name }}</h4>
</div>
<div class="card-body">
{{ bus.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a>
</div>
</div>
<hr>
<div class="card">
<div class="card-header text-center"
style="background-color: #{{ object.color|stringformat:"06X" }}; color: #{{ -16777215|add:object.color|stringformat:"06X"|slice:"1:" }};">
<h4>{{ object.name }}</h4>
</div>
<div class="card-body">
{{ object.description }}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1"
href="{% url 'wei:update_bus_team' pk=object.pk %}">{% trans "Edit" %}</a>
</div>
</div>
<hr>
{% if memberships.data or True %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %}
</a>
</div>
{% render_table memberships %}
</div>
<hr>
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}"
data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
</a>
{% endif %}
{% endblock %}

View file

@ -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 %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-header text-center">
<h4>{% trans "Survey WEI" %}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user }}</dd>
</dl>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="card-footer text-center">
<input class="btn btn-success" type="submit" value="{% trans "Next" %}"/>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-header text-center">
<h4>{% trans "Survey WEI" %}</h4>
</div>
<div class="card-body">
<p>
{% trans "The inscription for this WEI are now closed." %}
</p>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_detail' pk=club.pk %}">{% trans "Return to WEI detail" %}</a>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-header text-center">
<h4>{% trans "Survey WEI" %}</h4>
</div>
<div class="card-body">
<p>
{% trans "The survey is now ended. Your answers have been saved." %}
</p>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="card bg-white mb-3">
<div class="card-header text-center">
<h4>Week-End d'Intégration</h4>
</div>
<div class="card-body">
<p class="lead font-italic">
Le WEI (Week-End dIntégration), ou 3 jours dimmersion dans les profondeurs du
monde post-préparatoire.
</p>
<p>
Que serait une école sans son week-end dinté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. Lobjectif 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 lambiance familiale de lENS ainsi que de nouer des liens avec
ceux·elles quils côtoieront par la suite. Dose de chants et de fun garantie !
</p>
</div>
{% if club.is_current_wei %}
<div class="card-footer text-center">
{% if not my_registration %}
{% if not not_first_year %}
<a class="btn btn-success" href="{% url "wei:wei_register_1A_myself" wei_pk=club.pk %}" data-turbolinks="false">
{% trans "Register to the WEI! 1A" %}
</a>
{% endif %}
<a class="btn btn-success" href="{% url "wei:wei_register_2A_myself" wei_pk=club.pk %}" data-turbolinks="false">
{% trans "Register to the WEI! 2A+" %}</a>
{% else %}
<a class="btn btn-warning" href="{% url "wei:wei_update_registration" pk=my_registration.pk %}"
data-turbolinks="false">
{% trans "Update my registration" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% if buses.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="clubListHeading">
<span class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Buses" %}
</span>
</div>
{% render_table buses %}
</div>
{% endif %}
{% if member_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="clubListHeading">
<a class="stretched-link font-weight-bold text-decoration-none"
href="{% url "wei:wei_memberships" pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Members of the WEI" %}
</a>
</div>
{% render_table member_list %}
</div>
{% endif %}
{% if history_list.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>
<div id="history_list">
{% render_table history_list %}
</div>
</div>
{% endif %}
{% if pre_registrations.data %}
<div class="card bg-white mb-3">
<div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none"
href="{% url 'wei:wei_registrations' pk=club.pk %}">
<i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %}
</a>
</div>
<div id="history_list">
{% render_table pre_registrations %}
</div>
</div>
{% endif %}
{% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %}
{% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'wei:wei_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'wei:wei_detail' pk=object.pk %} #profile_infos");
}
$(document).ready(function () {
$(".no-perm").parent().addClass("d-none");
if ($("a[data-type='validate-membership']:not(.d-none)").length === 0) {
$("a[data-type='validate-membership']").parent().addClass("d-none");
$("#validate-membership-header").addClass("d-none");
}
if ($("a[data-type='delete-membership']:not(.d-none)").length === 0) {
$("a[data-type='delete-membership']").parent().addClass("d-none");
$("#delete-membership-header").addClass("d-none");
}
});
</script>
{% endblock %}

View file

@ -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 %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="row justify-content-center mb-4">
<div class="col-md-10 text-center">
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved()" id="search_field"/>
{% if can_create_wei %}
<hr>
<a class="btn btn-primary text-center my-4" href="{% url 'wei:wei_create' %}">{% trans "Create WEI" %}</a>
{% endif %}
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card card-border shadow">
<div class="card-header text-center">
<h5> {% trans "WEI listing" %}</h5>
</div>
<div class="card-body px-0 py-0" id="club_table">
{% render_table table %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
function getInfo() {
var asked = $("#search_field").val();
/* on ne fait la requête que si on a au moins un caractère pour chercher */
var sel = $(".table-row");
if (asked.length >= 1) {
$.getJSON("/api/wei/club/?format=json&search="+asked, function(buttons){
let selected_id = buttons.results.map((a => "#row-"+a.id));
$(".table-row,"+selected_id.join()).show();
$(".table-row").not(selected_id.join()).hide();
});
}else{
// show everything
$('table tr').show();
}
}
var timer;
var timer_on;
/* Fontion appelée quand le texte change (délenche le timer) */
function search_field_moved(secondfield) {
if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur.
clearTimeout(timer);
timer = setTimeout("getInfo(" + secondfield + ")", 300);
}
else { // Sinon, on le lance et on enregistre le fait qu'il tourne.
timer = setTimeout("getInfo(" + secondfield + ")", 300);
timer_on = true;
}
}
// clickable row
$(document).ready(function($) {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
});
</script>
{% endblock %}

View file

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

View file

@ -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 %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Review registration" %}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ registration.user.last_name }} {{ registration.user.first_name }}</dd>
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.username }}</dd>
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ registration.user.email }}">{{ registration.user.email }}</a></dd>
{% if not registration.user.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:registration.user.profile %}
<dd class="col-xl-12">
<div class="alert alert-warning">
{% trans "This user doesn't have confirmed his/her e-mail address." %}
<a href="{% url "registration:email_validation_resend" pk=registration.user.pk %}">{% trans "Click here to resend a validation link." %}</a>
</div>
</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'department'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.department }}</dd>
<dt class="col-xl-6">{% trans 'ENS year'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.ens_year }}</dd>
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.section }}</dd>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.phone_number }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.user.profile.paid|yesno }}</dd>
<hr>
<dt class="col-xl-6">{% trans 'first year'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.first_year|yesno }}</dd>
<dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.get_gender_display }}</dd>
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
<dt class="col-xl-6">{% trans 'clothing size'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_size }}</dd>
<dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.birth_date }}</dd>
<dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.health_issues }}</dd>
<dt class="col-xl-6">{% trans 'emergency contact name'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.emergency_contact_name }}</dd>
<dt class="col-xl-6">{% trans 'emergency contact phone'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.emergency_contact_phone }}</dd>
<dt class="col-xl-6">{% trans 'Payment from Société générale' %}</dt>
<dd class="col-xl-6">{{ registration.soge_credit|yesno }}</dd>
{% if registration.first_year %}
<dt class="col-xl-6">{% trans 'Suggested bus from the survey:' %}</dt>
{% if registration.information.valid or True %}
<dd class="col-xl-6">{{ suggested_bus }}</dd>
<div class="card-header text-center col-xl-12">
<h5>{% trans 'Raw survey information' %}</h5>
</div>
{% with information=registration.information %}
{% for key, value in information.items %}
<dt class="col-xl-6">{{ key }}</dt>
<dd class="col-xl-6">{{ value }}</dd>
{% endfor %}
{% endwith %}
{% else %}
<dd class="col-xl-6"><em>{% trans "The algorithm didn't run." %}</em></dd>
{% endif %}
{% else %}
<dt class="col-xl-6">{% trans 'caution check given'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.caution_check|yesno }}</dd>
{% with information=registration.information %}
<dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt>
<dd class="col-xl-6">{{ information.preferred_bus_name|join:', ' }}</dd>
<dt class="col-xl-6">{% trans 'preferred team'|capfirst %}</dt>
<dd class="col-xl-6">{{ information.preferred_team_name|join:', ' }}</dd>
<dt class="col-xl-6">{% trans 'preferred roles'|capfirst %}</dt>
<dd class="col-xl-6">{{ information.preferred_roles_name|join:', ' }}</dd>
{% endwith %}
{% endif %}
</dl>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm" href="{% url 'wei:wei_update_registration' registration.pk %}" data-turbolinks="false">{% trans 'Update registration' %}</a>
{% if "auth.change_user"|has_perm:registration.user %}
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' registration.user.pk %}">{% trans 'Update Profile' %}</a>
{% endif %}
</div>
</div>
<hr>
<div class="card bg-light shadow">
<form method="post">
<div class="card-header text-center" >
<h4> {% trans "Validate registration" %}</h4>
</div>
{% if registration.is_validated %}
<div class="alert alert-warning">
{% 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:", " }}
</div>
{% else %}
{% if registration.soge_credit %}
<div class="alert alert-warning">
{% 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 %}
</div>
{% else %}
{% if registration.user.note.balance < fee %}
<div class="alert alert-danger">
{% 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 %}
</div>
{% else %}
<div class="alert alert-success">
{% blocktrans trimmed with pretty_fee=fee|pretty_money %}
The note has enough money ({{ pretty_fee }} required), the registration is possible.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}
{% if not registration.caution_check and not registration.first_year %}
<div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %}
</div>
{% endif %}
{% if not kfet_member %}
<div class="alert alert-warning">
{% 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 %}
</div>
{% endif %}
<div class="card-body" id="profile_infos">
{% csrf_token %}
{{ form|crispy }}
</div>
<div class="card-footer text-center">
<button class="btn btn-success btn-sm">{% trans 'Validate registration' %}</button>
</div>
{% endif %}
</form>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function autocompleted(obj, prefix) {
console.log(prefix);
if (prefix === "id_bus") {
console.log(obj);
$("#id_team").attr('api_url', '/api/wei/team/?bus=' + obj.id);
}
}
</script>
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-body">
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/bus/équipe ...">
<hr>
<div id="memberships_table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no membership found with this pattern." %}
</div>
{% endif %}
</div>
</div>
<div class="card-footer text-center">
<a href="{% url 'wei:wei_registrations' pk=club.pk %}">
<button class="btn btn-block btn-info">{% trans "View unvalidated registrations..." %}</button>
</a>
<hr>
<a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button>
</a>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#searchbar");
function reloadTable() {
let pattern = searchbar_obj.val();
if (pattern === old_pattern)
return;
$("#memberships_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #memberships_table");
}
searchbar_obj.keyup(reloadTable);
});
</script>
{% endblock %}

View file

@ -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 %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{% trans "Delete registration" %}</h4>
</div>
{% if object.is_validated %}
<div class="card-body">
<div class="alert alert-danger">
{% blocktrans %}This registration is already validated and can't be deleted.{% endblocktrans %}
</div>
</div>
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% 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 %}
</div>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<a class="btn btn-warning" href="{% url 'wei:wei_update_registration' object.pk %}">{% trans "Update registration" %}</a>
<button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
</form>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -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 %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ membership_form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
{% if not object.membership %}
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
console.log(buses);
$("input[name='team']").each(function () {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function (bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endif %}
{% endblock %}

View file

@ -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 %}
<div class="card">
<div class="card-body">
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ...">
<hr>
<div id="registrations_table">
{% if table.data %}
{% render_table table %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no pre-registration found with this pattern." %}
</div>
{% endif %}
</div>
</div>
<div class="card-footer text-center">
<a href="{% url 'wei:wei_memberships' pk=club.pk %}">
<button class="btn btn-block btn-info">{% trans "View validated memberships..." %}</button>
</a>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#searchbar");
function reloadTable() {
let pattern = searchbar_obj.val();
if (pattern === old_pattern)
return;
$("#registrations_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #registrations_table");
}
searchbar_obj.keyup(reloadTable);
$(".no-perm").parent().addClass("d-none");
if ($("a[data-type='validate-membership']:not(.d-none)").length === 0) {
$("a[data-type='validate-membership']").parent().addClass("d-none");
$("#validate-membership-header").addClass("d-none");
}
if ($("a[data-type='delete-membership']:not(.d-none)").length === 0) {
$("a[data-type='delete-membership']").parent().addClass("d-none");
$("#delete-membership-header").addClass("d-none");
}
});
</script>
{% endblock %}

View file

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

View file

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

View file

@ -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&#39;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&#39;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/")

View file

@ -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/<int:pk>/', WEIDetailView.as_view(), name="wei_detail"),
path('update/<int:pk>/', WEIUpdateView.as_view(), name="wei_update"),
path('detail/<int:pk>/registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"),
path('detail/<int:pk>/memberships/', WEIMembershipsView.as_view(), name="wei_memberships"),
path('detail/<int:wei_pk>/memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"),
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/', MemberListRenderView.as_view(),
name="wei_memberships_bus_pdf"),
path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(),
name="wei_memberships_team_pdf"),
path('bus-1A/list/<int:pk>/', WEI1AListView.as_view(), name="wei_1A_list"),
path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"),
path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"),
path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"),
path('add-bus-team/<int:pk>/', BusTeamCreateView.as_view(), name="add_team"),
path('manage-bus-team/<int:pk>/', BusTeamManageView.as_view(), name="manage_bus_team"),
path('update-bus-team/<int:pk>/', BusTeamUpdateView.as_view(), name="update_bus_team"),
path('register/<int:wei_pk>/1A/', WEIRegister1AView.as_view(), name="wei_register_1A"),
path('register/<int:wei_pk>/2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"),
path('register/<int:wei_pk>/1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"),
path('register/<int:wei_pk>/2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"),
path('edit-registration/<int:pk>/', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"),
path('delete-registration/<int:pk>/', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"),
path('validate/<int:pk>/', WEIValidateRegistrationView.as_view(), name="validate_registration"),
path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"),
path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"),
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
]

File diff suppressed because it is too large Load diff

View file

@ -1,710 +0,0 @@
API WEI
=======
Wei
---
**Chemin :** `/api/wei/club/ <https://note.crans.org/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/ <https://note.crans.org/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/ <https://note.crans.org/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/ <https://note.crans.org/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/ <https://note.crans.org/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/ <https://note.crans.org/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)

View file

@ -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 <treasury>`_ 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 !

View file

@ -76,7 +76,6 @@ INSTALLED_APPS = [
'registration', 'registration',
'scripts', 'scripts',
'treasury', 'treasury',
'wei',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View file

@ -96,12 +96,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-credit-card"></i> {% trans 'Treasury' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-credit-card"></i> {% trans 'Treasury' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "wei.weiclub"|not_empty_model_list %}
<li class="nav-item">
{% url 'wei:current_wei_detail' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-bus"></i> {% trans 'WEI' %}</a>
</li>
{% endif %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
{% url 'permission:rights' as url %} {% url 'permission:rights' as url %}

View file

@ -20,7 +20,6 @@ urlpatterns = [
path('registration/', include('registration.urls')), path('registration/', include('registration.urls')),
path('activity/', include('activity.urls')), path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')), path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),