1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-06 15:24:01 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
f6ad6197de ListViews et templates 2025-07-05 19:47:35 +02:00
6c7d86185a Models 2025-07-03 14:34:04 +02:00
28 changed files with 545 additions and 131 deletions

View File

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

View File

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

View File

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

0
apps/family/__init__.py Normal file
View File

11
apps/family/apps.py Normal file
View File

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

View File

@ -0,0 +1,85 @@
# Generated by Django 4.2.21 on 2025-07-04 19:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChallengeCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
],
options={
'verbose_name': 'challenge category',
'verbose_name_plural': 'challenge categories',
},
),
migrations.CreateModel(
name='Family',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('score', models.PositiveIntegerField(verbose_name='score')),
('rank', models.PositiveIntegerField(verbose_name='rank')),
],
options={
'verbose_name': 'Family',
'verbose_name_plural': 'Families',
},
),
migrations.CreateModel(
name='Challenge',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('description', models.CharField(max_length=255, verbose_name='description')),
('points', models.PositiveIntegerField(verbose_name='points')),
('obtained', models.PositiveIntegerField(verbose_name='obtained')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challengecategory', verbose_name='category')),
],
options={
'verbose_name': 'challenge',
'verbose_name_plural': 'challenges',
},
),
migrations.CreateModel(
name='Achievement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('obtained_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='obtained at')),
('challenge', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challenge')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.family', verbose_name='family')),
],
options={
'verbose_name': 'achievement',
'verbose_name_plural': 'achievements',
},
),
migrations.CreateModel(
name='FamilyMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField(default=2025, verbose_name='year')),
('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='members', to='family.family', verbose_name='family')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='family_memberships', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'family membership',
'verbose_name_plural': 'family memberships',
'unique_together': {('user', 'year')},
},
),
]

View File

202
apps/family/models.py Normal file
View File

@ -0,0 +1,202 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models, transaction
from django.utils import timezone
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
class Family(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
unique=True
)
description = models.CharField(
max_length=255,
verbose_name=_('description')
)
score = models.PositiveIntegerField(
verbose_name=_('score')
)
rank = models.PositiveIntegerField(
verbose_name=_('rank'),
)
class Meta:
verbose_name = _('Family')
verbose_name_plural = _('Families')
def __str__(self):
return self.name
class FamilyMembership(models.Model):
user = models.OneToOneField(
User,
on_delete=models.PROTECT,
related_name=_('family_memberships'),
verbose_name=_('user'),
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
related_name=_('members'),
verbose_name=_('family'),
)
year = models.PositiveIntegerField(
verbose_name=_('year'),
default=timezone.now().year,
)
class Meta:
unique_together = ('user', 'year',)
verbose_name = _('family membership')
verbose_name_plural = _('family memberships')
def __str__(self):
return _('Family membership of {user} to {family}').format(user=self.user.username, family=self.family.name, )
class ChallengeCategory(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
unique=True,
)
class Meta:
verbose_name = _('challenge category')
verbose_name_plural = _('challenge categories')
def __str__(self):
return self.name
class Challenge(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_('name'),
)
description = models.CharField(
max_length=255,
verbose_name=_('description'),
)
points = models.PositiveIntegerField(
verbose_name=_('points'),
)
category = models.ForeignKey(
ChallengeCategory,
verbose_name=_('category'),
on_delete=models.PROTECT
)
obtained = models.PositiveIntegerField(
verbose_name=_('obtained')
)
class Meta:
verbose_name = _('challenge')
verbose_name_plural = _('challenges')
def __str__(self):
return self.name
class Achievement(models.Model):
challenge = models.ForeignKey(
Challenge,
on_delete=models.PROTECT,
)
family = models.ForeignKey(
Family,
on_delete=models.PROTECT,
verbose_name=_('family'),
)
obtained_at = models.DateTimeField(
verbose_name=_('obtained at'),
default=timezone.now,
)
class Meta:
verbose_name = _('achievement')
verbose_name_plural = _('achievements')
def __str__(self):
return _('Challenge {challenge} carried out by Family {family}').format(challenge=self.challenge.name, family=self.family.name, )
@classmethod
def update_ranking(cls, *args, **kwargs):
"""
Update ranking when adding or removing points
"""
family_set = cls.objects.select_for_update().all().order_by("-score")
for i in range(family_set.count()):
if i == 0 or family_set[i].score != family_set[i - 1].score:
new_rank = i + 1
family = family_set[i]
family.rank = new_rank
family._force_save = True
family.save()
@transaction.atomic
def save(self, *args, **kwargs):
"""
When saving, also grants points to the family
"""
self.family = Family.objects.select_for_update().get(pk=self.family_id)
self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id)
challenge_points = self.challenge.points
is_new = self.pk is None
super.save(*args, **kwargs)
# Only grant points when getting a new achievement
if is_new:
self.family.refresh_from_db()
self.family.score += challenge_points
self.family._force_save = True
self.family.save()
self.challenge.refresh_from_db()
self.challenge.obtained += 1
self.challenge._force_save = True
self.challenge.save()
self.__class__.update_ranking()
@transaction.atomic
def delete(self, *args, **kwargs):
"""
When deleting, also removes points from the family
"""
# Get the family and challenge before deletion
self.family = Family.objects.select_for_update().get(pk=self.family_id)
challenge_points = self.challenge.points
# Delete the achievement
super().delete(*args, **kwargs)
# Remove points from the family
self.family.refresh_from_db()
self.family.score -= challenge_points
self.family._force_save = True
self.family.save()
self.challenge.refresh_from_db()
self.challenge.obtained -= 1
self.challenge._force_save = True
self.challenge.save()
self.__class__.update_ranking()

