1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
84e9fea15f linters 2025-04-04 14:46:43 +02:00
b9ebb1718a Fixed some non timezone-aware displays 2025-04-04 00:29:22 +02:00
7ba5c76a89 Merge branch 'guests_schools' into 'main'
add school field to guest

See merge request bde/nk20!302
2025-03-25 18:59:46 +01:00
702ddb5679 add school field to guest 2025-03-25 17:39:31 +01:00
60355196ce Merge branch 'openid-connect' into 'main'
Openid connect

See merge request bde/nk20!293
2025-03-20 18:42:51 +01:00
9bffb32a5e documentation 2025-03-20 17:36:38 +01:00
6c63c6417c Typesetting 2025-03-08 16:08:40 +01:00
4563b2b640 Added configusation for OpenID support, along with installation information 2025-03-08 16:04:25 +01:00
14 changed files with 107 additions and 32 deletions

View File

@ -58,7 +58,13 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial (env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
``` ```
6. Enjoy : 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`).
7. Enjoy :
```bash ```bash
(env)$ ./manage.py runserver 0.0.0.0:8000 (env)$ ./manage.py runserver 0.0.0.0:8000
@ -228,7 +234,13 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
(env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
7. *Enjoy \o/* 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`).
8. *Enjoy \o/*
### Installation avec Docker ### Installation avec Docker

View File

@ -35,7 +35,7 @@ class GuestAdmin(admin.ModelAdmin):
""" """
Admin customisation for Guest Admin customisation for Guest
""" """
list_display = ('last_name', 'first_name', 'activity', 'inviter') list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter')
form = GuestForm form = GuestForm

View File

@ -51,9 +51,9 @@ class GuestViewSet(ReadProtectedModelViewSet):
queryset = Guest.objects.order_by('id') queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name', filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'school', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ] 'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name', search_fields = ['$activity__name', '$last_name', '$first_name', '$school', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ] '$inviter__alias__normalized_name', ]

View File

@ -107,7 +107,7 @@ class GuestForm(forms.ModelForm):
class Meta: class Meta:
model = Guest model = Guest
fields = ('last_name', 'first_name', 'inviter', ) fields = ('last_name', 'first_name', 'school', 'inviter', )
widgets = { widgets = {
"inviter": Autocomplete( "inviter": Autocomplete(
NoteUser, NoteUser,

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.20 on 2025-03-25 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("activity", "0005_alter_opener_options_alter_opener_opener"),
]
operations = [
migrations.AddField(
model_name="guest",
name="school",
field=models.CharField(default="", max_length=255, verbose_name="school"),
preserve_default=False,
),
]

View File

@ -201,7 +201,8 @@ class Entry(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) raise ValidationError(_("Already entered on ")
+ _("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(qs.get().time), ))
if self.guest: if self.guest:
self.note = self.guest.inviter self.note = self.guest.inviter
@ -247,6 +248,11 @@ class Guest(models.Model):
verbose_name=_("first name"), verbose_name=_("first name"),
) )
school = models.CharField(
max_length=255,
verbose_name=_("school"),
)
inviter = models.ForeignKey( inviter = models.ForeignKey(
NoteUser, NoteUser,
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -51,11 +51,11 @@ class GuestTable(tables.Table):
} }
model = Guest model = Guest
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ("last_name", "first_name", "inviter", ) fields = ("last_name", "first_name", "inviter", "school")
def render_entry(self, record): def render_entry(self, record):
if record.has_entry: if record.has_entry:
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(record.entry.time))))
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> ' return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize())) '{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))

View File

@ -50,6 +50,7 @@ class TestActivities(TestCase):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
def test_activity_list(self): def test_activity_list(self):
@ -156,6 +157,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -167,6 +169,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
@ -200,6 +203,7 @@ class TestActivityAPI(TestAPI):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
self.entry = Entry.objects.create( self.entry = Entry.objects.create(

View File

@ -168,6 +168,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
activity=activity, activity=activity,
first_name="", first_name="",
last_name="", last_name="",
school="",
inviter=self.request.user.note, inviter=self.request.user.note,
) )

View File

@ -55,6 +55,7 @@ Les adhérent⋅es ont la possibilité d'inviter des ami⋅es. Pour cela, les di
* Activité concernée (clé étrangère) * Activité concernée (clé étrangère)
* Nom de famille * Nom de famille
* Prénom * Prénom
* École
* Note de la personne ayant invité * Note de la personne ayant invité
Certaines contraintes s'appliquent : Certaines contraintes s'appliquent :

View File

@ -43,6 +43,11 @@ On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes', 'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator", 'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14), 'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
'PKCE_REQUIRED': False,
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
'SCOPES': { 'openid': "OpenID Connect scope" },
} }
Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``, Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``,
@ -57,6 +62,14 @@ On ajoute enfin les routes dans ``urls.py`` :
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')) path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
) )
Enfin pour utiliser OIDC, il faut générer une clé privé que l'on va, par défaut,
mettre dans `/var/secrets/oidc.key` :
.. code:: bash
cd /var/secrets/
openssl genrsa -out oidc.key 4096
L'OAuth2 est désormais prêt à être utilisé. L'OAuth2 est désormais prêt à être utilisé.

View File

@ -227,6 +227,22 @@ En production, ce fichier contient :
) )
Génération d'une clé privé pour OIDC
------------------------------------
Pour pouvoir proposer le service de connexion Openid Connect (OIDC) par OAuth2, il y a
besoin d'une clé privé. Par défaut, elle est cherché dans le fichier `/var/secrets/oidc.key`
(sinon, il faut modifier l'emplacement dans les fichiers de configurations).
Pour générer la clé, il faut aller dans le dossier `/var/secrets` (à créer, si nécessaire) puis
utiliser la commande de génération :
.. code:: bash
cd /var/secrets
openssl genrsa -out oidc.key 4096
Configuration des tâches récurrentes Configuration des tâches récurrentes
------------------------------------ ------------------------------------

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 21:08+0100\n" "POT-Creation-Date: 2025-03-25 11:16+0100\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n" "PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n" "Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -25,7 +25,7 @@ msgid "This opener already exists"
msgstr "Cette amitié existe déjà" msgstr "Cette amitié existe déjà"
#: apps/activity/apps.py:10 apps/activity/models.py:129 #: apps/activity/apps.py:10 apps/activity/models.py:129
#: apps/activity/models.py:169 apps/activity/models.py:323 #: apps/activity/models.py:169 apps/activity/models.py:328
msgid "activity" msgid "activity"
msgstr "activité" msgstr "activité"
@ -37,24 +37,24 @@ msgstr "La note du club est inactive."
msgid "The end date must be after the start date." msgid "The end date must be after the start date."
msgstr "La date de fin doit être après celle de début." msgstr "La date de fin doit être après celle de début."
#: apps/activity/forms.py:83 apps/activity/models.py:271 #: apps/activity/forms.py:83 apps/activity/models.py:276
msgid "You can't invite someone once the activity is started." msgid "You can't invite someone once the activity is started."
msgstr "" msgstr ""
"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré." "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
#: apps/activity/forms.py:86 apps/activity/models.py:274 #: apps/activity/forms.py:86 apps/activity/models.py:279
msgid "This activity is not validated yet." msgid "This activity is not validated yet."
msgstr "Cette activité n'est pas encore validée." msgstr "Cette activité n'est pas encore validée."
#: apps/activity/forms.py:96 apps/activity/models.py:282 #: apps/activity/forms.py:96 apps/activity/models.py:287
msgid "This person has been already invited 5 times this year." msgid "This person has been already invited 5 times this year."
msgstr "Cette personne a déjà été invitée 5 fois cette année." msgstr "Cette personne a déjà été invitée 5 fois cette année."
#: apps/activity/forms.py:100 apps/activity/models.py:286 #: apps/activity/forms.py:100 apps/activity/models.py:291
msgid "This person is already invited." msgid "This person is already invited."
msgstr "Cette personne est déjà invitée." msgstr "Cette personne est déjà invitée."
#: apps/activity/forms.py:104 apps/activity/models.py:290 #: apps/activity/forms.py:104 apps/activity/models.py:295
msgid "You can't invite more than 3 people to this activity." msgid "You can't invite more than 3 people to this activity."
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
@ -228,32 +228,36 @@ msgstr "nom de famille"
msgid "first name" msgid "first name"
msgstr "prénom" msgstr "prénom"
#: apps/activity/models.py:254 #: apps/activity/models.py:252
msgid "school"
msgstr "école"
#: apps/activity/models.py:259
msgid "inviter" msgid "inviter"
msgstr "hôte" msgstr "hôte"
#: apps/activity/models.py:258 #: apps/activity/models.py:263
msgid "guest" msgid "guest"
msgstr "invité·e" msgstr "invité·e"
#: apps/activity/models.py:259 #: apps/activity/models.py:264
msgid "guests" msgid "guests"
msgstr "invité·e·s" msgstr "invité·e·s"
#: apps/activity/models.py:312 #: apps/activity/models.py:317
msgid "Invitation" msgid "Invitation"
msgstr "Invitation" msgstr "Invitation"
#: apps/activity/models.py:330 apps/activity/models.py:334 #: apps/activity/models.py:335 apps/activity/models.py:339
msgid "Opener" msgid "Opener"
msgstr "Ouvreur⋅se" msgstr "Ouvreur⋅se"
#: apps/activity/models.py:335 #: apps/activity/models.py:340
#: apps/activity/templates/activity/activity_detail.html:16 #: apps/activity/templates/activity/activity_detail.html:16
msgid "Openers" msgid "Openers"
msgstr "Ouvreur⋅ses" msgstr "Ouvreur⋅ses"
#: apps/activity/models.py:339 #: apps/activity/models.py:344
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
#| msgid "Entry for {note} to the activity {activity}" #| msgid "Entry for {note} to the activity {activity}"
msgid "{opener} is opener of activity {acivity}" msgid "{opener} is opener of activity {acivity}"
@ -463,25 +467,25 @@ msgstr "Détails de l'activité"
msgid "Update activity" msgid "Update activity"
msgstr "Modifier l'activité" msgstr "Modifier l'activité"
#: apps/activity/views.py:177 #: apps/activity/views.py:178
msgid "Invite guest to the activity \"{}\"" msgid "Invite guest to the activity \"{}\""
msgstr "Invitation pour l'activité « {} »" msgstr "Invitation pour l'activité « {} »"
#: apps/activity/views.py:217 #: apps/activity/views.py:218
msgid "You are not allowed to display the entry interface for this activity." msgid "You are not allowed to display the entry interface for this activity."
msgstr "" msgstr ""
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette " "Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
"activité." "activité."
#: apps/activity/views.py:220 #: apps/activity/views.py:221
msgid "This activity does not support activity entries." msgid "This activity does not support activity entries."
msgstr "Cette activité ne requiert pas d'entrées." msgstr "Cette activité ne requiert pas d'entrées."
#: apps/activity/views.py:223 #: apps/activity/views.py:224
msgid "This activity is closed." msgid "This activity is closed."
msgstr "Cette activité est fermée." msgstr "Cette activité est fermée."
#: apps/activity/views.py:328 #: apps/activity/views.py:329
msgid "Entry for activity \"{}\"" msgid "Entry for activity \"{}\""
msgstr "Entrées pour l'activité « {} »" msgstr "Entrées pour l'activité « {} »"
@ -1989,10 +1993,6 @@ msgstr "Historique des transactions récentes"
#: apps/note/templates/note/mails/weekly_report.txt:32 #: apps/note/templates/note/mails/weekly_report.txt:32
#: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.html:40
#: apps/registration/templates/registration/mails/email_validation_email.txt:16 #: apps/registration/templates/registration/mails/email_validation_email.txt:16
#: apps/scripts/templates/scripts/horaires.html:35
#: apps/scripts/templates/scripts/horaires.txt:17
#: apps/scripts/templates/scripts/intro_mail.html:49
#: apps/scripts/templates/scripts/intro_mail.txt:25
msgid "Mail generated by the Note Kfet on the" msgid "Mail generated by the Note Kfet on the"
msgstr "Mail généré par la Note Kfet le" msgstr "Mail généré par la Note Kfet le"

View File

@ -268,6 +268,10 @@ OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator", 'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14), 'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
'SCOPES': { 'openid': "OpenID Connect scope" },
} }
# Take control on how widget templates are sourced # Take control on how widget templates are sourced