mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-22 08:53:28 +02:00
Compare commits
33 Commits
16cfaa809a
...
django-5.2
Author | SHA1 | Date | |
---|---|---|---|
85ea43a7cf | |||
f54dd30482 | |||
7eafe33945 | |||
6edef619aa | |||
8a1f30ebe2 | |||
b2c6b0e85d | |||
1567bc6ce5 | |||
c411197af3 | |||
bc517f02e5 | |||
e83ee8015f | |||
c26534b6b7 | |||
cdc6f0a3f8 | |||
c153d5f10a | |||
3f76ca6472 | |||
5c5f579729 | |||
a6df0e7c69 | |||
763535bea4 | |||
df0d886db9 | |||
092cc37320 | |||
16b55e23af | |||
97621e8704 | |||
cf4c23d1ac | |||
d71105976f | |||
89cc03141b | |||
6822500fdc | |||
63f6528adc | |||
40ac1daece | |||
e617048332 | |||
9eb6edb37d | |||
70a57bf02d | |||
02453e07ba | |||
4479e8f97a | |||
a351415494 |
@ -21,3 +21,6 @@ EMAIL_PASSWORD=CHANGE_ME
|
||||
# Wiki configuration
|
||||
WIKI_USER=NoteKfet2020
|
||||
WIKI_PASSWORD=
|
||||
|
||||
# OIDC
|
||||
OIDC_RSA_PRIVATE_KEY=CHANGE_ME
|
||||
|
@ -8,7 +8,7 @@ variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Ubuntu 22.04
|
||||
py310-django42:
|
||||
py310-django52:
|
||||
stage: test
|
||||
image: ubuntu:22.04
|
||||
before_script:
|
||||
@ -22,10 +22,10 @@ py310-django42:
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py310-django42
|
||||
script: tox -e py310-django52
|
||||
|
||||
# Debian Bookworm
|
||||
py311-django42:
|
||||
py311-django52:
|
||||
stage: test
|
||||
image: debian:bookworm
|
||||
before_script:
|
||||
@ -37,7 +37,7 @@ py311-django42:
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py311-django42
|
||||
script: tox -e py311-django52
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
|
@ -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**
|
||||
|
||||
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
|
||||
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
|
||||
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et copier la clé dans .env dans le champ
|
||||
`OIDC_RSA_PRIVATE_KEY`.
|
||||
|
||||
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**
|
||||
|
||||
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
|
||||
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
|
||||
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner le champ
|
||||
`OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`).
|
||||
|
||||
8. *Enjoy \o/*
|
||||
|
||||
|
@ -63,7 +63,8 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
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:
|
||||
qs = qs.none()
|
||||
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
|
||||
|
@ -44,7 +44,7 @@ class TemplateLoggedInTests(TestCase):
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
|
||||
|
||||
def test_logout(self):
|
||||
response = self.client.get(reverse("logout"))
|
||||
response = self.client.post(reverse("logout"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_admin_index(self):
|
||||
|
@ -13,7 +13,7 @@ def register_note_urls(router, path):
|
||||
router.register(path + '/note', NotePolymorphicViewSet)
|
||||
router.register(path + '/alias', AliasViewSet)
|
||||
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/transaction', TransactionViewSet)
|
||||
|
@ -1695,7 +1695,7 @@
|
||||
"wei",
|
||||
"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",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
@ -4046,6 +4046,310 @@
|
||||
"description": "Voir toutes les équipes WEI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 274,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"club"
|
||||
],
|
||||
"query": "{\"bus__wei\": [\"club\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les informations de clubs des bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 275,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"club"
|
||||
],
|
||||
"query": "{\"bus__wei\": [\"club\"]}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Modifier les clubs des bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 276,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus__wei\": [\"club\"]}",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Ajouter un⋅e membre à un club de bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 277,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus__wei\": [\"club\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les adhérents d'un club de bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 278,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus__wei\": [\"club\"]}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Modifier l'adhésion d'un club de bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 279,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"note"
|
||||
],
|
||||
"query": "{\"noteclub__club__bus__wei\": [\"club\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir la note d'un club de bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 280,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}]",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les transactions d'un club de bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 281,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club__bus__wei\": [\"club\"]}, {\"destination__noteclub__club__bus__wei\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Créer une transaction d'un club de bus tant que la source reste au dessus de -20 €"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 282,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"AND\", [\"OR\", {\"source__noteclub__club\": [\"club\"]}, {\"destination__noteclub__club\": [\"club\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]]",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Créer une transaction d'un WEI tant que la source reste au dessus de -20 €"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 283,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"memberships__club__name\": \"Kfet\", \"memberships__roles__name\": \"Adh\u00e9rent\u22c5e Kfet\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir n'importe quel⋅le utilisateur⋅rice qui est adhérent⋅e Kfet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 284,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"club"
|
||||
],
|
||||
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les informations de club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 285,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"club"
|
||||
],
|
||||
"query": "{\"bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Modifier le club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 286,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Ajouter un⋅e membre au club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 287,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les adhérents du club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 288,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"membership"
|
||||
],
|
||||
"query": "{\"club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Modifier l'adhésion au club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 289,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"note"
|
||||
],
|
||||
"query": "{\"noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir la note du club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 290,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"OR\", {\"source__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}, {\"destination__noteclub__club__bus\": [\"membership\", \"weimembership\", \"bus\"]}]",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir les transactions du club de son bus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 291,
|
||||
"fields": {
|
||||
"model": [
|
||||
"wei",
|
||||
"bus"
|
||||
],
|
||||
"query": "{\"pk\": [\"membership\", \"weimembership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}",
|
||||
"type": "view",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"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",
|
||||
"pk": 1,
|
||||
@ -4406,6 +4710,8 @@
|
||||
"name": "GC WEI",
|
||||
"permissions": [
|
||||
22,
|
||||
49,
|
||||
62,
|
||||
70,
|
||||
72,
|
||||
76,
|
||||
@ -4431,9 +4737,22 @@
|
||||
113,
|
||||
128,
|
||||
130,
|
||||
142,
|
||||
269,
|
||||
271,
|
||||
272,
|
||||
273
|
||||
273,
|
||||
274,
|
||||
275,
|
||||
276,
|
||||
277,
|
||||
278,
|
||||
279,
|
||||
280,
|
||||
281,
|
||||
282,
|
||||
283,
|
||||
292
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -4452,7 +4771,14 @@
|
||||
119,
|
||||
120,
|
||||
121,
|
||||
122
|
||||
122,
|
||||
284,
|
||||
285,
|
||||
286,
|
||||
287,
|
||||
289,
|
||||
290,
|
||||
291
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -4650,7 +4976,14 @@
|
||||
119,
|
||||
120,
|
||||
121,
|
||||
122
|
||||
122,
|
||||
284,
|
||||
285,
|
||||
286,
|
||||
287,
|
||||
289,
|
||||
290,
|
||||
291
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -1,8 +1,10 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from oauth2_provider.oauth2_validators import OAuth2Validator
|
||||
from oauth2_provider.scopes import BaseScopes
|
||||
from member.models import Club
|
||||
from note.models import Alias
|
||||
from note_kfet.middlewares import get_current_request
|
||||
|
||||
from .backends import PermissionBackend
|
||||
@ -17,25 +19,46 @@ class PermissionScopes(BaseScopes):
|
||||
"""
|
||||
|
||||
def get_all_scopes(self):
|
||||
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
|
||||
for p in Permission.objects.all() for club in Club.objects.all()}
|
||||
scopes = {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
|
||||
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):
|
||||
if not application:
|
||||
return []
|
||||
return [f"{p.id}_{p.membership.club.id}"
|
||||
for t in Permission.PERMISSION_TYPES
|
||||
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
|
||||
scopes = [f"{p.id}_{p.membership.club.id}"
|
||||
for t in Permission.PERMISSION_TYPES
|
||||
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):
|
||||
if not application:
|
||||
return []
|
||||
return [f"{p.id}_{p.membership.club.id}"
|
||||
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
|
||||
scopes = [f"{p.id}_{p.membership.club.id}"
|
||||
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
|
||||
scopes.append('openid')
|
||||
return scopes
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -54,6 +77,8 @@ class PermissionOAuth2Validator(OAuth2Validator):
|
||||
if scope in scopes:
|
||||
valid_scopes.add(scope)
|
||||
|
||||
request.scopes = valid_scopes
|
||||
if 'openid' in scopes:
|
||||
valid_scopes.add('openid')
|
||||
|
||||
request.scopes = valid_scopes
|
||||
return valid_scopes
|
||||
|
@ -19,6 +19,7 @@ EXCLUDED = [
|
||||
'oauth2_provider.accesstoken',
|
||||
'oauth2_provider.grant',
|
||||
'oauth2_provider.refreshtoken',
|
||||
'oauth2_provider.idtoken',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
@ -10,7 +10,7 @@ from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from activity.models import Activity
|
||||
from member.models import Club, Membership
|
||||
from note.models import NoteUser
|
||||
from note.models import NoteUser, NoteClub
|
||||
from wei.models import WEIClub, Bus, WEIRegistration
|
||||
|
||||
|
||||
@ -122,10 +122,13 @@ class TestPermissionDenied(TestCase):
|
||||
|
||||
def test_validate_weiregistration(self):
|
||||
wei = WEIClub.objects.create(
|
||||
name="WEI Test",
|
||||
membership_start=date.today(),
|
||||
date_start=date.today() + timedelta(days=1),
|
||||
date_end=date.today() + timedelta(days=1),
|
||||
parent_club=Club.objects.get(name="Kfet"),
|
||||
)
|
||||
NoteClub.objects.create(club=wei)
|
||||
registration = WEIRegistration.objects.create(wei=wei, user=self.user, birth_date="2000-01-01")
|
||||
response = self.client.get(reverse("wei:validate_registration", kwargs=dict(pk=registration.pk)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -171,7 +171,7 @@ class ScopesView(LoginRequiredMixin, TemplateView):
|
||||
available_scopes = scopes.get_available_scopes(app)
|
||||
context["scopes"][app] = OrderedDict()
|
||||
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:
|
||||
context["scopes"][app][k] = v
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# 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
|
||||
|
||||
__all__ = [
|
||||
'WEIForm', 'WEIRegistrationForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
||||
'WEIForm', 'WEIRegistrationForm', 'WEIRegistration1AForm', 'WEIRegistration2AForm', 'WEIMembership1AForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm',
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
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 note.models import NoteSpecial, NoteUser
|
||||
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
|
||||
@ -24,6 +24,7 @@ class WEIForm(forms.ModelForm):
|
||||
"membership_end": DatePickerInput(),
|
||||
"date_start": DatePickerInput(),
|
||||
"date_end": DatePickerInput(),
|
||||
"caution_amount": AmountInput(),
|
||||
}
|
||||
|
||||
|
||||
@ -39,9 +40,11 @@ class WEIRegistrationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = WEIRegistration
|
||||
fields = ['user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
|
||||
'health_issues', 'emergency_contact_name', 'emergency_contact_phone', 'first_year',
|
||||
'information_json']
|
||||
fields = [
|
||||
'user', 'soge_credit', 'birth_date', 'gender', 'clothing_size',
|
||||
'health_issues', 'emergency_contact_name', 'emergency_contact_phone',
|
||||
'first_year', 'information_json', 'caution_check'
|
||||
]
|
||||
widgets = {
|
||||
"user": Autocomplete(
|
||||
User,
|
||||
@ -51,11 +54,30 @@ class WEIRegistrationForm(forms.ModelForm):
|
||||
'placeholder': 'Nom ...',
|
||||
},
|
||||
),
|
||||
"birth_date": DatePickerInput(options={'minDate': '1900-01-01',
|
||||
'maxDate': '2100-01-01'}),
|
||||
"birth_date": DatePickerInput(options={
|
||||
'minDate': '1900-01-01',
|
||||
'maxDate': '2100-01-01'
|
||||
}),
|
||||
"caution_check": forms.BooleanField(
|
||||
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):
|
||||
bus = forms.ModelMultipleChoiceField(
|
||||
queryset=Bus.objects,
|
||||
@ -74,7 +96,7 @@ class WEIChooseBusForm(forms.Form):
|
||||
)
|
||||
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=WEIRole.objects.filter(~Q(name="1A")),
|
||||
queryset=WEIRole.objects.filter(~Q(name="1A") & ~Q(name="GC WEI")),
|
||||
label=_("WEI Roles"),
|
||||
help_text=_("Select the roles that you are interested in."),
|
||||
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
|
||||
@ -84,7 +106,7 @@ class WEIChooseBusForm(forms.Form):
|
||||
|
||||
class WEIMembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=WEIRole.objects,
|
||||
queryset=WEIRole.objects.filter(~Q(name="GC WEI")),
|
||||
label=_("WEI Roles"),
|
||||
widget=CheckboxSelectMultiple(),
|
||||
)
|
||||
@ -118,6 +140,19 @@ class WEIMembershipForm(forms.ModelForm):
|
||||
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):
|
||||
cleaned_data = super().clean()
|
||||
if 'team' in cleaned_data and cleaned_data["team"] is not None \
|
||||
@ -129,21 +164,8 @@ class WEIMembershipForm(forms.ModelForm):
|
||||
model = WEIMembership
|
||||
fields = ('roles', 'bus', 'team',)
|
||||
widgets = {
|
||||
"bus": Autocomplete(
|
||||
Bus,
|
||||
attrs={
|
||||
'api_url': '/api/wei/bus/',
|
||||
'placeholder': 'Bus ...',
|
||||
}
|
||||
),
|
||||
"team": Autocomplete(
|
||||
BusTeam,
|
||||
attrs={
|
||||
'api_url': '/api/wei/team/',
|
||||
'placeholder': 'Équipe ...',
|
||||
},
|
||||
resetable=True,
|
||||
),
|
||||
"bus": RadioSelect(),
|
||||
"team": RadioSelect(),
|
||||
}
|
||||
|
||||
|
||||
@ -191,4 +213,3 @@ class BusTeamForm(forms.ModelForm):
|
||||
),
|
||||
"color": ColorWidget(),
|
||||
}
|
||||
# "color": ColorWidget(),
|
||||
|
@ -2,11 +2,11 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||
from .wei2024 import WEISurvey2024
|
||||
from .wei2025 import WEISurvey2025
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||
]
|
||||
|
||||
CurrentSurvey = WEISurvey2024
|
||||
CurrentSurvey = WEISurvey2025
|
||||
|
@ -121,6 +121,13 @@ class WEISurveyAlgorithm:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_bus_information_form(cls):
|
||||
"""
|
||||
The class of the form to update the bus information.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WEISurvey:
|
||||
"""
|
||||
|
347
apps/wei/forms/surveys/wei2025.py
Normal file
347
apps/wei/forms/surveys/wei2025.py
Normal 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()
|
20
apps/wei/migrations/0012_bus_club.py
Normal file
20
apps/wei/migrations/0012_bus_club.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.21 on 2025-05-29 16:16
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0014_create_bda'),
|
||||
('wei', '0011_alter_weiclub_year'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bus',
|
||||
name='club',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bus', to='member.club', verbose_name='club'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -33,6 +33,11 @@ class WEIClub(Club):
|
||||
verbose_name=_("date end"),
|
||||
)
|
||||
|
||||
caution_amount = models.PositiveIntegerField(
|
||||
verbose_name=_("caution amount"),
|
||||
default=0,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WEI")
|
||||
verbose_name_plural = _("WEI")
|
||||
@ -72,6 +77,15 @@ class Bus(models.Model):
|
||||
default=50,
|
||||
)
|
||||
|
||||
club = models.OneToOneField(
|
||||
Club,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="bus",
|
||||
verbose_name=_("club"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
@ -188,6 +202,16 @@ class WEIRegistration(models.Model):
|
||||
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(
|
||||
verbose_name=_("birth date"),
|
||||
)
|
||||
|
@ -40,22 +40,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
|
||||
{% else %}
|
||||
{% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %}
|
||||
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
|
||||
<i class="fa fa-question-circle"
|
||||
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
|
||||
{% endwith %}
|
||||
|
||||
{% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %}
|
||||
<dt class="col-xl-6">{% trans 'WEI fee (paid students)'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}
|
||||
|
||||
<dt class="col-xl-6">{% trans 'WEI fee (unpaid students)'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
|
||||
<i class="fa fa-question-circle"
|
||||
title="{% trans "The BDE membership is included in the WEI registration." %}"></i></dd>
|
||||
{% endwith %}
|
||||
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}
|
||||
{% 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 %}
|
||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
|
||||
|
@ -16,8 +16,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center">
|
||||
{% if object.club %}
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_detail' pk=object.club.pk %}"
|
||||
data-turbolinks="false">{% trans "View club" %}</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}"
|
||||
data-turbolinks="false">{% trans "Edit" %}</a>
|
||||
<a class="btn btn-primary btn-sm my-1" href="{% url 'wei: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 %}"
|
||||
data-turbolinks="false">{% trans "Add team" %}</a>
|
||||
</div>
|
||||
|
@ -95,9 +95,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_validate_1a %}
|
||||
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
|
||||
{% endif %}
|
||||
{% if can_validate_1a %}
|
||||
<a href="{% url 'wei:wei_1A_list' pk=object.pk %}" class="btn btn-block btn-info">{% trans "Attribute buses" %}</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
|
@ -143,25 +143,35 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if registration.user.note.balance < fee %}
|
||||
<div class="alert alert-danger">
|
||||
{% with pretty_fee=fee|pretty_money %}
|
||||
{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
|
||||
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required).
|
||||
The registration may fail if you don't credit the note now.
|
||||
{% endblocktrans %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
{% blocktrans trimmed with pretty_fee=fee|pretty_money %}
|
||||
The note has enough money ({{ pretty_fee }} required), the registration is possible.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="alert {% if registration.user.note.balance < fee %}alert-danger{% else %}alert-success{% endif %}">
|
||||
<h5>{% trans "Required payments:" %}</h5>
|
||||
<ul>
|
||||
<li>{% blocktrans trimmed with amount=fee|pretty_money %}
|
||||
Membership fees: {{ amount }}
|
||||
{% endblocktrans %}</li>
|
||||
{% if registration.caution_type == 'note' %}
|
||||
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
|
||||
Deposit (by Note transaction): {{ amount }}
|
||||
{% endblocktrans %}</li>
|
||||
<li><strong>{% blocktrans trimmed with total=total_needed|pretty_money %}
|
||||
Total needed: {{ total }}
|
||||
{% endblocktrans %}</strong></li>
|
||||
{% else %}
|
||||
<li>{% blocktrans trimmed with amount=club.caution_amount|pretty_money %}
|
||||
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 %}
|
||||
|
||||
{% 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">
|
||||
{% trans "The user didn't give her/his caution check." %}
|
||||
</div>
|
||||
@ -200,4 +210,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
}
|
||||
}
|
||||
</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 %}
|
||||
|
@ -6,8 +6,6 @@ from datetime import date, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from note.models import NoteUser
|
||||
|
||||
from ..forms.surveys.wei2024 import WEIBusInformation2024, WEISurvey2024, WORDS, WEISurveyInformation2024
|
||||
from ..models import Bus, WEIClub, WEIRegistration
|
||||
@ -129,44 +127,3 @@ class TestWEIAlgorithm(TestCase):
|
||||
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
|
||||
|
||||
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
|
||||
|
||||
def test_register_1a(self):
|
||||
"""
|
||||
Test register a first year member to the WEI and complete the survey
|
||||
"""
|
||||
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
user = User.objects.create(username="toto", email="toto@example.com")
|
||||
NoteUser.objects.create(user=user)
|
||||
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||
user=user.id,
|
||||
soge_credit=True,
|
||||
birth_date=date(2000, 1, 1),
|
||||
gender='nonbinary',
|
||||
clothing_cut='female',
|
||||
clothing_size='XS',
|
||||
health_issues='I am a bot',
|
||||
emergency_contact_name='NoteKfet2020',
|
||||
emergency_contact_phone='+33123456789',
|
||||
))
|
||||
qs = WEIRegistration.objects.filter(user_id=user.id)
|
||||
self.assertTrue(qs.exists())
|
||||
registration = qs.get()
|
||||
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
|
||||
for question in WORDS:
|
||||
# Fill 1A Survey, 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())
|
||||
|
111
apps/wei/tests/test_wei_algorithm_2025.py
Normal file
111
apps/wei/tests/test_wei_algorithm_2025.py
Normal 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 %
|
@ -126,6 +126,7 @@ class TestWEIRegistration(TestCase):
|
||||
year=self.year + 1,
|
||||
date_start=str(self.year + 1) + "-09-01",
|
||||
date_end=str(self.year + 1) + "-09-03",
|
||||
caution_amount=12000,
|
||||
))
|
||||
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
|
||||
self.assertTrue(qs.exists())
|
||||
@ -160,6 +161,7 @@ class TestWEIRegistration(TestCase):
|
||||
membership_end="2000-09-30",
|
||||
date_start="2000-09-01",
|
||||
date_end="2000-09-03",
|
||||
caution_amount=12000,
|
||||
))
|
||||
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)
|
||||
@ -318,6 +320,7 @@ class TestWEIRegistration(TestCase):
|
||||
bus=[],
|
||||
team=[],
|
||||
roles=[],
|
||||
caution_type='check'
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.context["membership_form"].is_valid())
|
||||
@ -334,7 +337,8 @@ class TestWEIRegistration(TestCase):
|
||||
emergency_contact_phone='+33123456789',
|
||||
bus=[self.bus.id],
|
||||
team=[self.team.id],
|
||||
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
|
||||
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)
|
||||
self.assertTrue(qs.exists())
|
||||
@ -354,6 +358,7 @@ class TestWEIRegistration(TestCase):
|
||||
bus=[self.bus.id],
|
||||
team=[self.team.id],
|
||||
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
|
||||
caution_type='check'
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue("This user is already registered to this WEI." in str(response.context["form"].errors))
|
||||
@ -506,11 +511,12 @@ class TestWEIRegistration(TestCase):
|
||||
team=[self.team.id],
|
||||
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
|
||||
information_json=self.registration.information_json,
|
||||
caution_type='check'
|
||||
)
|
||||
)
|
||||
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
|
||||
self.assertTrue(qs.exists())
|
||||
self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
|
||||
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200)
|
||||
|
||||
# Check the page when the registration is already validated
|
||||
membership = WEIMembership(
|
||||
@ -560,11 +566,12 @@ class TestWEIRegistration(TestCase):
|
||||
team=[self.team.id],
|
||||
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
|
||||
information_json=self.registration.information_json,
|
||||
caution_type='check'
|
||||
)
|
||||
)
|
||||
qs = WEIRegistration.objects.filter(user_id=self.user.id, clothing_size="L")
|
||||
self.assertTrue(qs.exists())
|
||||
self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
|
||||
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=qs.get().wei.pk)), 302, 200)
|
||||
|
||||
# Test invalid form
|
||||
response = self.client.post(
|
||||
@ -583,6 +590,7 @@ class TestWEIRegistration(TestCase):
|
||||
team=[],
|
||||
roles=[],
|
||||
information_json=self.registration.information_json,
|
||||
caution_type='check'
|
||||
)
|
||||
)
|
||||
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_team = BusTeam.objects.create(bus=second_bus, name="Second team", color=42)
|
||||
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
||||
roles=[WEIRole.objects.get(name="GC WEI").id],
|
||||
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
|
||||
bus=self.bus.pk,
|
||||
team=second_team.pk,
|
||||
credit_type=4, # Bank transfer
|
||||
@ -632,13 +640,14 @@ class TestWEIRegistration(TestCase):
|
||||
last_name="admin",
|
||||
first_name="admin",
|
||||
bank="Société générale",
|
||||
caution_check=True,
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.context["form"].is_valid())
|
||||
self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors))
|
||||
|
||||
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
||||
roles=[WEIRole.objects.get(name="GC WEI").id],
|
||||
roles=[WEIRole.objects.get(name="Adhérent⋅e WEI").id],
|
||||
bus=self.bus.pk,
|
||||
team=self.team.pk,
|
||||
credit_type=4, # Bank transfer
|
||||
@ -646,8 +655,10 @@ class TestWEIRegistration(TestCase):
|
||||
last_name="admin",
|
||||
first_name="admin",
|
||||
bank="Société générale",
|
||||
caution_check=True,
|
||||
))
|
||||
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
|
||||
|
||||
# Check if the membership is successfully created
|
||||
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
|
||||
self.assertTrue(membership.exists())
|
||||
@ -767,7 +778,7 @@ class TestDefaultWEISurvey(TestCase):
|
||||
WEISurvey.update_form(None, None)
|
||||
|
||||
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):
|
||||
|
@ -4,7 +4,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import CurrentWEIDetailView, WEI1AListView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView, \
|
||||
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, \
|
||||
WEIRegistrationsView, WEIMembershipsView, MemberListRenderView, BusInformationUpdateView, \
|
||||
BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView, \
|
||||
WEIAttributeBus1AView, WEIAttributeBus1ANextView, WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, \
|
||||
WEIDeleteRegistrationView, WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView
|
||||
@ -42,4 +42,5 @@ urlpatterns = [
|
||||
path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"),
|
||||
path('bus-1A/<int:pk>/', WEIAttributeBus1AView.as_view(), name="wei_bus_1A"),
|
||||
path('bus-1A/next/<int:pk>/', WEIAttributeBus1ANextView.as_view(), name="wei_bus_1A_next"),
|
||||
path('update-bus-info/<int:pk>/', BusInformationUpdateView.as_view(), name="update_bus_info"),
|
||||
]
|
||||
|
@ -4,7 +4,7 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import date, timedelta
|
||||
from datetime import date
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from django.conf import settings
|
||||
@ -21,7 +21,7 @@ from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, UpdateView, RedirectView, TemplateView, CreateView
|
||||
from django.views.generic import DetailView, UpdateView, RedirectView, TemplateView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import BaseFormView, DeleteView
|
||||
from django_tables2 import SingleTableView, MultiTableMixin
|
||||
@ -35,11 +35,10 @@ from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
|
||||
from .forms.registration import WEIChooseBusForm
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole
|
||||
from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembership1AForm, \
|
||||
from .forms import WEIForm, WEIRegistrationForm, WEIRegistration1AForm, WEIRegistration2AForm, BusForm, BusTeamForm, WEIMembership1AForm, \
|
||||
WEIMembershipForm, CurrentSurvey
|
||||
from .tables import BusRepartitionTable, BusTable, BusTeamTable, WEITable, WEIRegistrationTable, \
|
||||
WEIRegistration1ATable, WEIMembershipTable
|
||||
from .forms.surveys import CurrentSurvey
|
||||
|
||||
|
||||
class CurrentWEIDetailView(LoginRequiredMixin, RedirectView):
|
||||
@ -443,13 +442,10 @@ class BusTeamCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
def get_template_names(self):
|
||||
names = super().get_template_names()
|
||||
return names
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
@ -482,13 +478,10 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
def get_template_names(self):
|
||||
names = super().get_template_names()
|
||||
return names
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
@ -517,7 +510,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
Register a new user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
form_class = WEIRegistration1AForm
|
||||
extra_context = {"title": _("Register first year student to the WEI")}
|
||||
|
||||
def get_sample_object(self):
|
||||
@ -563,7 +556,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields["user"].initial = self.request.user
|
||||
|
||||
|
||||
# Cacher les champs pendant l'inscription initiale
|
||||
if "first_year" in form.fields:
|
||||
del form.fields["first_year"]
|
||||
@ -571,7 +564,9 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
del form.fields["caution_check"]
|
||||
if "information_json" in form.fields:
|
||||
del form.fields["information_json"]
|
||||
|
||||
if "caution_type" in form.fields:
|
||||
del form.fields["caution_type"]
|
||||
|
||||
return form
|
||||
|
||||
@transaction.atomic
|
||||
@ -609,7 +604,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
Register an old user to the WEI
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
form_class = WEIRegistration2AForm
|
||||
extra_context = {"title": _("Register old student to the WEI")}
|
||||
|
||||
def get_sample_object(self):
|
||||
@ -675,6 +670,12 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
if "information_json" in form.fields:
|
||||
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
|
||||
|
||||
@transaction.atomic
|
||||
@ -700,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_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
form.instance.information = information
|
||||
|
||||
# Sauvegarder le type de caution
|
||||
form.instance.caution_type = form.cleaned_data["caution_type"]
|
||||
form.instance.save()
|
||||
|
||||
if 'treasury' in settings.INSTALLED_APPS:
|
||||
@ -774,10 +778,18 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||
# Masquer le champ caution_check pour tout le monde dans le formulaire de modification
|
||||
if "caution_check" in form.fields:
|
||||
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
|
||||
|
||||
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_amount"]
|
||||
del membership_form.fields["first_name"]
|
||||
@ -797,22 +809,22 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||
membership = form.instance.membership
|
||||
if membership is None:
|
||||
raise ValueError(_("No membership found for this registration"))
|
||||
|
||||
|
||||
membership_form = self.get_membership_form(self.request.POST, instance=membership)
|
||||
if not membership_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
# Vérifier que l'utilisateur a la permission de modifier le membership
|
||||
# On vérifie d'abord si l'utilisateur a la permission générale de modification
|
||||
if not self.request.user.has_perm("wei.change_weimembership"):
|
||||
raise PermissionDenied(_("You don't have the permission to update memberships"))
|
||||
|
||||
|
||||
# On vérifie ensuite les permissions spécifiques pour chaque champ modifié
|
||||
for field_name in membership_form.changed_data:
|
||||
perm = f"wei.change_weimembership_{field_name}"
|
||||
if not self.request.user.has_perm(perm):
|
||||
raise PermissionDenied(_("You don't have the permission to update the field %(field)s") % {'field': field_name})
|
||||
|
||||
|
||||
membership_form.save()
|
||||
except (WEIMembership.DoesNotExist, ValueError, PermissionDenied) as e:
|
||||
form.add_error(None, str(e))
|
||||
@ -831,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_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]]
|
||||
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()
|
||||
|
||||
return super().form_valid(form)
|
||||
@ -876,20 +892,30 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
|
||||
return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,))
|
||||
|
||||
|
||||
class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
Validate WEI Registration
|
||||
"""
|
||||
model = WEIMembership
|
||||
extra_context = {"title": _("Validate WEI registration")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Vérifier d'abord si l'utilisateur a la permission générale
|
||||
if not request.user.has_perm("wei.add_weimembership"):
|
||||
raise PermissionDenied(_("You don't have the permission to validate registrations"))
|
||||
|
||||
def get_sample_object(self):
|
||||
"""
|
||||
Return a sample object for permission checking
|
||||
"""
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
return WEIMembership(
|
||||
user=registration.user,
|
||||
club=registration.wei,
|
||||
date_start=registration.wei.date_start,
|
||||
fee=registration.wei.membership_fee_paid if registration.user.profile.paid else registration.wei.membership_fee_unpaid,
|
||||
# Add any fields needed for proper permission checking
|
||||
registration=registration,
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
wei = registration.wei
|
||||
today = date.today()
|
||||
# We can't validate anyone once the WEI is started and before the membership start date
|
||||
@ -921,7 +947,14 @@ class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
date_start__gte=bde.membership_start,
|
||||
).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"]
|
||||
if registration.soge_credit:
|
||||
@ -933,10 +966,17 @@ class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
|
||||
def get_form_class(self):
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
if registration.first_year and 'sleected_bus_pk' not in registration.information:
|
||||
if registration.first_year and 'selected_bus_pk' not in registration.information:
|
||||
return WEIMembership1AForm
|
||||
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):
|
||||
form = super().get_form(form_class)
|
||||
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
|
||||
@ -945,12 +985,22 @@ class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
|
||||
# Ajouter le champ caution_check uniquement pour les non-première année et le rendre obligatoire
|
||||
if not registration.first_year:
|
||||
form.fields["caution_check"] = forms.BooleanField(
|
||||
required=True,
|
||||
initial=registration.caution_check,
|
||||
label=_("Caution check given"),
|
||||
help_text=_("Please make sure the check is given before validating the registration")
|
||||
)
|
||||
if registration.caution_type == 'check':
|
||||
form.fields["caution_check"] = forms.BooleanField(
|
||||
required=True,
|
||||
initial=registration.caution_check,
|
||||
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:
|
||||
form.fields["credit_type"].disabled = True
|
||||
@ -1034,10 +1084,20 @@ class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
if credit_type is None or registration.soge_credit:
|
||||
credit_amount = 0
|
||||
|
||||
if not registration.soge_credit and user.note.balance + credit_amount < fee:
|
||||
# Users must have money before registering to the WEI.
|
||||
# Calculer le montant total nécessaire (frais + caution si transaction)
|
||||
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',
|
||||
_("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)
|
||||
|
||||
if credit_amount:
|
||||
@ -1077,6 +1137,18 @@ class WEIValidateRegistrationView(LoginRequiredMixin, CreateView):
|
||||
membership.refresh_from_db()
|
||||
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)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -1296,6 +1368,7 @@ class WEI1AListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
|
||||
def get_queryset(self, filter_permissions=True, **kwargs):
|
||||
qs = super().get_queryset(filter_permissions, **kwargs)
|
||||
qs = qs.filter(first_year=True, membership__isnull=False)
|
||||
qs = qs.filter(wei=self.club)
|
||||
qs = qs.order_by('-membership__bus')
|
||||
return qs
|
||||
|
||||
@ -1338,7 +1411,7 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
|
||||
if not wei.exists():
|
||||
raise Http404
|
||||
wei = wei.get()
|
||||
|
||||
|
||||
# On cherche d'abord les 1A qui ont une inscription validée (membership) mais pas de bus
|
||||
qs = WEIRegistration.objects.filter(
|
||||
wei=wei,
|
||||
@ -1346,14 +1419,40 @@ class WEIAttributeBus1ANextView(LoginRequiredMixin, RedirectView):
|
||||
membership__isnull=False,
|
||||
membership__bus__isnull=True
|
||||
)
|
||||
|
||||
|
||||
# Parmi eux, on prend ceux qui ont répondu au questionnaire (ont un bus préféré)
|
||||
qs = qs.filter(information_json__contains='selected_bus_pk')
|
||||
|
||||
|
||||
if not qs.exists():
|
||||
# Si on ne trouve personne, on affiche un message et on retourne à la liste
|
||||
messages.info(self.request, _("No first year student without a bus found. Either all of them have a bus, or none has filled the survey yet."))
|
||||
return reverse_lazy('wei:wei_1A_list', args=(wei.pk,))
|
||||
|
||||
|
||||
# On redirige vers la page d'attribution pour le premier étudiant trouvé
|
||||
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})
|
||||
|
@ -136,7 +136,7 @@ de diffusion utiles.
|
||||
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``,
|
||||
``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
|
||||
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
|
||||
malheureusement pas aussi simple que de simplement supposer que ces listes sont exhaustives.
|
||||
|
@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \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"
|
||||
"Last-Translator: bleizi <bleizi@crans.org>\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/permission/models.py:329
|
||||
#: 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/weimembership_form.html:14 apps/wrapped/models.py:16
|
||||
msgid "name"
|
||||
@ -101,7 +101,7 @@ msgstr "Vearnstaltungarte"
|
||||
#: apps/activity/models.py:68
|
||||
#: apps/activity/templates/activity/includes/activity_info.html:19
|
||||
#: 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"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
@ -122,7 +122,7 @@ msgstr "Type"
|
||||
|
||||
#: 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/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
|
||||
msgid "user"
|
||||
msgstr "User"
|
||||
@ -295,14 +295,14 @@ msgstr "Type"
|
||||
|
||||
#: apps/activity/tables.py:86 apps/member/forms.py:199
|
||||
#: 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"
|
||||
msgstr "Nachname"
|
||||
|
||||
#: apps/activity/tables.py:88 apps/member/forms.py:204
|
||||
#: apps/note/templates/note/transaction_form.html:138
|
||||
#: 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"
|
||||
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."
|
||||
|
||||
#: 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"
|
||||
msgstr "Kredittype"
|
||||
|
||||
#: 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"
|
||||
msgstr "Kein Kredit"
|
||||
|
||||
@ -1044,13 +1044,13 @@ msgid "You can credit the note of the user."
|
||||
msgstr "Sie dûrfen diese Note kreditieren."
|
||||
|
||||
#: 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"
|
||||
msgstr "Kreditanzahl"
|
||||
|
||||
#: apps/member/forms.py:209 apps/note/templates/note/transaction_form.html:144
|
||||
#: apps/registration/forms.py:101 apps/treasury/forms.py:135
|
||||
#: apps/wei/forms/registration.py:117
|
||||
#: apps/wei/forms/registration.py:126
|
||||
msgid "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/profile_info.html:40
|
||||
#: 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
|
||||
msgid "email"
|
||||
msgstr "Email"
|
||||
@ -1311,7 +1311,7 @@ msgid "add to registration form"
|
||||
msgstr "Registrierung validieren"
|
||||
|
||||
#: 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"
|
||||
msgstr "Club"
|
||||
|
||||
@ -1514,13 +1514,13 @@ msgstr "Mitgliedsachftpreis"
|
||||
#: apps/member/templates/member/includes/club_info.html:43
|
||||
#: apps/member/templates/member/includes/profile_info.html:55
|
||||
#: apps/treasury/templates/treasury/sogecredit_detail.html:24
|
||||
#: apps/wei/templates/wei/base.html:60
|
||||
#: apps/wei/templates/wei/base.html:58
|
||||
msgid "balance"
|
||||
msgstr "Kontostand"
|
||||
|
||||
#: apps/member/templates/member/includes/club_info.html:47
|
||||
#: 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"
|
||||
msgstr "Aliases"
|
||||
|
||||
@ -1702,7 +1702,7 @@ msgstr "Club bearbeiten"
|
||||
msgid "Add new member to the club"
|
||||
msgstr "Neue Mitglieder"
|
||||
|
||||
#: apps/member/views.py:750 apps/wei/views.py:1040
|
||||
#: apps/member/views.py:750
|
||||
msgid ""
|
||||
"This user don't have enough money to join this club, and can't have a "
|
||||
"negative balance."
|
||||
@ -2038,8 +2038,8 @@ msgstr ""
|
||||
"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:363 apps/wei/views.py:1045
|
||||
#: apps/wei/views.py:1049
|
||||
#: apps/note/models/transactions.py:363 apps/wei/views.py:1097
|
||||
#: apps/wei/views.py:1101
|
||||
#: env/lib/python3.11/site-packages/django/forms/fields.py:91
|
||||
msgid "This field is required."
|
||||
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/wei/tables.py:49 apps/wei/tables.py:50
|
||||
#: apps/wei/templates/wei/base.html:89
|
||||
#: apps/wei/templates/wei/bus_detail.html:20
|
||||
#: apps/wei/templates/wei/base.html:87
|
||||
#: apps/wei/templates/wei/bus_detail.html:24
|
||||
#: apps/wei/templates/wei/busteam_detail.html:20
|
||||
#: apps/wei/templates/wei/busteam_detail.html:42
|
||||
#: 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/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"
|
||||
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"
|
||||
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/models.py:62 apps/wei/models.py:178
|
||||
#: apps/wei/apps.py:10 apps/wei/models.py:42 apps/wei/models.py:43
|
||||
#: apps/wei/models.py:67 apps/wei/models.py:192
|
||||
#: note_kfet/templates/base.html:108
|
||||
msgid "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"
|
||||
msgstr ""
|
||||
|
||||
#: apps/wei/forms/registration.py:62 apps/wei/models.py:126
|
||||
#: apps/wei/models.py:324
|
||||
#: apps/wei/forms/registration.py:71 apps/wei/models.py:140
|
||||
#: apps/wei/models.py:348
|
||||
msgid "bus"
|
||||
msgstr "Bus"
|
||||
|
||||
#: apps/wei/forms/registration.py:63
|
||||
#: apps/wei/forms/registration.py:72
|
||||
msgid ""
|
||||
"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."
|
||||
@ -3113,11 +3113,11 @@ msgstr ""
|
||||
"einen Bus und ein Team zuzuweisen, insbesondere wenn Sie ein freies Elektron "
|
||||
"sind."
|
||||
|
||||
#: apps/wei/forms/registration.py:70
|
||||
#: apps/wei/forms/registration.py:79
|
||||
msgid "Team"
|
||||
msgstr "Team"
|
||||
|
||||
#: apps/wei/forms/registration.py:72
|
||||
#: apps/wei/forms/registration.py:81
|
||||
msgid ""
|
||||
"Leave this field empty if you won't be in a team (staff, bus chief, free "
|
||||
"electron)"
|
||||
@ -3125,16 +3125,16 @@ msgstr ""
|
||||
"Lassen Sie dieses Feld leer, wenn Sie nicht in einem Team sind (Mitarbeiter, "
|
||||
"Buschef, freies Elektron)"
|
||||
|
||||
#: apps/wei/forms/registration.py:78 apps/wei/forms/registration.py:88
|
||||
#: apps/wei/models.py:160
|
||||
#: apps/wei/forms/registration.py:87 apps/wei/forms/registration.py:97
|
||||
#: apps/wei/models.py:174
|
||||
msgid "WEI Roles"
|
||||
msgstr "WEI Rollen"
|
||||
|
||||
#: apps/wei/forms/registration.py:79
|
||||
#: apps/wei/forms/registration.py:88
|
||||
msgid "Select the roles that you are interested in."
|
||||
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."
|
||||
msgstr "Dieses Team gehört nicht zum angegebenen Bus."
|
||||
|
||||
@ -3156,118 +3156,140 @@ msgstr "Anfangsdatum"
|
||||
msgid "date end"
|
||||
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
|
||||
#| msgid "The user joined the bus"
|
||||
msgid "seat count in the bus"
|
||||
msgstr "Der Benutzer ist dem Bus beigetreten"
|
||||
|
||||
#: apps/wei/models.py:83
|
||||
#: apps/wei/models.py:97
|
||||
msgid "survey information"
|
||||
msgstr "Umfrage Infos"
|
||||
|
||||
#: apps/wei/models.py:84
|
||||
#: apps/wei/models.py:98
|
||||
msgid "Information about the survey for new members, encoded 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"
|
||||
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"
|
||||
msgstr "Buses"
|
||||
|
||||
#: apps/wei/models.py:135
|
||||
#: apps/wei/models.py:149
|
||||
msgid "color"
|
||||
msgstr "Farbe"
|
||||
|
||||
#: apps/wei/models.py:136
|
||||
#: apps/wei/models.py:150
|
||||
msgid "The color of the T-Shirt, stored with its number equivalent"
|
||||
msgstr "Die Farbe des T-Shirts, gespeichert mit der entsprechenden Nummer"
|
||||
|
||||
#: apps/wei/models.py:147
|
||||
#: apps/wei/models.py:161
|
||||
msgid "Bus team"
|
||||
msgstr "Bus Team"
|
||||
|
||||
#: apps/wei/models.py:148
|
||||
#: apps/wei/models.py:162
|
||||
msgid "Bus teams"
|
||||
msgstr "Bus Teams"
|
||||
|
||||
#: apps/wei/models.py:159
|
||||
#: apps/wei/models.py:173
|
||||
msgid "WEI Role"
|
||||
msgstr "WEI Rolle"
|
||||
|
||||
#: apps/wei/models.py:183
|
||||
#: apps/wei/models.py:197
|
||||
msgid "Credit from 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"
|
||||
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"
|
||||
msgstr "Geburtsdatum"
|
||||
|
||||
#: apps/wei/models.py:198 apps/wei/models.py:208
|
||||
#: apps/wei/models.py:222 apps/wei/models.py:232
|
||||
msgid "Male"
|
||||
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"
|
||||
msgstr "Weiblich"
|
||||
|
||||
#: apps/wei/models.py:200
|
||||
#: apps/wei/models.py:224
|
||||
msgid "Non binary"
|
||||
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
|
||||
msgid "gender"
|
||||
msgstr "Geschlecht"
|
||||
|
||||
#: apps/wei/models.py:210
|
||||
#: apps/wei/models.py:234
|
||||
msgid "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"
|
||||
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"
|
||||
msgstr "Kleidergröße"
|
||||
|
||||
#: apps/wei/models.py:232
|
||||
#: apps/wei/models.py:256
|
||||
msgid "health issues"
|
||||
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"
|
||||
msgstr "Notfall-Kontakt"
|
||||
|
||||
#: apps/wei/models.py:238
|
||||
#: apps/wei/models.py:262
|
||||
msgid "The emergency contact must not be a WEI participant"
|
||||
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"
|
||||
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"
|
||||
msgstr "Erste Jahr"
|
||||
|
||||
#: apps/wei/models.py:249
|
||||
#: apps/wei/models.py:273
|
||||
msgid "Tells if the user is new in the school."
|
||||
msgstr "Gibt an, ob der USer neu in der Schule ist."
|
||||
|
||||
#: apps/wei/models.py:254
|
||||
#: apps/wei/models.py:278
|
||||
msgid "registration information"
|
||||
msgstr "Registrierung Detailen"
|
||||
|
||||
#: apps/wei/models.py:255
|
||||
#: apps/wei/models.py:279
|
||||
msgid ""
|
||||
"Information about the registration (buses for old members, survey for the "
|
||||
"new members), encoded in JSON"
|
||||
@ -3275,27 +3297,27 @@ msgstr ""
|
||||
"Informationen zur Registrierung (Busse für alte Mitglieder, Umfrage für neue "
|
||||
"Mitglieder), verschlüsselt in JSON"
|
||||
|
||||
#: apps/wei/models.py:261
|
||||
#: apps/wei/models.py:285
|
||||
msgid "WEI User"
|
||||
msgstr "WEI User"
|
||||
|
||||
#: apps/wei/models.py:262
|
||||
#: apps/wei/models.py:286
|
||||
msgid "WEI Users"
|
||||
msgstr "WEI Users"
|
||||
|
||||
#: apps/wei/models.py:334
|
||||
#: apps/wei/models.py:358
|
||||
msgid "team"
|
||||
msgstr "Team"
|
||||
|
||||
#: apps/wei/models.py:344
|
||||
#: apps/wei/models.py:368
|
||||
msgid "WEI registration"
|
||||
msgstr "WEI Registrierung"
|
||||
|
||||
#: apps/wei/models.py:348
|
||||
#: apps/wei/models.py:372
|
||||
msgid "WEI membership"
|
||||
msgstr "WEI Mitgliedschaft"
|
||||
|
||||
#: apps/wei/models.py:349
|
||||
#: apps/wei/models.py:373
|
||||
msgid "WEI memberships"
|
||||
msgstr "WEI Mitgliedschaften"
|
||||
|
||||
@ -3327,7 +3349,7 @@ msgstr "Jahr"
|
||||
msgid "preferred 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
|
||||
msgid "Teams"
|
||||
msgstr "Teams"
|
||||
@ -3401,44 +3423,52 @@ msgstr "Tastenliste"
|
||||
msgid "WEI fee (paid students)"
|
||||
msgstr "WEI Preis (bezahlte Studenten)"
|
||||
|
||||
#: apps/wei/templates/wei/base.html:47 apps/wei/templates/wei/base.html:54
|
||||
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
|
||||
#: apps/wei/templates/wei/base.html:47
|
||||
msgid "WEI fee (unpaid students)"
|
||||
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"
|
||||
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"
|
||||
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+"
|
||||
msgstr "2A+ Registrieren"
|
||||
|
||||
#: apps/wei/templates/wei/base.html:93
|
||||
#: apps/wei/templates/wei/base.html:91
|
||||
msgid "Add bus"
|
||||
msgstr "Neue Bus"
|
||||
|
||||
#: apps/wei/templates/wei/base.html:97
|
||||
#: apps/wei/templates/wei/base.html:95
|
||||
msgid "View WEI"
|
||||
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
|
||||
msgid "Add team"
|
||||
msgstr "Neue Team"
|
||||
|
||||
#: apps/wei/templates/wei/bus_detail.html:45
|
||||
#: apps/wei/templates/wei/bus_detail.html:49
|
||||
msgid "Members"
|
||||
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/weimembership_list.html:31
|
||||
msgid "View as PDF"
|
||||
@ -3446,8 +3476,8 @@ msgstr "Als PDF schauen"
|
||||
|
||||
#: apps/wei/templates/wei/survey.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/views.py:1150 apps/wei/views.py:1197
|
||||
#: apps/wei/templates/wei/survey_end.html:11 apps/wei/views.py:1159
|
||||
#: apps/wei/views.py:1214 apps/wei/views.py:1261
|
||||
msgid "Survey WEI"
|
||||
msgstr "WEI Umfrage"
|
||||
|
||||
@ -3491,7 +3521,7 @@ msgstr "Unvalidierte Registrierungen"
|
||||
msgid "Attribute buses"
|
||||
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"
|
||||
msgstr "Neue WEI"
|
||||
|
||||
@ -3575,29 +3605,42 @@ msgstr ""
|
||||
"validieren, sobald die Bank die Erstellung des Kontos validiert hat, oder "
|
||||
"die Zahlungsmethode ändern."
|
||||
|
||||
#: apps/wei/templates/wei/weimembership_form.html:147
|
||||
msgid "Required payments:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/wei/templates/wei/weimembership_form.html:149
|
||||
#, 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."
|
||||
#, fuzzy, python-format
|
||||
#| msgid "membership fee (paid students)"
|
||||
msgid "Membership fees: %(amount)s"
|
||||
msgstr "Mitgliedschaftpreis (bezahlte Studenten)"
|
||||
|
||||
#: apps/wei/templates/wei/weimembership_form.html:157
|
||||
#: apps/wei/templates/wei/weimembership_form.html:153
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The note has enough money (%(pretty_fee)s required), the registration is "
|
||||
"possible."
|
||||
msgid "Deposit (by Note transaction): %(amount)s"
|
||||
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."
|
||||
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 ""
|
||||
"This user is not a member of the Kfet club for the coming year. The "
|
||||
"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..."
|
||||
msgstr "Validierte Mitgliedschaften anzeigen ..."
|
||||
|
||||
#: apps/wei/views.py:62
|
||||
#: apps/wei/views.py:61
|
||||
msgid "Search WEI"
|
||||
msgstr "WEI finden"
|
||||
|
||||
#: apps/wei/views.py:113
|
||||
#: apps/wei/views.py:112
|
||||
msgid "WEI Detail"
|
||||
msgstr "WEI Infos"
|
||||
|
||||
#: apps/wei/views.py:213
|
||||
#: apps/wei/views.py:212
|
||||
msgid "View members of the WEI"
|
||||
msgstr "Mitglied der WEI schauen"
|
||||
|
||||
#: apps/wei/views.py:246
|
||||
#: apps/wei/views.py:245
|
||||
msgid "Find WEI Membership"
|
||||
msgstr "WEI Mitgliedschaft finden"
|
||||
|
||||
#: apps/wei/views.py:256
|
||||
#: apps/wei/views.py:255
|
||||
msgid "View registrations to the WEI"
|
||||
msgstr "Mitglied der WEI schauen"
|
||||
|
||||
#: apps/wei/views.py:285
|
||||
#: apps/wei/views.py:284
|
||||
msgid "Find WEI Registration"
|
||||
msgstr "WEI Registrierung finden"
|
||||
|
||||
#: apps/wei/views.py:296
|
||||
#: apps/wei/views.py:295
|
||||
msgid "Update the WEI"
|
||||
msgstr "WEI bearbeiten"
|
||||
|
||||
#: apps/wei/views.py:317
|
||||
#: apps/wei/views.py:316
|
||||
msgid "Create new bus"
|
||||
msgstr "Neue Bus"
|
||||
|
||||
#: apps/wei/views.py:355
|
||||
#: apps/wei/views.py:354
|
||||
msgid "Update bus"
|
||||
msgstr "Bus bearbeiten"
|
||||
|
||||
#: apps/wei/views.py:387
|
||||
#: apps/wei/views.py:386
|
||||
msgid "Manage bus"
|
||||
msgstr "Bus ändern"
|
||||
|
||||
#: apps/wei/views.py:414
|
||||
#: apps/wei/views.py:413
|
||||
msgid "Create new team"
|
||||
msgstr "Neue Bus Team"
|
||||
|
||||
#: apps/wei/views.py:461
|
||||
#: apps/wei/views.py:457
|
||||
msgid "Update team"
|
||||
msgstr "Team bearbeiten"
|
||||
|
||||
#: apps/wei/views.py:499
|
||||
#: apps/wei/views.py:492
|
||||
msgid "Manage WEI team"
|
||||
msgstr "WEI Team bearbeiten"
|
||||
|
||||
#: apps/wei/views.py:521
|
||||
#: apps/wei/views.py:514
|
||||
msgid "Register first year student to the 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."
|
||||
msgstr "Dieser Benutzer ist bereits bei dieser WEI registriert."
|
||||
|
||||
#: apps/wei/views.py:590
|
||||
#: apps/wei/views.py:585
|
||||
msgid ""
|
||||
"This user can't be in her/his first year since he/she has already "
|
||||
"participated to a WEI."
|
||||
@ -3701,25 +3744,29 @@ msgstr ""
|
||||
"Dieser Benutzer kann nicht in seinem ersten Jahr sein, da er bereits an "
|
||||
"einer WEI teilgenommen hat."
|
||||
|
||||
#: apps/wei/views.py:613
|
||||
#: apps/wei/views.py:608
|
||||
msgid "Register old student to the 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."
|
||||
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"
|
||||
msgstr "WEI Registrierung aktualisieren"
|
||||
|
||||
#: apps/wei/views.py:799
|
||||
#: apps/wei/views.py:810
|
||||
#, fuzzy
|
||||
#| msgid "The BDE membership is included in the WEI registration."
|
||||
msgid "No membership found for this registration"
|
||||
msgstr "Die BDE-Mitgliedschaft ist in der WEI-Registrierung enthalten."
|
||||
|
||||
#: apps/wei/views.py:808
|
||||
#: apps/wei/views.py:819
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "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}. "
|
||||
"{model_name} hinzufügen."
|
||||
|
||||
#: apps/wei/views.py:814
|
||||
#: apps/wei/views.py:825
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "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}. "
|
||||
"{model_name} zulöschen."
|
||||
|
||||
#: apps/wei/views.py:855
|
||||
#: apps/wei/views.py:870
|
||||
msgid "Delete WEI registration"
|
||||
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."
|
||||
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"
|
||||
msgstr "Überprüfen Sie die WEI-Registrierung"
|
||||
|
||||
#: apps/wei/views.py:889
|
||||
#, 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
|
||||
#: apps/wei/views.py:985
|
||||
#, fuzzy
|
||||
#| 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"
|
||||
@ -3765,14 +3806,50 @@ msgstr ""
|
||||
"Bitte bitten Sie den Benutzer, seine Note gutzuschreiben, bevor Sie diese "
|
||||
"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"
|
||||
msgstr ""
|
||||
|
||||
#: apps/wei/views.py:1315
|
||||
#: apps/wei/views.py:1379
|
||||
msgid "Attribute bus"
|
||||
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
|
||||
msgid "wrapped"
|
||||
msgstr ""
|
||||
@ -5769,6 +5846,31 @@ msgstr ""
|
||||
"müssen Ihre E-Mail-Adresse auch überprüfen, indem Sie dem Link folgen, den "
|
||||
"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
|
||||
#~| msgid "active"
|
||||
#~ msgid "is active"
|
||||
@ -5794,11 +5896,6 @@ msgstr ""
|
||||
#~ msgid "View details"
|
||||
#~ msgstr "Profile detail"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "created at"
|
||||
#~ msgid "Creation date"
|
||||
#~ msgstr "erschafft am"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "There is no results."
|
||||
#~ 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
@ -39,6 +39,7 @@ SECURE_HSTS_PRELOAD = True
|
||||
INSTALLED_APPS = [
|
||||
# External apps
|
||||
'bootstrap_datepicker_plus',
|
||||
'cas_server',
|
||||
'colorfield',
|
||||
'crispy_bootstrap4',
|
||||
'crispy_forms',
|
||||
@ -270,7 +271,7 @@ OAUTH2_PROVIDER = {
|
||||
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
|
||||
'OIDC_ENABLED': True,
|
||||
'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" },
|
||||
}
|
||||
|
||||
|
@ -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 %}">
|
||||
<i class="fa fa-user"></i> {% trans "My account" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url 'logout' %}">
|
||||
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
|
||||
</a>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="dropdown-item" type=submit">
|
||||
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
|
@ -1,20 +1,20 @@
|
||||
beautifulsoup4~=4.12.3
|
||||
crispy-bootstrap4~=2023.1
|
||||
Django~=4.2.9
|
||||
beautifulsoup4~=4.13.4
|
||||
crispy-bootstrap4~=2025.6
|
||||
Django~=5.2.4
|
||||
django-bootstrap-datepicker-plus~=5.0.5
|
||||
#django-cas-server~=2.0.0
|
||||
django-colorfield~=0.11.0
|
||||
django-crispy-forms~=2.1.0
|
||||
django-extensions>=3.2.3
|
||||
django-filter~=23.5
|
||||
django-cas-server~=3.1.0
|
||||
django-colorfield~=0.14.0
|
||||
django-crispy-forms~=2.4.0
|
||||
django-extensions>=4.1.0
|
||||
django-filter~=25.1
|
||||
#django-htcpcp-tea~=0.8.1
|
||||
django-mailer~=2.3.1
|
||||
django-oauth-toolkit~=2.3.0
|
||||
django-phonenumber-field~=7.3.0
|
||||
django-mailer~=2.3.2
|
||||
django-oauth-toolkit~=3.0.1
|
||||
django-phonenumber-field~=8.1.0
|
||||
django-polymorphic~=3.1.0
|
||||
djangorestframework~=3.14.0
|
||||
djangorestframework~=3.16.0
|
||||
django-rest-polymorphic~=0.1.10
|
||||
django-tables2~=2.7.0
|
||||
django-tables2~=2.7.5
|
||||
python-memcached~=1.62
|
||||
phonenumbers~=8.13.28
|
||||
Pillow>=10.2.0
|
||||
phonenumbers~=9.0.8
|
||||
Pillow>=11.3.0
|
||||
|
Reference in New Issue
Block a user