1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-22 08:53:28 +02:00

Compare commits

...

28 Commits

Author SHA1 Message Date
85ea43a7cf change pipeline 2025-07-04 16:27:04 +02:00
f54dd30482 fix logout test 2025-07-03 15:18:29 +02:00
7eafe33945 Merge branch 'main' into django-5.2 2025-07-03 14:24:58 +02:00
6edef619aa change requirements.txt 2025-07-03 11:37:07 +02:00
8a1f30ebe2 Merge branch 'wei' into 'main'
Wei

See merge request bde/nk20!325
2025-07-01 18:14:45 +02:00
b2c6b0e85d Sélection de bus/équipe plus ergonomique 2025-07-01 17:48:39 +02:00
1567bc6ce5 Merge branch 'oidc' into 'main'
Oidc

See merge request bde/nk20!324
2025-06-27 22:29:51 +02:00
c411197af3 multiline support for RSA key in env 2025-06-27 22:13:43 +02:00
bc517f02e5 Traduction 2025-06-27 19:26:58 +02:00
e83ee8015f Tests 2025-06-27 18:50:37 +02:00
c26534b6b7 Année et algorithme 2025-06-27 16:56:17 +02:00
cdc6f0a3f8 Fix jwks.json 2025-06-27 12:13:54 +02:00
c153d5f10a Merge branch 'wei' into 'main'
Wei

See merge request bde/nk20!323
2025-06-26 17:08:27 +02:00
3f76ca6472 Tables 1A (et typo) 2025-06-21 17:16:15 +02:00
5c5f579729 Traductions 2025-06-20 14:24:03 +02:00
a6df0e7c69 Autres permissions 2025-06-17 20:51:46 +02:00
763535bea4 Merge branch 'oidc' into 'main'
OIDC 0 Quark 1

See merge request bde/nk20!322
2025-06-17 16:02:40 +02:00
df0d886db9 linters 2025-06-17 11:46:33 +02:00
092cc37320 OIDC 0 Quark 1 2025-06-17 00:38:11 +02:00
16b55e23af Merge branch 'thomasl-main-patch-84944' into 'main'
Update doc about scripts

See merge request bde/nk20!321
2025-06-14 20:24:49 +02:00
97621e8704 Update doc about scripts 2025-06-14 20:07:29 +02:00
cf4c23d1ac Merge branch 'oidc' into 'main'
oidc

See merge request bde/nk20!320
2025-06-14 18:36:24 +02:00
d71105976f oidc 2025-06-14 18:01:42 +02:00
89cc03141b allow search with club name 2025-06-12 18:48:29 +02:00
6822500fdc Correction des tests et autres 2025-06-12 17:39:34 +02:00
63f6528adc Suppression du choix GC WEI dans les roles 2025-06-12 13:59:59 +02:00
40ac1daece Tests et permissions 2025-06-02 17:51:33 +02:00
e617048332 Meilleure gestion des cautions 2025-06-02 01:09:51 +02:00
34 changed files with 2401 additions and 3624 deletions

View File

@ -21,3 +21,6 @@ EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration # Wiki configuration
WIKI_USER=NoteKfet2020 WIKI_USER=NoteKfet2020
WIKI_PASSWORD= WIKI_PASSWORD=
# OIDC
OIDC_RSA_PRIVATE_KEY=CHANGE_ME

View File

@ -8,7 +8,7 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04 # Ubuntu 22.04
py310-django42: py310-django52:
stage: test stage: test
image: ubuntu:22.04 image: ubuntu:22.04
before_script: before_script:
@ -22,10 +22,10 @@ py310-django42:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py310-django42 script: tox -e py310-django52
# Debian Bookworm # Debian Bookworm
py311-django42: py311-django52:
stage: test stage: test
image: debian:bookworm image: debian:bookworm
before_script: before_script:
@ -37,7 +37,7 @@ py311-django42:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py311-django42 script: tox -e py311-django52
linters: linters:
stage: quality-assurance stage: quality-assurance

View File