40
apps/family/tables.py Normal file
View File

@ -0,0 +1,40 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django_tables2 import A
from .models import Family, Challenge
class FamilyTable(tables.Table):
"""
List all families
"""
name = tables.LinkColumn(
"family:family_detail",
args=[A("pk")],
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Family
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'score', 'rank',)
order_by = ('rank',)
class ChallengeTable(tables.Table):
"""
List all challenges
"""
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('id',)
model = Challenge
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'points', 'category',)

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="{% url "family:family_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Families" %}
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Challenges" %}
</a>
</div>
</div>
</div>
<<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Families" %}
</a>
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Challenges" %}
</a>
</div>
</div>
</div>
<<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
{% render_table table %}
</div>
{% endblock %}

12
apps/family/urls.py Normal file
View File

@ -0,0 +1,12 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import FamilyListView, ChallengeListView
app_name = 'family'
urlpatterns = [
path('list/', FamilyListView.as_view(), name="family_list"),
path('challenge/list/', ChallengeListView.as_view(), name="challenge_list"),
]

63
apps/family/views.py Normal file
View File

@ -0,0 +1,63 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView, UpdateView
from django.utils.translation import gettext_lazy as _
from django_tables2 import SingleTableView
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .models import Family, Challenge
from .tables import FamilyTable, ChallengeTable
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create family
"""
model = Family
extra_context = {"title": _('Create family')}
def get_sample_object(self):
return Family(
name="",
description="Sample family",
score=0,
rank=0,
)
class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing Families
"""
model = Family
table_class = FamilyTable
extra_context = {"title": _('Families list')}
class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Display details of a family
"""
model = Family
context_object_name = "family"
extra_context = {"title": _('Family detail')}
class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the information of a family.
"""
model = Family
context_object_name = "family"
extra_context = {"title": _('Update family')}
class ChallengeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List all challenges
"""
model = Challenge
table_class = ChallengeTable
extra_context = {"title": _('Challenges list')}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.forms import CheckboxSelectMultiple, RadioSelect from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -140,19 +140,6 @@ class WEIMembershipForm(forms.ModelForm):
required=False, required=False,
) )
def __init__(self, *args, wei=None, **kwargs):
super().__init__(*args, **kwargs)
if 'bus' in self.fields:
if wei is not None:
self.fields['bus'].queryset = Bus.objects.filter(wei=wei)
else:
self.fields['bus'].queryset = Bus.objects.none()
if 'team' in self.fields:
if wei is not None:
self.fields['team'].queryset = BusTeam.objects.filter(bus__wei=wei)
else:
self.fields['team'].queryset = BusTeam.objects.none()
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if 'team' in cleaned_data and cleaned_data["team"] is not None \ if 'team' in cleaned_data and cleaned_data["team"] is not None \
@ -164,8 +151,21 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership model = WEIMembership
fields = ('roles', 'bus', 'team',) fields = ('roles', 'bus', 'team',)
widgets = { widgets = {
"bus": RadioSelect(), "bus": Autocomplete(
"team": RadioSelect(), Bus,
attrs={
'api_url': '/api/wei/bus/',
'placeholder': 'Bus ...',
}
),
"team": Autocomplete(
BusTeam,
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
},
resetable=True,
),
} }

View File

@ -210,27 +210,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
} }
} }
</script> </script>
<script>
$(document).ready(function () {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function (ignored) {
buses.push($(this).parent().text().trim());
});
console.log(buses);
$("input[name='team']").each(function () {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function (bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endblock %} {% endblock %}

View File

@ -788,8 +788,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
return form return form
def get_membership_form(self, data=None, instance=None): def get_membership_form(self, data=None, instance=None):
registration = self.get_object() membership_form = WEIMembershipForm(data if data else None, instance=instance)
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
del membership_form.fields["credit_type"] del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"] del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"] del membership_form.fields["first_name"]
@ -970,13 +969,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
return WEIMembership1AForm return WEIMembership1AForm
return WEIMembershipForm return WEIMembershipForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])
wei = registration.wei
kwargs['wei'] = wei
return kwargs
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) registration = WEIRegistration.objects.get(pk=self.kwargs["pk"])

View File

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

View File

@ -39,7 +39,6 @@ SECURE_HSTS_PRELOAD = True
INSTALLED_APPS = [ INSTALLED_APPS = [
# External apps # External apps
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'cas_server',
'colorfield', 'colorfield',
'crispy_bootstrap4', 'crispy_bootstrap4',
'crispy_forms', 'crispy_forms',
@ -71,6 +70,7 @@ INSTALLED_APPS = [
# Note apps # Note apps
'api', 'api',
'activity', 'activity',
'family',
'food', 'food',
'logs', 'logs',
'member', 'member',

View File

@ -78,6 +78,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated %}
<li class="nav-item">
{% url 'family:family_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Families' %}</a>
</li>
{% endif %}
{% if "auth.user"|model_list_length >= 2 %} {% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item"> <li class="nav-item">
{% url 'member:user_list' as url %} {% url 'member:user_list' as url %}
@ -138,12 +145,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
<i class="fa fa-user"></i> {% trans "My account" %} <i class="fa fa-user"></i> {% trans "My account" %}
</a> </a>
<form method="post" action="{% url 'logout' %}"> <a class="dropdown-item" href="{% url 'logout' %}">
{% csrf_token %} <i class="fa fa-sign-out"></i> {% trans "Log out" %}
<button class="dropdown-item" type=submit"> </a>
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</button>
</form>
</div> </div>
</li> </li>
{% else %} {% else %}

View File

@ -21,8 +21,9 @@ urlpatterns = [
path('activity/', include('activity.urls')), path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')), path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')), path('wei/', include('wei.urls')),
path('food/',include('food.urls')), path('food/', include('food.urls')),
path('wrapped/',include('wrapped.urls')), path('wrapped/', include('wrapped.urls')),
path('family/', include('family.urls')),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),

View File

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

View File

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