1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-04 14:52:13 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
6c7d86185a Models 2025-07-03 14:34:04 +02:00
22 changed files with 240 additions and 122 deletions

View File

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

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**
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
`OIDC_RSA_PRIVATE_KEY`.
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`).
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 le champ
`OIDC_RSA_PRIVATE_KEY` dans le .env (par défaut `/var/secrets/oidc.key`).
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`).
8. *Enjoy \o/*

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

3
apps/family/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

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

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

@ -0,0 +1,165 @@
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:
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
)
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, )
@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)
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()
@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()

3
apps/family/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -63,8 +63,7 @@ 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})
| Q(**{f'owner__name{suffix}': prefix + pattern}))
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern}))
else:
qs = qs.none()
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)
def test_logout(self):
response = self.client.post(reverse("logout"))
response = self.client.get(reverse("logout"))
self.assertEqual(response.status_code, 200)
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 + '/alias', AliasViewSet)
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/transaction', TransactionViewSet)

View File

@ -1,10 +1,8 @@
# 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
@ -19,46 +17,25 @@ class PermissionScopes(BaseScopes):
"""
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()}
scopes['openid'] = "OpenID Connect"
return scopes
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
return []
scopes = [f"{p.id}_{p.membership.club.id}"
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.append('openid')
return scopes
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
if not application:
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')]
scopes.append('openid')
return scopes
class PermissionOAuth2Validator(OAuth2Validator):
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"]
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
@ -77,8 +54,6 @@ class PermissionOAuth2Validator(OAuth2Validator):
if scope in scopes:
valid_scopes.add(scope)
if 'openid' in scopes:
valid_scopes.add('openid')
request.scopes = valid_scopes
return valid_scopes

View File

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

View File

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

View File

@ -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, RadioSelect
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, NoteUser
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
@ -140,19 +140,6 @@ 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 \
@ -164,8 +151,21 @@ class WEIMembershipForm(forms.ModelForm):
model = WEIMembership
fields = ('roles', 'bus', 'team',)
widgets = {
"bus": RadioSelect(),
"team": RadioSelect(),
"bus": Autocomplete(
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>
$(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 %}

View File

@ -788,8 +788,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
return form
def get_membership_form(self, data=None, instance=None):
registration = self.get_object()
membership_form = WEIMembershipForm(data if data else None, instance=instance, wei=registration.wei)
membership_form = WEIMembershipForm(data if data else None, instance=instance)
del membership_form.fields["credit_type"]
del membership_form.fields["credit_amount"]
del membership_form.fields["first_name"]
@ -970,13 +969,6 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
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"])

View File

@ -136,7 +136,7 @@ de diffusion utiles.
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``,
``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
laquelle des ``n+1`` dernières années.
* ``--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.
Le script sort sur la sortie standard la liste des adresses mails à inscrire.
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.

View File

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

View File

@ -138,12 +138,9 @@ 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>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button class="dropdown-item" type=submit">
<a class="dropdown-item" href="{% url 'logout' %}">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</button>
</form>
</a>
</div>
</li>
{% else %}

View File

@ -1,20 +1,20 @@
beautifulsoup4~=4.13.4
crispy-bootstrap4~=2025.6
Django~=5.2.4
beautifulsoup4~=4.12.3
crispy-bootstrap4~=2023.1
Django~=4.2.9
django-bootstrap-datepicker-plus~=5.0.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-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-htcpcp-tea~=0.8.1
django-mailer~=2.3.2
django-oauth-toolkit~=3.0.1
django-phonenumber-field~=8.1.0
django-mailer~=2.3.1
django-oauth-toolkit~=2.3.0
django-phonenumber-field~=7.3.0
django-polymorphic~=3.1.0
djangorestframework~=3.16.0
djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10
django-tables2~=2.7.5
django-tables2~=2.7.0
python-memcached~=1.62
phonenumbers~=9.0.8
Pillow>=11.3.0
phonenumbers~=8.13.28
Pillow>=10.2.0