@ -61,8 +61,8 @@ Bien que cela permette de créer une instance sur toutes les distributions,
6. (Optionnel) **Création d'une clé privée OpenID Connect** 6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et copier la clé dans .env dans le champ
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`). `OIDC_RSA_PRIVATE_KEY`.
7. Enjoy : 7. Enjoy :
@ -237,8 +237,8 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
7. **Création d'une clé privée OpenID Connect** 7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner le champ
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`). `OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/* 8. *Enjoy \o/*

View File

@ -63,7 +63,8 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
valid_regex = is_regex(pattern) valid_regex = is_regex(pattern)
suffix = '__iregex' if valid_regex else '__istartswith' suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else '' prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})) qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
| Q(**{f'owner__name{suffix}': prefix + pattern}))
else: else:
qs = qs.none() qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view')) search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))

View File

@ -44,7 +44,7 @@ class TemplateLoggedInTests(TestCase):
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302) self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self): def test_logout(self):
response = self.client.get(reverse("logout")) response = self.client.post(reverse("logout"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_admin_index(self): def test_admin_index(self):

View File

@ -13,7 +13,7 @@ def register_note_urls(router, path):
router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet) router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet) router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet) router.register(path + '/consumer', ConsumerViewSet, basename='alias2')
router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/transaction', TransactionViewSet)

View File

@ -1695,7 +1695,7 @@
"wei", "wei",
"weimembership" "weimembership"
], ],
"query": "[\"AND\", {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}, [\"OR\", {\"registration__soge_credit\": true}, {\"user__note__balance__gte\": {\"F\": [\"F\", \"fee\"]}}]]", "query": "{\"club\": [\"club\"]}",
"type": "add", "type": "add",
"mask": 2, "mask": 2,
"field": "", "field": "",
@ -4334,6 +4334,22 @@
"description": "Voir mon bus" "description": "Voir mon bus"
} }
}, },
{
"model": "permission.permission",
"pk": 292,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club__pk__lte\": 2}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter un membre au BDE ou à la Kfet"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -4694,6 +4710,8 @@
"name": "GC WEI", "name": "GC WEI",
"permissions": [ "permissions": [
22, 22,
49,
62,
70, 70,
72, 72,
76, 76,
@ -4719,6 +4737,8 @@
113, 113,
128, 128,
130, 130,
142,
269,
271, 271,
272, 272,
273, 273,
@ -4731,7 +4751,8 @@
280, 280,
281, 281,
282, 282,
283 283,
292
] ]
} }
}, },
@ -4755,7 +4776,6 @@
285, 285,
286, 286,
287, 287,
288,
289, 289,
290, 290,
291 291
@ -4961,7 +4981,6 @@
285, 285,
286, 286,
287, 287,
288,
289, 289,
290, 290,
291 291

View File

@ -1,8 +1,10 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes from oauth2_provider.scopes import BaseScopes
from member.models import Club from member.models import Club
from note.models import Alias
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_request
from .backends import PermissionBackend from .backends import PermissionBackend
@ -17,25 +19,46 @@ class PermissionScopes(BaseScopes):
""" """
def get_all_scopes(self): def get_all_scopes(self):
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})" scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
for p in Permission.objects.all() for club in Club.objects.all()} for p in Permission.objects.all() for club in Club.objects.all()}
scopes['openid'] = "OpenID Connect"
return scopes
def get_available_scopes(self, application=None, request=None, *args, **kwargs): def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
return [f"{p.id}_{p.membership.club.id}" scopes = [f"{p.id}_{p.membership.club.id}"
for t in Permission.PERMISSION_TYPES for t in Permission.PERMISSION_TYPES
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])] for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
scopes.append('openid')
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs): def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application: if not application:
return [] return []
return [f"{p.id}_{p.membership.club.id}" scopes = [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')] for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
scopes.append('openid')
return scopes
class PermissionOAuth2Validator(OAuth2Validator): class PermissionOAuth2Validator(OAuth2Validator):
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0 oidc_claim_scope = OAuth2Validator.oidc_claim_scope
oidc_claim_scope.update({"name": 'openid',
"normalized_name": 'openid',
"email": 'openid',
})
def get_additional_claims(self, request):
return {
"name": request.user.username,
"normalized_name": Alias.normalize(request.user.username),
"email": request.user.email,
}
def get_discovery_claims(self, request):
claims = super().get_discovery_claims(self)
return claims + ["name", "normalized_name", "email"]
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
""" """
@ -54,6 +77,8 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes: if scope in scopes:
valid_scopes.add(scope) valid_scopes.add(scope)
request.scopes = valid_scopes if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes return valid_scopes

View File

@ -19,6 +19,7 @@ EXCLUDED = [
'oauth2_provider.accesstoken', 'oauth2_provider.accesstoken',
'oauth2_provider.grant', 'oauth2_provider.grant',
'oauth2_provider.refreshtoken', 'oauth2_provider.refreshtoken',
'oauth2_provider.idtoken',
'sessions.session', 'sessions.session',
] ]

View File

@ -171,7 +171,7 @@ class ScopesView(LoginRequiredMixin, TemplateView):
available_scopes = scopes.get_available_scopes(app) available_scopes = scopes.get_available_scopes(app)
context["scopes"][app] = OrderedDict() context["scopes"][app] = OrderedDict()
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes] items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0]))) # items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
for k, v in items: for k, v in items:
context["scopes"][app][k] = v context["scopes"][app][k] = v

View File

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

View File

@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple, RadioSelect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -24,6 +24,7 @@ class WEIForm(forms.ModelForm):
"membership_end": DatePickerInput(), "membership_end": DatePickerInput(),
"date_start": DatePickerInput(), "date_start": DatePickerInput(),
"date_end": DatePickerInput(), "date_end": DatePickerInput(),
"caution_amount": AmountInput(),
} }
@ -58,12 +59,25 @@ class WEIRegistrationForm(forms.ModelForm):
'maxDate': '2100-01-01' 'maxDate': '2100-01-01'
}), }),
"caution_check": forms.BooleanField( "caution_check": forms.BooleanField(
label=_("I confirm that I have read the caution and that I am aware of the risks involved."),
required=False, required=False,
), ),
} }
class WEIRegistration2AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields + ['caution_type']
widgets = WEIRegistrationForm.Meta.widgets.copy()
widgets.update({
"caution_type": forms.RadioSelect(),
})
class WEIRegistration1AForm(WEIRegistrationForm):
class Meta(WEIRegistrationForm.Meta):
fields = WEIRegistrationForm.Meta.fields
class WEIChooseBusForm(forms.Form): class WEIChooseBusForm(forms.Form):
bus = forms.ModelMultipleChoiceField( bus = forms.ModelMultipleChoiceField(
queryset=Bus.objects, queryset=Bus.objects,
@ -82,7 +96,7 @@ class WEIChooseBusForm(forms.Form):
) )
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A")), queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")),
label=_("WEI Roles"), label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."), help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(), initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
@ -92,7 +106,7 @@ class WEIChooseBusForm(forms.Form):
class WEIMembershipForm(forms.ModelForm): class WEIMembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects, queryset=WEIRole.objects.filter(~Q(name="GC WEI")),
label=_("WEI Roles"), label=_("WEI Roles"),
widget=CheckboxSelectMultiple(), widget=CheckboxSelectMultiple(),
) )
@ -126,6 +140,19 @@ class WEIMembershipForm(forms.ModelForm):
required=False, required=False,
) )
def __init__(self, *args, wei=None, **kwargs):
super().__init__(*args, **kwargs)
if 'bus' in self.fields:
if wei is not None:
self.fields['bus'].queryset = Bus.objects.filter(wei=wei)
else:
self.fields['bus'].queryset = Bus.objects.none()
if 'team' in self.fields:
if wei is not None:
self.fields['team'].queryset = BusTeam.objects.filter(bus__wei=wei)
else:
self.fields['team'].queryset = BusTeam.objects.none()
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \ if 'team' in cleaned_data and cleaned_data["team"] is not None \
@ -137,21 +164,8 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership model = WEIMembership
fields = ('roles', 'bus', 'team',) fields = ('roles', 'bus', 'team',)
widgets = { widgets = {
"bus": Autocomplete( "bus": RadioSelect(),
Bus, "team": RadioSelect(),
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
} }
@ -199,4 +213,3 @@ class BusTeamForm(forms.ModelForm):
), ),
"color": ColorWidget(), "color": ColorWidget(),
} }
# "color": ColorWidget(),

View File

@ -2,11 +2,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from .wei2024 import WEISurvey2024 from .wei2025 import WEISurvey2025
__all__ = [ __all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
] ]
CurrentSurvey = WEISurvey2024 CurrentSurvey = WEISurvey2025

View File

@ -121,6 +121,13 @@ class WEISurveyAlgorithm:
""" """
raise NotImplementedError raise NotImplementedError
@classmethod
def get_bus_information_form(cls):
"""
The class of the form to update the bus information.
"""
raise NotImplementedError
class WEISurvey: class WEISurvey:
""" """

View File

@ -0,0 +1,347 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import time
import json
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, Bus
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 WEISurveyForm2025(forms.Form):
"""
Survey form for the year 2025.
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 = WEISurveyInformation2025(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)
buses = WEISurveyAlgorithm2025.get_buses()
informations = {bus: WEIBusInformation2025(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
if scores:
average_score = sum(scores) / len(scores)
else:
average_score = 0
preferred_words = {bus: [word for word in WORDS
if informations[bus].scores[word] >= average_score]
for bus in buses}
# Correction : proposer plusieurs mots différents à chaque étape
n_choices = 4 # Nombre de mots à proposer à chaque étape
all_preferred_words = set()
for bus_words in preferred_words.values():
all_preferred_words.update(bus_words)
all_preferred_words = list(all_preferred_words)
rng.shuffle(all_preferred_words)
words = all_preferred_words[:n_choices]
self.fields["word"].choices = [(w, w) for w in words]
class WEIBusInformation2025(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
super().__init__(bus)
class BusInformationForm2025(forms.ModelForm):
class Meta:
model = Bus
fields = ['information_json']
widgets = {}
def __init__(self, *args, words=None, **kwargs):
super().__init__(*args, **kwargs)
initial_scores = {}
if self.instance and self.instance.information_json:
try:
info = json.loads(self.instance.information_json)
initial_scores = info.get("scores", {})
except (json.JSONDecodeError, TypeError, AttributeError):
initial_scores = {}
if words is None:
words = WORDS
self.words = words
choices = [(i, str(i)) for i in range(6)] # [(0, '0'), (1, '1'), ..., (5, '5')]
for word in words:
self.fields[word] = forms.TypedChoiceField(
label=word,
choices=choices,
coerce=int,
initial=initial_scores.get(word, 0),
required=True,
widget=forms.RadioSelect,
help_text=_("Rate between 0 and 5."),
)
def clean(self):
cleaned_data = super().clean()
scores = {}
for word in self.words:
value = cleaned_data.get(word)
if value is not None:
scores[word] = value
# On encode en JSON
cleaned_data['information_json'] = json.dumps({"scores": scores})
return cleaned_data
class WEISurveyInformation2025(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 WEISurvey2025(WEISurvey):
"""
Survey for the year 2025.
"""
@classmethod
def get_year(cls):
return 2025
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2025
def get_form_class(self):
return WEISurveyForm2025
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 WEISurveyAlgorithm2025
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 WEISurveyAlgorithm2025(WEISurveyAlgorithm):
"""
The algorithm class for the year 2025.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2025
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2025
@classmethod
def get_bus_information_form(cls):
return BusInformationForm2025
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
WEISurvey2025.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

@ -0,0 +1,23 @@
# Generated by Django 4.2.21 on 2025-06-01 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0012_bus_club'),
]
operations = [
migrations.AddField(
model_name='weiclub',
name='caution_amount',
field=models.PositiveIntegerField(default=0, verbose_name='caution amount'),
),
migrations.AddField(
model_name='weiregistration',
name='caution_type',
field=models.CharField(choices=[('check', 'Check'), ('note', 'Note transaction')], default='check', max_length=16, verbose_name='caution type'),
),
]

View File

@ -33,6 +33,11 @@ class WEIClub(Club):
verbose_name=_("date end"), verbose_name=_("date end"),
) )
caution_amount = models.PositiveIntegerField(
verbose_name=_("caution amount"),
default=0,
)
class Meta: class Meta:
verbose_name = _("WEI") verbose_name = _("WEI")
verbose_name_plural = _("WEI") verbose_name_plural = _("WEI")
@ -197,6 +202,16 @@ class WEIRegistration(models.Model):
verbose_name=_("Caution check given") verbose_name=_("Caution check given")
) )
caution_type = models.CharField(
max_length=16,
choices=(
('check', _("Check")),
('note', _("Note transaction")),
),
default='check',
verbose_name=_("caution type"),
)
birth_date = models.DateField( birth_date = models.DateField(
verbose_name=_("birth date"), verbose_name=_("birth date"),
) )

View File

@ -49,6 +49,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if club.caution_amount > 0 %}
<dt class="col-xl-6">{% trans 'Caution amount'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.caution_amount|pretty_money }}</dd>
{% endif %}
{% if "note.view_note"|has_perm:club.note %} {% if "note.view_note"|has_perm:club.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>

View File

@ -22,6 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit" %}</a> data-turbolinks="false">{% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_info' pk=object.pk %}"
data-turbolinks="false">{% trans "Edit information" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}" <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}"
data-turbolinks="false">{% trans "Add team" %}</a> data-turbolinks="false">{% trans "Add team" %}</a>
</div> </div>

View File

@ -95,9 +95,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
{% endif %} {% endif %}
{% if can_validate_1a %} {% if can_validate_1a %}
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a> <a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}

View File

@ -143,25 +143,35 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% else %} {% else %}
{% if registration.user.note.balance < fee %} <div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}">
<div class="alert alert-danger"> <h5>{% trans "Required payments:" %}</h5>
{% with pretty_fee=fee|pretty_money %} <ul>
{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %} <li>{% blocktrans trimmed with amount=fee|pretty_money %}
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required). Membership fees: {{ amount }}
The registration may fail if you don't credit the note now. {% endblocktrans %}</li>
{% endblocktrans %} {% if registration.caution_type == 'note' %}
{% endwith %} <li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
</div> Deposit (by Note transaction): {{ amount }}
{% else %} {% endblocktrans %}</li>
<div class="alert alert-success"> <li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %}
{% blocktrans trimmed with pretty_fee=fee|pretty_money %} Total needed: {{ total }}
The note has enough money ({{ pretty_fee }} required), the registration is possible. {% endblocktrans %}</strong></li>
{% endblocktrans %} {% else %}
</div> <li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
{% endif %} Deposit (by check): {{ amount }}
{% endblocktrans %}</li>
<li><strong>{% blocktrans trimmed with total=fee|pretty_money %}
Total needed: {{ total }}
{% endblocktrans %}</strong></li>
{% endif %}
</ul>
<p>{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
Current balance: {{ balance }}
{% endblocktrans %}</p>
</div>
{% endif %} {% endif %}
{% if not registration.caution_check and not registration.first_year %} {% if not registration.caution_check and not registration.first_year and registration.caution_type == 'check' %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{% trans "The user didn't give her/his caution check." %} {% trans "The user didn't give her/his caution check." %}
</div> </div>
@ -200,4 +210,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
} }
} }
</script> </script>
<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>
{% endblock %} {% endblock %}

View File

@ -6,8 +6,6 @@ from datetime import date, timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from note.models import NoteUser
from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024 from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024
from ..models import Bus, WEIClub, WEIRegistration from ..models import Bus, WEIClub, WEIRegistration
@ -129,44 +127,3 @@ class TestWEIAlgorithm(TestCase):
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 % self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
def test_register_1a(self):
"""
Test register a first year member to the WEI and complete the survey
"""
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
self.assertEqual(response.status_code, 200)
user = User.objects.create(username="toto", email="toto@example.com")
NoteUser.objects.create(user=user)
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
user=user.id,
soge_credit=True,
birth_date=date(2000, 1, 1),
gender='nonbinary',
clothing_cut='female',
clothing_size='XS',
health_issues='I am a bot',
emergency_contact_name='NoteKfet2020',
emergency_contact_phone='+33123456789',
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
registration = qs.get()
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
for question in WORDS:
# Fill 1A Survey, 10 pages
# be careful if questionnary form change (number of page, type of answer...)
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
question: "1"
})
registration.refresh_from_db()
survey = WEISurvey2024(registration)
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
302 if survey.is_complete() else 200)
self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
survey = WEISurvey2024(registration)
self.assertTrue(survey.is_complete())
survey.select_bus(self.buses[0])
survey.save()
self.assertIsNotNone(survey.information.get_selected_bus())

View File

@ -0,0 +1,111 @@
# Copyright (C) 2018-2025 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.wei2025 import WEIBusInformation2025, WEISurvey2025, WORDS, WEISurveyInformation2025
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 2025",
email="wei2025@example.com",
date_start='2025-09-12',
date_end='2025-09-14',
year=2025,
membership_start='2025-06-01'
)
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 = WEIBusInformation2025(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 = WEISurveyInformation2025(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
WEISurvey2025.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2025(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 = WEISurveyInformation2025(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
WEISurvey2025.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 = WEISurvey2025(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

@ -126,6 +126,7 @@ class TestWEIRegistration(TestCase):
year=self.year + 1, year=self.year + 1,
date_start=str(self.year + 1) + "-09-01", date_start=str(self.year + 1) + "-09-01",
date_end=str(self.year + 1) + "-09-03", date_end=str(self.year + 1) + "-09-03",
caution_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1) qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -160,6 +161,7 @@ class TestWEIRegistration(TestCase):
membership_end="2000-09-30", membership_end="2000-09-30",
date_start="2000-09-01", date_start="2000-09-01",
date_end="2000-09-03", date_end="2000-09-03",
caution_amount=12000,
)) ))
qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id) 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.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
@ -318,6 +320,7 @@ class TestWEIRegistration(TestCase):
bus=[], bus=[],
team=[], team=[],
roles=[], roles=[],
caution_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -334,7 +337,8 @@ class TestWEIRegistration(TestCase):
emergency_contact_phone='+33123456789', emergency_contact_phone='+33123456789',
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")).all()],
caution_type='check'
)) ))
qs = WEIRegistration.objects.filter(user_id=user.id) qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
@ -354,6 +358,7 @@ class TestWEIRegistration(TestCase):
bus=[self.bus.id], bus=[self.bus.id],
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()], roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
caution_type='check'
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors)) self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors))
@ -506,6 +511,7 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M") qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
@ -560,6 +566,7 @@ class TestWEIRegistration(TestCase):
team=[self.team.id], team=[self.team.id],
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()], roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L") qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
@ -583,6 +590,7 @@ class TestWEIRegistration(TestCase):
team=[], team=[],
roles=[], roles=[],
information_json=self.registration.information_json, information_json=self.registration.information_json,
caution_type='check'
) )
) )
self.assertFalse(response.context["membership_form"].is_valid()) self.assertFalse(response.context["membership_form"].is_valid())
@ -624,7 +632,7 @@ class TestWEIRegistration(TestCase):
second_bus = Bus.objects.create(wei=self.wei, name="Second 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) 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( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="GC WEI").id], roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=second_team.pk, team=second_team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -639,7 +647,7 @@ class TestWEIRegistration(TestCase):
self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors)) self.assertTrue("This team doesn&#x27;t belong to the given bus." in str(response.context["form"].errors))
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict( response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
roles=[WEIRole.objects.get(name="GC WEI").id], roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
bus=self.bus.pk, bus=self.bus.pk,
team=self.team.pk, team=self.team.pk,
credit_type=4, # Bank transfer credit_type=4, # Bank transfer
@ -770,7 +778,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None) WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey) self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
self.assertEqual(CurrentSurvey.get_year(), 2024) self.assertEqual(CurrentSurvey.get_year(), 2025)
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):

View File

@ -4,7 +4,7 @@
from django.urls import path from django.urls import path
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \ from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \ WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \ BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \ WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
@ -42,4 +42,5 @@ urlpatterns = [
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"), 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/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"), path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
] ]

View File

@ -35,7 +35,7 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms.registration import WEIChooseBusForm from .forms.registration import WEIChooseBusForm
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \ from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
WEIMembershipForm, CurrentSurvey WEIMembershipForm, CurrentSurvey
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \ from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
WEIRegistration1ATable, WEIMembershipTable WEIRegistration1ATable, WEIMembershipTable
@ -510,7 +510,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
Register a new user to the WEI Register a new user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistrationForm form_class = WEIRegistration1AForm
extra_context = {"title": _("Register first year student to the WEI")} extra_context = {"title": _("Register first year student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -564,6 +564,8 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
del form.fields["caution_check"] del form.fields["caution_check"]
if "information_json" in form.fields: if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
if "caution_type" in form.fields:
del form.fields["caution_type"]
return form return form
@ -602,7 +604,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
Register an old user to the WEI Register an old user to the WEI
""" """
model = WEIRegistration model = WEIRegistration
form_class = WEIRegistrationForm form_class = WEIRegistration2AForm
extra_context = {"title": _("Register old student to the WEI")} extra_context = {"title": _("Register old student to the WEI")}
def get_sample_object(self): def get_sample_object(self):
@ -668,6 +670,12 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
if "information_json" in form.fields: if "information_json" in form.fields:
del form.fields["information_json"] del form.fields["information_json"]
# S'assurer que le champ caution_type est obligatoire
if "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form return form
@transaction.atomic @transaction.atomic
@ -693,6 +701,9 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save() form.instance.save()
if 'treasury' in settings.INSTALLED_APPS: if 'treasury' in settings.INSTALLED_APPS:
@ -767,10 +778,18 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
# Masquer le champ caution_check pour tout le monde dans le formulaire de modification # Masquer le champ caution_check pour tout le monde dans le formulaire de modification
if "caution_check" in form.fields: if "caution_check" in form.fields:
del form.fields["caution_check"] del form.fields["caution_check"]
# S'assurer que le champ caution_type est obligatoire pour les 2A+
if not self.object.first_year and "caution_type" in form.fields:
form.fields["caution_type"].required = True
form.fields["caution_type"].help_text = _("Choose how you want to pay the deposit")
form.fields["caution_type"].widget = forms.RadioSelect(choices=form.fields["caution_type"].choices)
return form return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
membership_form = WEIMembershipForm(data if data else None, instance=instance) registration = self.get_object()
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
del membership_form.fields["credit_type"] del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"] del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"] del membership_form.fields["first_name"]
@ -824,6 +843,10 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]]
information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
form.instance.information = information form.instance.information = information
# Sauvegarder le type de caution pour les 2A+
if "caution_type" in form.cleaned_data:
form.instance.caution_type = form.cleaned_data["caution_type"]
form.instance.save() form.instance.save()
return super().form_valid(form) return super().form_valid(form)
@ -924,7 +947,14 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
date_start__gte=bde.membership_start, date_start__gte=bde.membership_start,
).exists() ).exists()
context["fee"] = registration.fee fee = registration.fee
context["fee"] = fee
# Calculer le montant total nécessaire (frais + caution si transaction)
total_needed = fee
if registration.caution_type == 'note':
total_needed += registration.wei.caution_amount
context["total_needed"] = total_needed
form = context["form"] form = context["form"]
if registration.soge_credit: if registration.soge_credit:
@ -936,10 +966,17 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form_class(self): def get_form_class(self):
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
if registration.first_year and 'sleected_bus_pk' not in registration.information: if registration.first_year and 'selected_bus_pk' not in registration.information:
return WEIMembership1AForm return WEIMembership1AForm
return WEIMembershipForm return WEIMembershipForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
wei = registration.wei
kwargs['wei'] = wei
return kwargs
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
@ -948,12 +985,22 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
# Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire # Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire
if not registration.first_year: if not registration.first_year:
form.fields["caution_check"] = forms.BooleanField( if registration.caution_type == 'check':
required=True, form.fields["caution_check"] = forms.BooleanField(
initial=registration.caution_check, required=True,
label=_("Caution check given"), initial=registration.caution_check,
help_text=_("Please make sure the check is given before validating the registration") label=_("Caution check given"),
) help_text=_("Please make sure the check is given before validating the registration")
)
else:
form.fields["caution_check"] = forms.BooleanField(
required=True,
initial=False,
label=_("Create deposit transaction"),
help_text=_("A transaction of %(amount).2f€ will be created from the user's Note account") % {
'amount': registration.wei.caution_amount / 100
}
)
if registration.soge_credit: if registration.soge_credit:
form.fields["credit_type"].disabled = True form.fields["credit_type"].disabled = True
@ -1037,10 +1084,20 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
if credit_type is None or registration.soge_credit: if credit_type is None or registration.soge_credit:
credit_amount = 0 credit_amount = 0
if not registration.soge_credit and user.note.balance + credit_amount < fee: # Calculer le montant total nécessaire (frais + caution si transaction)
# Users must have money before registering to the WEI. total_needed = fee
if registration.caution_type == 'note':
total_needed += club.caution_amount
# Vérifier que l'utilisateur a assez d'argent pour tout payer
if not registration.soge_credit and user.note.balance + credit_amount < total_needed:
form.add_error('credit_type', form.add_error('credit_type',
_("This user don't have enough money to join this club, and can't have a negative balance.")) _("This user doesn't have enough money to join this club and pay the deposit. "
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d") % {
'balance': user.note.balance,
'credit': credit_amount,
'needed': total_needed}
)
return super().form_invalid(form) return super().form_invalid(form)
if credit_amount: if credit_amount:
@ -1080,6 +1137,18 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db() membership.refresh_from_db()
membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI")) membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI"))
# Créer la transaction de caution si nécessaire
if registration.caution_type == 'note':
from note.models import Transaction
Transaction.objects.create(
source=user.note,
destination=club.note,
quantity=1,
amount=club.caution_amount,
reason=_("Caution %(name)s") % {'name': club.name},
valid=True,
)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -1299,6 +1368,7 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
def get_queryset(self, filter_permissions=True, **kwargs): def get_queryset(self, filter_permissions=True, **kwargs):
qs = super().get_queryset(filter_permissions, **kwargs) qs = super().get_queryset(filter_permissions, **kwargs)
qs = qs.filter(first_year=True, membership__isnull=False) qs = qs.filter(first_year=True, membership__isnull=False)
qs = qs.filter(wei=self.club)
qs = qs.order_by('-membership__bus') qs = qs.order_by('-membership__bus')
return qs return qs
@ -1360,3 +1430,29 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
# On redirige vers la page d'attribution pour le premier étudiant trouvé # On redirige vers la page d'attribution pour le premier étudiant trouvé
return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,)) return reverse_lazy('wei:wei_bus_1A', args=(qs.first().pk,))
class BusInformationUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Bus
def get_form_class(self):
return CurrentSurvey.get_algorithm_class().get_bus_information_form()
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()
# We can't update a bus once the WEI is started
if today >= wei.date_start:
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = self.object.wei
context["information"] = CurrentSurvey.get_algorithm_class().get_bus_information(self.object)
self.object.save()
return context
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk})

View File

@ -136,7 +136,7 @@ de diffusion utiles.
Faîtes attention, donc où la sortie est stockée. Faîtes attention, donc où la sortie est stockée.
Il prend 2 options : Il prend 4 options :
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``, * ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es ``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es
@ -149,7 +149,10 @@ Il prend 2 options :
pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe pour la ML Adhérents, pour exporter les mails des adhérents au BDE pendant n'importe
laquelle des ``n+1`` dernières années. laquelle des ``n+1`` dernières années.
Le script sort sur la sortie standard la liste des adresses mails à inscrire. * ``--email``, qui prend en argument une chaine de caractère contenant une adresse email.
Si aucun email n'est renseigné, le script sort sur la sortie standard la liste des adresses mails à inscrire.
Dans le cas contraire, la liste est envoyée à l'adresse passée en argument.
Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est Attention : il y a parfois certains cas particuliers à prendre en compte, il n'est
malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives. malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives.

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-27 16:46+0200\n" "POT-Creation-Date: 2025-06-02 00:58+0200\n"
"PO-Revision-Date: 2020-11-16 20:02+0000\n" "PO-Revision-Date: 2020-11-16 20:02+0000\n"
"Last-Translator: bleizi <bleizi@crans.org>\n" "Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20/de/>\n" "Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20/de/>\n"
@ -66,7 +66,7 @@ msgstr "Sie dürfen höchstens 3 Leute zu dieser Veranstaltung einladen."
#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:299 #: apps/note/models/transactions.py:46 apps/note/models/transactions.py:299
#: apps/permission/models.py:329 #: apps/permission/models.py:329
#: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/registration/templates/registration/future_profile_detail.html:16
#: apps/wei/models.py:67 apps/wei/models.py:131 apps/wei/tables.py:282 #: apps/wei/models.py:72 apps/wei/models.py:145 apps/wei/tables.py:282
#: apps/wei/templates/wei/base.html:26 #: apps/wei/templates/wei/base.html:26
#: apps/wei/templates/wei/weimembership_form.html:14 apps/wrapped/models.py:16 #: apps/wei/templates/wei/weimembership_form.html:14 apps/wrapped/models.py:16
msgid "name" msgid "name"
@ -101,7 +101,7 @@ msgstr "Vearnstaltungarte"
#: apps/activity/models.py:68 #: apps/activity/models.py:68
#: apps/activity/templates/activity/includes/activity_info.html:19 #: apps/activity/templates/activity/includes/activity_info.html:19
#: apps/note/models/transactions.py:82 apps/permission/models.py:109 #: apps/note/models/transactions.py:82 apps/permission/models.py:109
#: apps/permission/models.py:188 apps/wei/models.py:78 apps/wei/models.py:142 #: apps/permission/models.py:188 apps/wei/models.py:92 apps/wei/models.py:156
msgid "description" msgid "description"
msgstr "Beschreibung" msgstr "Beschreibung"
@ -122,7 +122,7 @@ msgstr "Type"
#: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325 #: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325
#: apps/note/models/notes.py:148 apps/treasury/models.py:294 #: apps/note/models/notes.py:148 apps/treasury/models.py:294
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13 #: apps/wei/models.py:185 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15 #: apps/wei/templates/wei/survey.html:15
msgid "user" msgid "user"
msgstr "User" msgstr "User"
@ -295,14 +295,14 @@ msgstr "Type"
#: apps/activity/tables.py:86 apps/member/forms.py:199 #: apps/activity/tables.py:86 apps/member/forms.py:199
#: apps/registration/forms.py:91 apps/treasury/forms.py:131 #: apps/registration/forms.py:91 apps/treasury/forms.py:131
#: apps/wei/forms/registration.py:107 #: apps/wei/forms/registration.py:116
msgid "Last name" msgid "Last name"
msgstr "Nachname" msgstr "Nachname"
#: apps/activity/tables.py:88 apps/member/forms.py:204 #: apps/activity/tables.py:88 apps/member/forms.py:204
#: apps/note/templates/note/transaction_form.html:138 #: apps/note/templates/note/transaction_form.html:138
#: apps/registration/forms.py:96 apps/treasury/forms.py:133 #: apps/registration/forms.py:96 apps/treasury/forms.py:133
#: apps/wei/forms/registration.py:112 #: apps/wei/forms/registration.py:121
msgid "First name" msgid "First name"
msgstr "Vorname" msgstr "Vorname"
@ -1030,12 +1030,12 @@ msgid "Check this case if the Société Générale paid the inscription."
msgstr "Die Société Générale die Mitgliedschaft bezahlt." msgstr "Die Société Générale die Mitgliedschaft bezahlt."
#: apps/member/forms.py:185 apps/registration/forms.py:78 #: apps/member/forms.py:185 apps/registration/forms.py:78
#: apps/wei/forms/registration.py:94 #: apps/wei/forms/registration.py:103
msgid "Credit type" msgid "Credit type"
msgstr "Kredittype" msgstr "Kredittype"
#: apps/member/forms.py:186 apps/registration/forms.py:79 #: apps/member/forms.py:186 apps/registration/forms.py:79
#: apps/wei/forms/registration.py:95 #: apps/wei/forms/registration.py:104
msgid "No credit" msgid "No credit"
msgstr "Kein Kredit" msgstr "Kein Kredit"
@ -1044,13 +1044,13 @@ msgid "You can credit the note of the user."
msgstr "Sie dûrfen diese Note kreditieren." msgstr "Sie dûrfen diese Note kreditieren."
#: apps/member/forms.py:192 apps/registration/forms.py:84 #: apps/member/forms.py:192 apps/registration/forms.py:84
#: apps/wei/forms/registration.py:100 #: apps/wei/forms/registration.py:109
msgid "Credit amount" msgid "Credit amount"
msgstr "Kreditanzahl" msgstr "Kreditanzahl"
#: apps/member/forms.py:209 apps/note/templates/note/transaction_form.html:144 #: apps/member/forms.py:209 apps/note/templates/note/transaction_form.html:144
#: apps/registration/forms.py:101 apps/treasury/forms.py:135 #: apps/registration/forms.py:101 apps/treasury/forms.py:135
#: apps/wei/forms/registration.py:117 #: apps/wei/forms/registration.py:126
msgid "Bank" msgid "Bank"
msgstr "Bank" msgstr "Bank"
@ -1257,7 +1257,7 @@ msgstr "Ihre Note Kfet Konto bestätigen"
#: apps/member/templates/member/includes/club_info.html:55 #: apps/member/templates/member/includes/club_info.html:55
#: apps/member/templates/member/includes/profile_info.html:40 #: apps/member/templates/member/includes/profile_info.html:40
#: apps/registration/templates/registration/future_profile_detail.html:22 #: apps/registration/templates/registration/future_profile_detail.html:22
#: apps/wei/templates/wei/base.html:70 #: apps/wei/templates/wei/base.html:68
#: apps/wei/templates/wei/weimembership_form.html:20 #: apps/wei/templates/wei/weimembership_form.html:20
msgid "email" msgid "email"
msgstr "Email" msgstr "Email"
@ -1311,7 +1311,7 @@ msgid "add to registration form"
msgstr "Registrierung validieren" msgstr "Registrierung validieren"
#: apps/member/models.py:268 apps/member/models.py:331 #: apps/member/models.py:268 apps/member/models.py:331
#: apps/note/models/notes.py:176 #: apps/note/models/notes.py:176 apps/wei/models.py:86
msgid "club" msgid "club"
msgstr "Club" msgstr "Club"
@ -1514,13 +1514,13 @@ msgstr "Mitgliedsachftpreis"
#: apps/member/templates/member/includes/club_info.html:43 #: apps/member/templates/member/includes/club_info.html:43
#: apps/member/templates/member/includes/profile_info.html:55 #: apps/member/templates/member/includes/profile_info.html:55
#: apps/treasury/templates/treasury/sogecredit_detail.html:24 #: apps/treasury/templates/treasury/sogecredit_detail.html:24
#: apps/wei/templates/wei/base.html:60 #: apps/wei/templates/wei/base.html:58
msgid "balance" msgid "balance"
msgstr "Kontostand" msgstr "Kontostand"
#: apps/member/templates/member/includes/club_info.html:47 #: apps/member/templates/member/includes/club_info.html:47
#: apps/member/templates/member/includes/profile_info.html:20 #: apps/member/templates/member/includes/profile_info.html:20
#: apps/note/models/notes.py:287 apps/wei/templates/wei/base.html:66 #: apps/note/models/notes.py:287 apps/wei/templates/wei/base.html:64
msgid "aliases" msgid "aliases"
msgstr "Aliases" msgstr "Aliases"
@ -1702,7 +1702,7 @@ msgstr "Club bearbeiten"
msgid "Add new member to the club" msgid "Add new member to the club"
msgstr "Neue Mitglieder" msgstr "Neue Mitglieder"
#: apps/member/views.py:750 apps/wei/views.py:1040 #: apps/member/views.py:750
msgid "" msgid ""
"This user don't have enough money to join this club, and can't have a " "This user don't have enough money to join this club, and can't have a "
"negative balance." "negative balance."
@ -2038,8 +2038,8 @@ msgstr ""
"Zahlungsmethode zugeordnet ist, und einem User oder einem Club möglich" "Zahlungsmethode zugeordnet ist, und einem User oder einem Club möglich"
#: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360 #: apps/note/models/transactions.py:357 apps/note/models/transactions.py:360
#: apps/note/models/transactions.py:363 apps/wei/views.py:1045 #: apps/note/models/transactions.py:363 apps/wei/views.py:1097
#: apps/wei/views.py:1049 #: apps/wei/views.py:1101
#: env/lib/python3.11/site-packages/django/forms/fields.py:91 #: env/lib/python3.11/site-packages/django/forms/fields.py:91
msgid "This field is required." msgid "This field is required."
msgstr "Dies ist ein Pflichtfeld." msgstr "Dies ist ein Pflichtfeld."
@ -2076,8 +2076,8 @@ msgstr "Neue Bus"
#: apps/note/tables.py:262 apps/note/templates/note/conso_form.html:151 #: apps/note/tables.py:262 apps/note/templates/note/conso_form.html:151
#: apps/wei/tables.py:49 apps/wei/tables.py:50 #: apps/wei/tables.py:49 apps/wei/tables.py:50
#: apps/wei/templates/wei/base.html:89 #: apps/wei/templates/wei/base.html:87
#: apps/wei/templates/wei/bus_detail.html:20 #: apps/wei/templates/wei/bus_detail.html:24
#: apps/wei/templates/wei/busteam_detail.html:20 #: apps/wei/templates/wei/busteam_detail.html:20
#: apps/wei/templates/wei/busteam_detail.html:42 #: apps/wei/templates/wei/busteam_detail.html:42
#: env/lib/python3.11/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:37 #: env/lib/python3.11/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:37
@ -2552,7 +2552,7 @@ msgstr "Sie haben bereits ein Konto in der Société générale eröffnet."
#: apps/registration/templates/registration/future_profile_detail.html:73 #: apps/registration/templates/registration/future_profile_detail.html:73
#: apps/wei/templates/wei/weimembership_form.html:127 #: apps/wei/templates/wei/weimembership_form.html:127
#: apps/wei/templates/wei/weimembership_form.html:186 #: apps/wei/templates/wei/weimembership_form.html:196
msgid "Validate registration" msgid "Validate registration"
msgstr "Registrierung validieren" msgstr "Registrierung validieren"
@ -3089,22 +3089,22 @@ msgstr "Kreditliste von Société générale"
msgid "Manage credits from the Société générale" msgid "Manage credits from the Société générale"
msgstr "Krediten von der Société générale handeln" msgstr "Krediten von der Société générale handeln"
#: apps/wei/apps.py:10 apps/wei/models.py:37 apps/wei/models.py:38 #: apps/wei/apps.py:10 apps/wei/models.py:42 apps/wei/models.py:43
#: apps/wei/models.py:62 apps/wei/models.py:178 #: apps/wei/models.py:67 apps/wei/models.py:192
#: note_kfet/templates/base.html:108 #: note_kfet/templates/base.html:108
msgid "WEI" msgid "WEI"
msgstr "WEI" msgstr "WEI"
#: apps/wei/forms/registration.py:36 #: apps/wei/forms/registration.py:37
msgid "The selected user is not validated. Please validate its account first" msgid "The selected user is not validated. Please validate its account first"
msgstr "" msgstr ""
#: apps/wei/forms/registration.py:62 apps/wei/models.py:126 #: apps/wei/forms/registration.py:71 apps/wei/models.py:140
#: apps/wei/models.py:324 #: apps/wei/models.py:348
msgid "bus" msgid "bus"
msgstr "Bus" msgstr "Bus"
#: apps/wei/forms/registration.py:63 #: apps/wei/forms/registration.py:72
msgid "" msgid ""
"This choice is not definitive. The WEI organizers are free to attribute for " "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." "you a bus and a team, in particular if you are a free eletron."
@ -3113,11 +3113,11 @@ msgstr ""
"einen Bus und ein Team zuzuweisen, insbesondere wenn Sie ein freies Elektron " "einen Bus und ein Team zuzuweisen, insbesondere wenn Sie ein freies Elektron "
"sind." "sind."
#: apps/wei/forms/registration.py:70 #: apps/wei/forms/registration.py:79
msgid "Team" msgid "Team"
msgstr "Team" msgstr "Team"
#: apps/wei/forms/registration.py:72 #: apps/wei/forms/registration.py:81
msgid "" msgid ""
"Leave this field empty if you won't be in a team (staff, bus chief, free " "Leave this field empty if you won't be in a team (staff, bus chief, free "
"electron)" "electron)"
@ -3125,16 +3125,16 @@ msgstr ""
"Lassen Sie dieses Feld leer, wenn Sie nicht in einem Team sind (Mitarbeiter, " "Lassen Sie dieses Feld leer, wenn Sie nicht in einem Team sind (Mitarbeiter, "
"Buschef, freies Elektron)" "Buschef, freies Elektron)"
#: apps/wei/forms/registration.py:78 apps/wei/forms/registration.py:88 #: apps/wei/forms/registration.py:87 apps/wei/forms/registration.py:97
#: apps/wei/models.py:160 #: apps/wei/models.py:174
msgid "WEI Roles" msgid "WEI Roles"
msgstr "WEI Rollen" msgstr "WEI Rollen"
#: apps/wei/forms/registration.py:79 #: apps/wei/forms/registration.py:88
msgid "Select the roles that you are interested in." msgid "Select the roles that you are interested in."
msgstr "Wählen Sie die Rollen aus, an denen Sie interessiert sind." msgstr "Wählen Sie die Rollen aus, an denen Sie interessiert sind."
#: apps/wei/forms/registration.py:125 #: apps/wei/forms/registration.py:134
msgid "This team doesn't belong to the given bus." msgid "This team doesn't belong to the given bus."
msgstr "Dieses Team gehört nicht zum angegebenen Bus." msgstr "Dieses Team gehört nicht zum angegebenen Bus."
@ -3156,118 +3156,140 @@ msgstr "Anfangsdatum"
msgid "date end" msgid "date end"
msgstr "Abschlussdatum" msgstr "Abschlussdatum"
#: apps/wei/models.py:71 apps/wei/tables.py:305 #: apps/wei/models.py:37
#, fuzzy
#| msgid "total amount"
msgid "caution amount"
msgstr "Totalanzahlt"
#: apps/wei/models.py:76 apps/wei/tables.py:305
#, fuzzy #, fuzzy
#| msgid "The user joined the bus" #| msgid "The user joined the bus"
msgid "seat count in the bus" msgid "seat count in the bus"
msgstr "Der Benutzer ist dem Bus beigetreten" msgstr "Der Benutzer ist dem Bus beigetreten"
#: apps/wei/models.py:83 #: apps/wei/models.py:97
msgid "survey information" msgid "survey information"
msgstr "Umfrage Infos" msgstr "Umfrage Infos"
#: apps/wei/models.py:84 #: apps/wei/models.py:98
msgid "Information about the survey for new members, encoded in JSON" msgid "Information about the survey for new members, encoded in JSON"
msgstr "Informationen zur Umfrage für neue Mitglieder, codiert in JSON" msgstr "Informationen zur Umfrage für neue Mitglieder, codiert in JSON"
#: apps/wei/models.py:88 #: apps/wei/models.py:102
msgid "Bus" msgid "Bus"
msgstr "Bus" msgstr "Bus"
#: apps/wei/models.py:89 apps/wei/templates/wei/weiclub_detail.html:51 #: apps/wei/models.py:103 apps/wei/templates/wei/weiclub_detail.html:51
msgid "Buses" msgid "Buses"
msgstr "Buses" msgstr "Buses"
#: apps/wei/models.py:135 #: apps/wei/models.py:149
msgid "color" msgid "color"
msgstr "Farbe" msgstr "Farbe"
#: apps/wei/models.py:136 #: apps/wei/models.py:150
msgid "The color of the T-Shirt, stored with its number equivalent" msgid "The color of the T-Shirt, stored with its number equivalent"
msgstr "Die Farbe des T-Shirts, gespeichert mit der entsprechenden Nummer" msgstr "Die Farbe des T-Shirts, gespeichert mit der entsprechenden Nummer"
#: apps/wei/models.py:147 #: apps/wei/models.py:161
msgid "Bus team" msgid "Bus team"
msgstr "Bus Team" msgstr "Bus Team"
#: apps/wei/models.py:148 #: apps/wei/models.py:162
msgid "Bus teams" msgid "Bus teams"
msgstr "Bus Teams" msgstr "Bus Teams"
#: apps/wei/models.py:159 #: apps/wei/models.py:173
msgid "WEI Role" msgid "WEI Role"
msgstr "WEI Rolle" msgstr "WEI Rolle"
#: apps/wei/models.py:183 #: apps/wei/models.py:197
msgid "Credit from Société générale" msgid "Credit from Société générale"
msgstr "Kredit von der Société générale" msgstr "Kredit von der Société générale"
#: apps/wei/models.py:188 apps/wei/views.py:951 #: apps/wei/models.py:202 apps/wei/views.py:984
msgid "Caution check given" msgid "Caution check given"
msgstr "Caution check given" msgstr "Caution check given"
#: apps/wei/models.py:192 apps/wei/templates/wei/weimembership_form.html:64 #: apps/wei/models.py:208
msgid "Check"
msgstr ""
#: apps/wei/models.py:209
#, fuzzy
#| msgid "transactions"
msgid "Note transaction"
msgstr "Transaktionen"
#: apps/wei/models.py:212
#, fuzzy
#| msgid "created at"
msgid "caution type"
msgstr "erschafft am"
#: apps/wei/models.py:216 apps/wei/templates/wei/weimembership_form.html:64
msgid "birth date" msgid "birth date"
msgstr "Geburtsdatum" msgstr "Geburtsdatum"
#: apps/wei/models.py:198 apps/wei/models.py:208 #: apps/wei/models.py:222 apps/wei/models.py:232
msgid "Male" msgid "Male"
msgstr "Männlich" msgstr "Männlich"
#: apps/wei/models.py:199 apps/wei/models.py:209 #: apps/wei/models.py:223 apps/wei/models.py:233
msgid "Female" msgid "Female"
msgstr "Weiblich" msgstr "Weiblich"
#: apps/wei/models.py:200 #: apps/wei/models.py:224
msgid "Non binary" msgid "Non binary"
msgstr "Nicht binär" msgstr "Nicht binär"
#: apps/wei/models.py:202 apps/wei/templates/wei/attribute_bus_1A.html:22 #: apps/wei/models.py:226 apps/wei/templates/wei/attribute_bus_1A.html:22
#: apps/wei/templates/wei/weimembership_form.html:55 #: apps/wei/templates/wei/weimembership_form.html:55
msgid "gender" msgid "gender"
msgstr "Geschlecht" msgstr "Geschlecht"
#: apps/wei/models.py:210 #: apps/wei/models.py:234
msgid "Unisex" msgid "Unisex"
msgstr "Unisex" msgstr "Unisex"
#: apps/wei/models.py:213 apps/wei/templates/wei/weimembership_form.html:58 #: apps/wei/models.py:237 apps/wei/templates/wei/weimembership_form.html:58
msgid "clothing cut" msgid "clothing cut"
msgstr "Kleidung Schnitt" msgstr "Kleidung Schnitt"
#: apps/wei/models.py:226 apps/wei/templates/wei/weimembership_form.html:61 #: apps/wei/models.py:250 apps/wei/templates/wei/weimembership_form.html:61
msgid "clothing size" msgid "clothing size"
msgstr "Kleidergröße" msgstr "Kleidergröße"
#: apps/wei/models.py:232 #: apps/wei/models.py:256
msgid "health issues" msgid "health issues"
msgstr "Gesundheitsprobleme" msgstr "Gesundheitsprobleme"
#: apps/wei/models.py:237 apps/wei/templates/wei/weimembership_form.html:70 #: apps/wei/models.py:261 apps/wei/templates/wei/weimembership_form.html:70
msgid "emergency contact name" msgid "emergency contact name"
msgstr "Notfall-Kontakt" msgstr "Notfall-Kontakt"
#: apps/wei/models.py:238 #: apps/wei/models.py:262
msgid "The emergency contact must not be a WEI participant" msgid "The emergency contact must not be a WEI participant"
msgstr "Der Notfallkontakt darf kein WEI-Teilnehmer sein" msgstr "Der Notfallkontakt darf kein WEI-Teilnehmer sein"
#: apps/wei/models.py:243 apps/wei/templates/wei/weimembership_form.html:73 #: apps/wei/models.py:267 apps/wei/templates/wei/weimembership_form.html:73
msgid "emergency contact phone" msgid "emergency contact phone"
msgstr "Notfallkontakttelefon" msgstr "Notfallkontakttelefon"
#: apps/wei/models.py:248 apps/wei/templates/wei/weimembership_form.html:52 #: apps/wei/models.py:272 apps/wei/templates/wei/weimembership_form.html:52
msgid "first year" msgid "first year"
msgstr "Erste Jahr" msgstr "Erste Jahr"
#: apps/wei/models.py:249 #: apps/wei/models.py:273
msgid "Tells if the user is new in the school." msgid "Tells if the user is new in the school."
msgstr "Gibt an, ob der USer neu in der Schule ist." msgstr "Gibt an, ob der USer neu in der Schule ist."
#: apps/wei/models.py:254 #: apps/wei/models.py:278
msgid "registration information" msgid "registration information"
msgstr "Registrierung Detailen" msgstr "Registrierung Detailen"
#: apps/wei/models.py:255 #: apps/wei/models.py:279
msgid "" msgid ""
"Information about the registration (buses for old members, survey for the " "Information about the registration (buses for old members, survey for the "
"new members), encoded in JSON" "new members), encoded in JSON"
@ -3275,27 +3297,27 @@ msgstr ""
"Informationen zur Registrierung (Busse für alte Mitglieder, Umfrage für neue " "Informationen zur Registrierung (Busse für alte Mitglieder, Umfrage für neue "
"Mitglieder), verschlüsselt in JSON" "Mitglieder), verschlüsselt in JSON"
#: apps/wei/models.py:261 #: apps/wei/models.py:285
msgid "WEI User" msgid "WEI User"
msgstr "WEI User" msgstr "WEI User"
#: apps/wei/models.py:262 #: apps/wei/models.py:286
msgid "WEI Users" msgid "WEI Users"
msgstr "WEI Users" msgstr "WEI Users"
#: apps/wei/models.py:334 #: apps/wei/models.py:358
msgid "team" msgid "team"
msgstr "Team" msgstr "Team"
#: apps/wei/models.py:344 #: apps/wei/models.py:368
msgid "WEI registration" msgid "WEI registration"
msgstr "WEI Registrierung" msgstr "WEI Registrierung"
#: apps/wei/models.py:348 #: apps/wei/models.py:372
msgid "WEI membership" msgid "WEI membership"
msgstr "WEI Mitgliedschaft" msgstr "WEI Mitgliedschaft"
#: apps/wei/models.py:349 #: apps/wei/models.py:373
msgid "WEI memberships" msgid "WEI memberships"
msgstr "WEI Mitgliedschaften" msgstr "WEI Mitgliedschaften"
@ -3327,7 +3349,7 @@ msgstr "Jahr"
msgid "preferred bus" msgid "preferred bus"
msgstr "bevorzugter Bus" msgstr "bevorzugter Bus"
#: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:32 #: apps/wei/tables.py:210 apps/wei/templates/wei/bus_detail.html:36
#: apps/wei/templates/wei/busteam_detail.html:52 #: apps/wei/templates/wei/busteam_detail.html:52
msgid "Teams" msgid "Teams"
msgstr "Teams" msgstr "Teams"
@ -3401,44 +3423,52 @@ msgstr "Tastenliste"
msgid "WEI fee (paid students)" msgid "WEI fee (paid students)"
msgstr "WEI Preis (bezahlte Studenten)" msgstr "WEI Preis (bezahlte Studenten)"
#: apps/wei/templates/wei/base.html:47 apps/wei/templates/wei/base.html:54 #: apps/wei/templates/wei/base.html:47
msgid "The BDE membership is included in the WEI registration."
msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten."
#: apps/wei/templates/wei/base.html:51
msgid "WEI fee (unpaid students)" msgid "WEI fee (unpaid students)"
msgstr "WEI Preis (unbezahlte Studenten)" msgstr "WEI Preis (unbezahlte Studenten)"
#: apps/wei/templates/wei/base.html:76 #: apps/wei/templates/wei/base.html:53
#, fuzzy
#| msgid "total amount"
msgid "Caution amount"
msgstr "Totalanzahlt"
#: apps/wei/templates/wei/base.html:74
msgid "WEI list" msgid "WEI list"
msgstr "WEI Liste" msgstr "WEI Liste"
#: apps/wei/templates/wei/base.html:81 apps/wei/views.py:557 #: apps/wei/templates/wei/base.html:79 apps/wei/views.py:550
msgid "Register 1A" msgid "Register 1A"
msgstr "1A Registrieren" msgstr "1A Registrieren"
#: apps/wei/templates/wei/base.html:85 apps/wei/views.py:649 #: apps/wei/templates/wei/base.html:83 apps/wei/views.py:644
msgid "Register 2A+" msgid "Register 2A+"
msgstr "2A+ Registrieren" msgstr "2A+ Registrieren"
#: apps/wei/templates/wei/base.html:93 #: apps/wei/templates/wei/base.html:91
msgid "Add bus" msgid "Add bus"
msgstr "Neue Bus" msgstr "Neue Bus"
#: apps/wei/templates/wei/base.html:97 #: apps/wei/templates/wei/base.html:95
msgid "View WEI" msgid "View WEI"
msgstr "WEI schauen" msgstr "WEI schauen"
#: apps/wei/templates/wei/bus_detail.html:22 #: apps/wei/templates/wei/bus_detail.html:21
#, fuzzy
#| msgid "club"
msgid "View club"
msgstr "Club"
#: apps/wei/templates/wei/bus_detail.html:26
#: apps/wei/templates/wei/busteam_detail.html:24 #: apps/wei/templates/wei/busteam_detail.html:24
msgid "Add team" msgid "Add team"
msgstr "Neue Team" msgstr "Neue Team"
#: apps/wei/templates/wei/bus_detail.html:45 #: apps/wei/templates/wei/bus_detail.html:49
msgid "Members" msgid "Members"
msgstr "Mitglied" msgstr "Mitglied"
#: apps/wei/templates/wei/bus_detail.html:54 #: apps/wei/templates/wei/bus_detail.html:58
#: apps/wei/templates/wei/busteam_detail.html:62 #: apps/wei/templates/wei/busteam_detail.html:62
#: apps/wei/templates/wei/weimembership_list.html:31 #: apps/wei/templates/wei/weimembership_list.html:31
msgid "View as PDF" msgid "View as PDF"
@ -3446,8 +3476,8 @@ msgstr "Als PDF schauen"
#: apps/wei/templates/wei/survey.html:11 #: apps/wei/templates/wei/survey.html:11
#: apps/wei/templates/wei/survey_closed.html:11 #: apps/wei/templates/wei/survey_closed.html:11
#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1095 #: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1159
#: apps/wei/views.py:1150 apps/wei/views.py:1197 #: apps/wei/views.py:1214 apps/wei/views.py:1261
msgid "Survey WEI" msgid "Survey WEI"
msgstr "WEI Umfrage" msgstr "WEI Umfrage"
@ -3491,7 +3521,7 @@ msgstr "Unvalidierte Registrierungen"
msgid "Attribute buses" msgid "Attribute buses"
msgstr "" msgstr ""
#: apps/wei/templates/wei/weiclub_list.html:14 apps/wei/views.py:83 #: apps/wei/templates/wei/weiclub_list.html:14 apps/wei/views.py:82
msgid "Create WEI" msgid "Create WEI"
msgstr "Neue WEI" msgstr "Neue WEI"
@ -3575,29 +3605,42 @@ msgstr ""
"validieren, sobald die Bank die Erstellung des Kontos validiert hat, oder " "validieren, sobald die Bank die Erstellung des Kontos validiert hat, oder "
"die Zahlungsmethode ändern." "die Zahlungsmethode ändern."
#: apps/wei/templates/wei/weimembership_form.html:147
msgid "Required payments:"
msgstr ""
#: apps/wei/templates/wei/weimembership_form.html:149 #: apps/wei/templates/wei/weimembership_form.html:149
#, python-format #, fuzzy, python-format
msgid "" #| msgid "membership fee (paid students)"
"The note don't have enough money (%(balance)s, %(pretty_fee)s required). The " msgid "Membership fees: %(amount)s"
"registration may fail if you don't credit the note now." msgstr "Mitgliedschaftpreis (bezahlte Studenten)"
msgstr ""
"Die Note hat nicht genug Geld (%(balance)s,%(pretty_fee)s erforderlich). Die "
"Registrierung kann fehlschlagen, wenn Sie die Note jetzt nicht gutschreiben."
#: apps/wei/templates/wei/weimembership_form.html:157 #: apps/wei/templates/wei/weimembership_form.html:153
#, python-format #, python-format
msgid "" msgid "Deposit (by Note transaction): %(amount)s"
"The note has enough money (%(pretty_fee)s required), the registration is "
"possible."
msgstr "" msgstr ""
"Die Note hat genug Geld (%(pretty_fee)s erforderlich), die Registrierung ist "
"möglich."
#: apps/wei/templates/wei/weimembership_form.html:166 #: apps/wei/templates/wei/weimembership_form.html:156
#: apps/wei/templates/wei/weimembership_form.html:163
#, python-format
msgid "Total needed: %(total)s"
msgstr ""
#: apps/wei/templates/wei/weimembership_form.html:160
#, python-format
msgid "Deposit (by check): %(amount)s"
msgstr ""
#: apps/wei/templates/wei/weimembership_form.html:168
#, python-format
msgid "Current balance: %(balance)s"
msgstr ""
#: apps/wei/templates/wei/weimembership_form.html:176
msgid "The user didn't give her/his caution check." msgid "The user didn't give her/his caution check."
msgstr "Der User hat nicht sein Vorsichtsprüfung gegeben." msgstr "Der User hat nicht sein Vorsichtsprüfung gegeben."
#: apps/wei/templates/wei/weimembership_form.html:174 #: apps/wei/templates/wei/weimembership_form.html:184
msgid "" msgid ""
"This user is not a member of the Kfet club for the coming year. The " "This user is not a member of the Kfet club for the coming year. The "
"membership will be processed automatically, the WEI registration includes " "membership will be processed automatically, the WEI registration includes "
@ -3633,67 +3676,67 @@ msgstr "Bei diesem Muster wurde keine Vorregistrierung gefunden."
msgid "View validated memberships..." msgid "View validated memberships..."
msgstr "Validierte Mitgliedschaften anzeigen ..." msgstr "Validierte Mitgliedschaften anzeigen ..."
#: apps/wei/views.py:62 #: apps/wei/views.py:61
msgid "Search WEI" msgid "Search WEI"
msgstr "WEI finden" msgstr "WEI finden"
#: apps/wei/views.py:113 #: apps/wei/views.py:112
msgid "WEI Detail" msgid "WEI Detail"
msgstr "WEI Infos" msgstr "WEI Infos"
#: apps/wei/views.py:213 #: apps/wei/views.py:212
msgid "View members of the WEI" msgid "View members of the WEI"
msgstr "Mitglied der WEI schauen" msgstr "Mitglied der WEI schauen"
#: apps/wei/views.py:246 #: apps/wei/views.py:245
msgid "Find WEI Membership" msgid "Find WEI Membership"
msgstr "WEI Mitgliedschaft finden" msgstr "WEI Mitgliedschaft finden"
#: apps/wei/views.py:256 #: apps/wei/views.py:255
msgid "View registrations to the WEI" msgid "View registrations to the WEI"
msgstr "Mitglied der WEI schauen" msgstr "Mitglied der WEI schauen"
#: apps/wei/views.py:285 #: apps/wei/views.py:284
msgid "Find WEI Registration" msgid "Find WEI Registration"
msgstr "WEI Registrierung finden" msgstr "WEI Registrierung finden"
#: apps/wei/views.py:296 #: apps/wei/views.py:295
msgid "Update the WEI" msgid "Update the WEI"
msgstr "WEI bearbeiten" msgstr "WEI bearbeiten"
#: apps/wei/views.py:317 #: apps/wei/views.py:316
msgid "Create new bus" msgid "Create new bus"
msgstr "Neue Bus" msgstr "Neue Bus"
#: apps/wei/views.py:355 #: apps/wei/views.py:354
msgid "Update bus" msgid "Update bus"
msgstr "Bus bearbeiten" msgstr "Bus bearbeiten"
#: apps/wei/views.py:387 #: apps/wei/views.py:386
msgid "Manage bus" msgid "Manage bus"
msgstr "Bus ändern" msgstr "Bus ändern"
#: apps/wei/views.py:414 #: apps/wei/views.py:413
msgid "Create new team" msgid "Create new team"
msgstr "Neue Bus Team" msgstr "Neue Bus Team"
#: apps/wei/views.py:461 #: apps/wei/views.py:457
msgid "Update team" msgid "Update team"
msgstr "Team bearbeiten" msgstr "Team bearbeiten"
#: apps/wei/views.py:499 #: apps/wei/views.py:492
msgid "Manage WEI team" msgid "Manage WEI team"
msgstr "WEI Team bearbeiten" msgstr "WEI Team bearbeiten"
#: apps/wei/views.py:521 #: apps/wei/views.py:514
msgid "Register first year student to the WEI" msgid "Register first year student to the WEI"
msgstr "Registrieren Sie den Erstsemester beim WEI" msgstr "Registrieren Sie den Erstsemester beim WEI"
#: apps/wei/views.py:585 apps/wei/views.py:688 #: apps/wei/views.py:580 apps/wei/views.py:689
msgid "This user is already registered to this WEI." msgid "This user is already registered to this WEI."
msgstr "Dieser Benutzer ist bereits bei dieser WEI registriert." msgstr "Dieser Benutzer ist bereits bei dieser WEI registriert."
#: apps/wei/views.py:590 #: apps/wei/views.py:585
msgid "" msgid ""
"This user can't be in her/his first year since he/she has already " "This user can't be in her/his first year since he/she has already "
"participated to a WEI." "participated to a WEI."
@ -3701,25 +3744,29 @@ msgstr ""
"Dieser Benutzer kann nicht in seinem ersten Jahr sein, da er bereits an " "Dieser Benutzer kann nicht in seinem ersten Jahr sein, da er bereits an "
"einer WEI teilgenommen hat." "einer WEI teilgenommen hat."
#: apps/wei/views.py:613 #: apps/wei/views.py:608
msgid "Register old student to the WEI" msgid "Register old student to the WEI"
msgstr "Registrieren Sie einen alten Studenten beim WEI" msgstr "Registrieren Sie einen alten Studenten beim WEI"
#: apps/wei/views.py:668 apps/wei/views.py:764 #: apps/wei/views.py:663 apps/wei/views.py:768
msgid "You already opened an account in the Société générale." msgid "You already opened an account in the Société générale."
msgstr "Sie haben bereits ein Konto in der Société générale eröffnet." msgstr "Sie haben bereits ein Konto in der Société générale eröffnet."
#: apps/wei/views.py:724 #: apps/wei/views.py:676 apps/wei/views.py:785
msgid "Choose how you want to pay the deposit"
msgstr ""
#: apps/wei/views.py:728
msgid "Update WEI Registration" msgid "Update WEI Registration"
msgstr "WEI Registrierung aktualisieren" msgstr "WEI Registrierung aktualisieren"
#: apps/wei/views.py:799 #: apps/wei/views.py:810
#, fuzzy #, fuzzy
#| msgid "The BDE membership is included in the WEI registration." #| msgid "The BDE membership is included in the WEI registration."
msgid "No membership found for this registration" msgid "No membership found for this registration"
msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten." msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten."
#: apps/wei/views.py:808 #: apps/wei/views.py:819
#, fuzzy #, fuzzy
#| msgid "" #| msgid ""
#| "You don't have the permission to add an instance of model {app_label}." #| "You don't have the permission to add an instance of model {app_label}."
@ -3729,7 +3776,7 @@ msgstr ""
"Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. " "Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. "
"{model_name} hinzufügen." "{model_name} hinzufügen."
#: apps/wei/views.py:814 #: apps/wei/views.py:825
#, fuzzy, python-format #, fuzzy, python-format
#| msgid "" #| msgid ""
#| "You don't have the permission to delete this instance of model " #| "You don't have the permission to delete this instance of model "
@ -3739,25 +3786,19 @@ msgstr ""
"Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. " "Sie haben nicht die Berechtigung, eine Instanz von model {app_label}. "
"{model_name} zulöschen." "{model_name} zulöschen."
#: apps/wei/views.py:855 #: apps/wei/views.py:870
msgid "Delete WEI registration" msgid "Delete WEI registration"
msgstr "WEI Registrierung löschen" msgstr "WEI Registrierung löschen"
#: apps/wei/views.py:866 #: apps/wei/views.py:881
msgid "You don't have the right to delete this WEI registration." msgid "You don't have the right to delete this WEI registration."
msgstr "Sie haben nicht das Recht, diese WEI-Registrierung zu löschen." msgstr "Sie haben nicht das Recht, diese WEI-Registrierung zu löschen."
#: apps/wei/views.py:884 #: apps/wei/views.py:899
msgid "Validate WEI registration" msgid "Validate WEI registration"
msgstr "Überprüfen Sie die WEI-Registrierung" msgstr "Überprüfen Sie die WEI-Registrierung"
#: apps/wei/views.py:889 #: apps/wei/views.py:985
#, fuzzy
#| msgid "You don't have the right to delete this WEI registration."
msgid "You don't have the permission to validate registrations"
msgstr "Sie haben nicht das Recht, diese WEI-Registrierung zu löschen."
#: apps/wei/views.py:952
#, fuzzy #, fuzzy
#| msgid "Please ask the user to credit its note before deleting this credit." #| msgid "Please ask the user to credit its note before deleting this credit."
msgid "Please make sure the check is given before validating the registration" msgid "Please make sure the check is given before validating the registration"
@ -3765,14 +3806,50 @@ msgstr ""
"Bitte bitten Sie den Benutzer, seine Note gutzuschreiben, bevor Sie diese " "Bitte bitten Sie den Benutzer, seine Note gutzuschreiben, bevor Sie diese "
"Kredit löschen." "Kredit löschen."
#: apps/wei/views.py:1290 #: apps/wei/views.py:991
#, fuzzy
#| msgid "credit transaction"
msgid "Create deposit transaction"
msgstr "Kredit Transaktion"
#: apps/wei/views.py:992
#, python-format
msgid ""
"A transaction of %(amount).2f€ will be created from the user's Note account"
msgstr ""
#: apps/wei/views.py:1087
#, fuzzy, python-format
#| msgid ""
#| "This user don't have enough money to join this club, and can't have a "
#| "negative balance."
msgid ""
"This user doesn't have enough money to join this club and pay the deposit. "
"Current balance: %(balance)d€, credit: %(credit)d€, needed: %(needed)d€"
msgstr ""
"Diese User hat nicht genug Geld um Mitglied zu werden, und darf nich im Rot "
"sein."
#: apps/wei/views.py:1140
#, fuzzy, python-format
#| msgid "created at"
msgid "Caution %(name)s"
msgstr "erschafft am"
#: apps/wei/views.py:1354
msgid "Attribute buses to first year members" msgid "Attribute buses to first year members"
msgstr "" msgstr ""
#: apps/wei/views.py:1315 #: apps/wei/views.py:1379
msgid "Attribute bus" msgid "Attribute bus"
msgstr "" msgstr ""
#: apps/wei/views.py:1419
msgid ""
"No first year student without a bus found. Either all of them have a bus, or "
"none has filled the survey yet."
msgstr ""
#: apps/wrapped/apps.py:10 #: apps/wrapped/apps.py:10
msgid "wrapped" msgid "wrapped"
msgstr "" msgstr ""
@ -5769,6 +5846,31 @@ msgstr ""
"müssen Ihre E-Mail-Adresse auch überprüfen, indem Sie dem Link folgen, den " "müssen Ihre E-Mail-Adresse auch überprüfen, indem Sie dem Link folgen, den "
"Sie erhalten haben." "Sie erhalten haben."
#~ msgid "The BDE membership is included in the WEI registration."
#~ msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten."
#, python-format
#~ msgid ""
#~ "The note don't have enough money (%(balance)s, %(pretty_fee)s required). "
#~ "The registration may fail if you don't credit the note now."
#~ msgstr ""
#~ "Die Note hat nicht genug Geld (%(balance)s,%(pretty_fee)s erforderlich). "
#~ "Die Registrierung kann fehlschlagen, wenn Sie die Note jetzt nicht "
#~ "gutschreiben."
#, python-format
#~ msgid ""
#~ "The note has enough money (%(pretty_fee)s required), the registration is "
#~ "possible."
#~ msgstr ""
#~ "Die Note hat genug Geld (%(pretty_fee)s erforderlich), die Registrierung "
#~ "ist möglich."
#, fuzzy
#~| msgid "You don't have the right to delete this WEI registration."
#~ msgid "You don't have the permission to validate registrations"
#~ msgstr "Sie haben nicht das Recht, diese WEI-Registrierung zu löschen."
#, fuzzy #, fuzzy
#~| msgid "active" #~| msgid "active"
#~ msgid "is active" #~ msgid "is active"
@ -5794,11 +5896,6 @@ msgstr ""
#~ msgid "View details" #~ msgid "View details"
#~ msgstr "Profile detail" #~ msgstr "Profile detail"
#, fuzzy
#~| msgid "created at"
#~ msgid "Creation date"
#~ msgstr "erschafft am"
#, fuzzy #, fuzzy
#~| msgid "There is no results." #~| msgid "There is no results."
#~ msgid "There is no meal." #~ msgid "There is no meal."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@ SECURE_HSTS_PRELOAD = True
INSTALLED_APPS = [ INSTALLED_APPS = [
# External apps # External apps
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'cas_server',
'colorfield', 'colorfield',
'crispy_bootstrap4', 'crispy_bootstrap4',
'crispy_forms', 'crispy_forms',
@ -270,7 +271,7 @@ OAUTH2_PROVIDER = {
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
'OIDC_ENABLED': True, 'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': 'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'), os.getenv('OIDC_RSA_PRIVATE_KEY', 'CHANGE_ME_IN_ENV_SETTINGS').replace('\\n', '\n'), # for multilines
'SCOPES': { 'openid': "OpenID Connect scope" }, 'SCOPES': { 'openid': "OpenID Connect scope" },
} }

View File

@ -138,9 +138,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
<i class="fa fa-user"></i> {% trans "My account" %} <i class="fa fa-user"></i> {% trans "My account" %}
</a> </a>
<a class="dropdown-item" href="{% url 'logout' %}"> <form method="post" action="{% url 'logout' %}">
<i class="fa fa-sign-out"></i> {% trans "Log out" %} {% csrf_token %}
</a> <button class="dropdown-item" type=submit">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</button>
</form>
</div> </div>
</li> </li>
{% else %} {% else %}

View File

@ -1,20 +1,20 @@
beautifulsoup4~=4.12.3 beautifulsoup4~=4.13.4
crispy-bootstrap4~=2023.1 crispy-bootstrap4~=2025.6
Django~=4.2.9 Django~=5.2.4
django-bootstrap-datepicker-plus~=5.0.5 django-bootstrap-datepicker-plus~=5.0.5
#django-cas-server~=2.0.0 django-cas-server~=3.1.0
django-colorfield~=0.11.0 django-colorfield~=0.14.0
django-crispy-forms~=2.1.0 django-crispy-forms~=2.4.0
django-extensions>=3.2.3 django-extensions>=4.1.0
django-filter~=23.5 django-filter~=25.1
#django-htcpcp-tea~=0.8.1 #django-htcpcp-tea~=0.8.1
django-mailer~=2.3.1 django-mailer~=2.3.2
django-oauth-toolkit~=2.3.0 django-oauth-toolkit~=3.0.1
django-phonenumber-field~=7.3.0 django-phonenumber-field~=8.1.0
django-polymorphic~=3.1.0 django-polymorphic~=3.1.0
djangorestframework~=3.14.0 djangorestframework~=3.16.0
django-rest-polymorphic~=0.1.10 django-rest-polymorphic~=0.1.10
django-tables2~=2.7.0 django-tables2~=2.7.5
python-memcached~=1.62 python-memcached~=1.62
phonenumbers~=8.13.28 phonenumbers~=9.0.8
Pillow>=10.2.0 Pillow>=11.3.0

View File

@ -1,13 +1,13 @@
[tox] [tox]
envlist = envlist =
# Ubuntu 22.04 Python # Ubuntu 22.04 Python
py310-django42 py310-django52
# Debian Bookworm Python # Debian Bookworm Python
py311-django42 py311-django52
# Ubuntu 24.04 Python # Ubuntu 24.04 Python
py312-django42 py312-django52
linters linters
skipsdist = True skipsdist = True