From 462a9e0f2d6f9693917992378370444e74206685 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 9 Mar 2020 22:13:11 +0100 Subject: [PATCH 01/29] Fix CAS settings --- note_kfet/settings/__init__.py | 5 +++++ note_kfet/settings/base.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 6d871599..c1df7477 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -1,4 +1,9 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.utils.translation import gettext_lazy as _ import re + from .base import * diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 4fe12fbf..3a5c3200 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -39,6 +39,8 @@ INSTALLED_APPS = [ 'polymorphic', 'crispy_forms', 'django_tables2', + 'cas_server', + 'cas', # Django contrib 'django.contrib.admin', 'django.contrib.admindocs', From 83b1ad47515b6fa9ce875709b4813bbe92d823ce Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 9 Mar 2020 22:23:47 +0100 Subject: [PATCH 02/29] Multi-connexions --- note_kfet/urls.py | 4 ++-- templates/registration/login.html | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 896c0655..407659f8 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -37,8 +37,8 @@ if "cas" in settings.INSTALLED_APPS: from cas import views as cas_views urlpatterns += [ # Include CAS Client routers - path('accounts/login/', cas_views.login, name='login'), - path('accounts/logout/', cas_views.logout, name='logout'), + path('accounts/login/cas/', cas_views.login, name='cas_login'), + path('accounts/logout/cas/', cas_views.logout, name='cas_logout'), ] if "debug_toolbar" in settings.INSTALLED_APPS: diff --git a/templates/registration/login.html b/templates/registration/login.html index 04ef8d7d..5a4322d1 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -17,6 +17,10 @@ SPDX-License-Identifier: GPL-2.0-or-later

{% endif %} +
+ Vous pouvez aussi vous connecter via l'authentification centralisée en suivant ce lien. +
+
{% csrf_token %} {{ form | crispy }} From fffd674c44c034361ede6f010f8ea3c16365befd Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 9 Mar 2020 22:29:14 +0100 Subject: [PATCH 03/29] Fix initial fixture hardcode --- apps/note/fixtures/initial.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index f80332c0..01242b22 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -3,7 +3,7 @@ "model": "note.note", "pk": 1, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 37, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 37, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 37, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 37, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 38, + "polymorphic_ctype": 36, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 38, + "polymorphic_ctype": 36, "balance": 0, "is_active": true, "display_image": "", From 20e2d41563a1f38c4c36b82a434080b3666d5148 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Mar 2020 00:01:40 +0100 Subject: [PATCH 04/29] Use a middleware rather than inspect the stack to get current user and IP --- apps/logs/middlewares.py | 55 ++++++++++++++++++++++++++++++++++ apps/logs/signals.py | 45 ++++------------------------ note_kfet/settings/__init__.py | 6 +++- 3 files changed, 65 insertions(+), 41 deletions(-) create mode 100644 apps/logs/middlewares.py diff --git a/apps/logs/middlewares.py b/apps/logs/middlewares.py new file mode 100644 index 00000000..69bbef92 --- /dev/null +++ b/apps/logs/middlewares.py @@ -0,0 +1,55 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from threading import local + + +USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') +IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') + +_thread_locals = local() + + +def _set_current_user_and_ip(user=None, ip=None): + """ + Sets current user in local thread. + Can be used as a hook e.g. for shell jobs (when request object is not + available). + """ + setattr(_thread_locals, USER_ATTR_NAME, user) + setattr(_thread_locals, IP_ATTR_NAME, ip) + + +def get_current_user(): + return getattr(_thread_locals, USER_ATTR_NAME, None) + + +def get_current_ip(): + return getattr(_thread_locals, IP_ATTR_NAME, None) + + +def get_current_authenticated_user(): + current_user = get_current_user() + if isinstance(current_user, AnonymousUser): + return None + return current_user + + +class LogsMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + user = request.user + if 'HTTP_X_FORWARDED_FOR' in request.META: + ip = request.META.get('HTTP_X_FORWARDED_FOR') + else: + ip = request.META.get('REMOTE_ADDR') + + _set_current_user_and_ip(user, ip) + + response = self.get_response(request) + + return response diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 415e7c1c..e4e47e18 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -1,48 +1,15 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -import inspect - from django.contrib.contenttypes.models import ContentType from django.core import serializers from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver +from .middlewares import get_current_authenticated_user, get_current_ip from .models import Changelog -def get_request_in_signal(sender): - req = None - for entry in reversed(inspect.stack()): - try: - req = entry[0].f_locals['request'] - # Check if there is a user - # noinspection PyStatementEffect - req.user - break - except: - pass - - if not req: - print("WARNING: Attempt to save " + str(sender) + " with no user") - - return req - - -def get_user_and_ip(sender): - req = get_request_in_signal(sender) - try: - user = req.user - if 'HTTP_X_FORWARDED_FOR' in req.META: - ip = req.META.get('HTTP_X_FORWARDED_FOR') - else: - ip = req.META.get('REMOTE_ADDR') - except: - user = None - ip = None - return user, ip - - EXCLUDED = [ 'admin.logentry', 'authtoken.token', @@ -75,13 +42,11 @@ def save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + print("LOGGING SOMETHING") + previous = instance._previous - user, ip = get_user_and_ip(sender) - - from django.contrib.auth.models import AnonymousUser - if isinstance(user, AnonymousUser): - user = None + user, ip = get_current_authenticated_user(), get_current_ip() if user is not None and instance._meta.label_lower == "auth.user" and previous: # Don't save last login modifications @@ -111,7 +76,7 @@ def delete_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return - user, ip = get_user_and_ip(sender) + user, ip = get_current_authenticated_user(), get_current_ip() instance_json = serializers.serialize('json', [instance, ])[1:-1] Changelog.objects.create(user=user, diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index c1df7477..b370a430 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -73,7 +73,11 @@ if "cas" in INSTALLED_APPS: 'cas_explained', ] AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',) - + + +if "logs" in INSTALLED_APPS: + MIDDLEWARE += ('logs.middlewares.LogsMiddleware',) + if "debug_toolbar" in INSTALLED_APPS: MIDDLEWARE.insert(1,"debug_toolbar.middleware.DebugToolbarMiddleware") INTERNAL_IPS = [ '127.0.0.1'] From db69091d9fe8dc7f2b229462fec3eb4cc3fddcc3 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Mar 2020 01:04:00 +0100 Subject: [PATCH 05/29] When an update is made from the shell, the username of the user connected to the shell is queried --- apps/logs/middlewares.py | 11 +++++------ apps/logs/signals.py | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/apps/logs/middlewares.py b/apps/logs/middlewares.py index 69bbef92..cc6e1f4c 100644 --- a/apps/logs/middlewares.py +++ b/apps/logs/middlewares.py @@ -13,11 +13,6 @@ _thread_locals = local() def _set_current_user_and_ip(user=None, ip=None): - """ - Sets current user in local thread. - Can be used as a hook e.g. for shell jobs (when request object is not - available). - """ setattr(_thread_locals, USER_ATTR_NAME, user) setattr(_thread_locals, IP_ATTR_NAME, ip) @@ -38,6 +33,10 @@ def get_current_authenticated_user(): class LogsMiddleware(object): + """ + Ce middleware permet de récupérer l'utilisateur actif ainsi que son adresse IP à chaque connexion. + """ + def __init__(self, get_response): self.get_response = get_response @@ -49,7 +48,7 @@ class LogsMiddleware(object): ip = request.META.get('REMOTE_ADDR') _set_current_user_and_ip(user, ip) - response = self.get_response(request) + _set_current_user_and_ip(None, None) return response diff --git a/apps/logs/signals.py b/apps/logs/signals.py index e4e47e18..85f0c585 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -5,11 +5,14 @@ from django.contrib.contenttypes.models import ContentType from django.core import serializers from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver +import getpass +from note.models import NoteUser, Alias from .middlewares import get_current_authenticated_user, get_current_ip from .models import Changelog +# Ces modèles ne nécessitent pas de logs EXCLUDED = [ 'admin.logentry', 'authtoken.token', @@ -22,13 +25,15 @@ EXCLUDED = [ 'note.noteclub', 'note.notespecial', 'sessions.session', - 'reversion.revision', - 'reversion.version', ] @receiver(pre_save) def pre_save_object(sender, instance, **kwargs): + """ + Avant la sauvegarde d'un modèle, on récupère l'ancienne instance actuellement en base de données + que l'on garde en mémoire + """ qs = sender.objects.filter(pk=instance.pk).all() if qs.exists(): instance._previous = qs.get() @@ -38,26 +43,43 @@ def pre_save_object(sender, instance, **kwargs): @receiver(post_save) def save_object(sender, instance, **kwargs): + """ + Dès qu'un modèle est sauvegardé, une entrée dans la table `Changelog` est ajouté dans la base de données + afin de répertorier chaque modification effectuée + """ # noinspection PyProtectedMember if instance._meta.label_lower in EXCLUDED: return - print("LOGGING SOMETHING") - + # noinspection PyProtectedMember previous = instance._previous + # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP user, ip = get_current_authenticated_user(), get_current_ip() + if user is None: + # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` + # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée + # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info + ip = "127.0.0.1" + username = Alias.normalize(getpass.getuser()) + note = NoteUser.objects.filter(alias__normalized_name__regex="^" + username + "$") + if not note.exists(): + print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username) + else: + user = note.get().user + if user is not None and instance._meta.label_lower == "auth.user" and previous: - # Don't save last login modifications + # On n'enregistre pas les connexions if instance.last_login != previous.last_login: return + # Les modèles sont sauvegardés au format JSON previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None instance_json = serializers.serialize('json', [instance, ])[1:-1] if previous_json == instance_json: - # No modification + # Pas de log s'il n'y a pas de modification return Changelog.objects.create(user=user, @@ -72,10 +94,14 @@ def save_object(sender, instance, **kwargs): @receiver(post_delete) def delete_object(sender, instance, **kwargs): + """ + Dès qu'un modèle est supprimé, une entrée dans la table `Changelog` est ajouté dans la base de données + """ # noinspection PyProtectedMember if instance._meta.label_lower in EXCLUDED: return + # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP user, ip = get_current_authenticated_user(), get_current_ip() instance_json = serializers.serialize('json', [instance, ])[1:-1] From 185f6ce4e31c2bbd090a8d53071799e95e0241be Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Mar 2020 01:07:37 +0100 Subject: [PATCH 06/29] Alias have to be exact --- apps/logs/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 85f0c585..89354210 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -63,7 +63,7 @@ def save_object(sender, instance, **kwargs): # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info ip = "127.0.0.1" username = Alias.normalize(getpass.getuser()) - note = NoteUser.objects.filter(alias__normalized_name__regex="^" + username + "$") + note = NoteUser.objects.filter(alias__normalized_name="^" + username + "$") if not note.exists(): print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username) else: From aa731d1ae4a3ed68b22af39b147c74370ecca8aa Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Mar 2020 17:16:03 +0100 Subject: [PATCH 07/29] Regexp must begin with `^` --- apps/member/views.py | 2 +- apps/note/api/views.py | 6 +++--- apps/note/views.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/member/views.py b/apps/member/views.py index 21c8de5f..82c15b99 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -300,7 +300,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView): qs = User.objects.all() if self.q: - qs = qs.filter(username__regex=self.q) + qs = qs.filter(username__regex="^" + self.q) return qs diff --git a/apps/note/api/views.py b/apps/note/api/views.py index cf0136f2..abcf915b 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -69,8 +69,8 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( - Q(alias__name__regex=alias) - | Q(alias__normalized_name__regex=alias.lower())) + Q(alias__name__regex="^" + alias) + | Q(alias__normalized_name__regex="^" + alias.lower())) note_type = self.request.query_params.get("type", None) if note_type: @@ -107,7 +107,7 @@ class AliasViewSet(viewsets.ModelViewSet): alias = self.request.query_params.get("alias", ".*") queryset = queryset.filter( - Q(name__regex=alias) | Q(normalized_name__regex=alias.lower())) + Q(name__regex="^" + alias) | Q(normalized_name__regex="^" + alias.lower())) note_id = self.request.query_params.get("note", None) if note_id: diff --git a/apps/note/views.py b/apps/note/views.py index 09846057..ccba8c9f 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -67,7 +67,7 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): # self.q est le paramètre de la recherche if self.q: - qs = qs.filter(Q(name__regex=self.q) | Q(normalized_name__regex=Alias.normalize(self.q))) \ + qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \ .order_by('normalized_name').distinct() # Filtrage par type de note (user, club, special) From 6baf437eefedfe3fd7214077577d1885f628a795 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 10 Mar 2020 23:37:54 +0100 Subject: [PATCH 08/29] Fix Dockerfile --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a2f45b00..d42bdd1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,13 +9,13 @@ RUN apt update && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ rm -rf /var/lib/apt/lists/* -COPY requirements.txt /code/ +COPY . /code/ + +# Comment what is not needed RUN pip install -r requirements/base.txt RUN pip install -r requirements/api.txt RUN pip install -r requirements/cas.txt RUN pip install -r requirements/production.txt -COPY . /code/ - ENTRYPOINT ["/code/entrypoint.sh"] EXPOSE 8000 From be877276f8a6b2ca554a46c7d0e28ecda7a51490 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 00:41:37 +0100 Subject: [PATCH 09/29] Fix JSON serialization --- apps/logs/signals.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 89354210..1cd74067 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -2,9 +2,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib.contenttypes.models import ContentType -from django.core import serializers from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver +from rest_framework.renderers import JSONRenderer +from rest_framework.serializers import ModelSerializer import getpass from note.models import NoteUser, Alias @@ -19,11 +20,10 @@ EXCLUDED = [ 'cas_server.user', 'cas_server.userattributes', 'contenttypes.contenttype', - 'logs.changelog', + 'logs.changelog', # Never remove this line 'migrations.migration', - 'note.noteuser', - 'note.noteclub', - 'note.notespecial', + 'note.note' # We only store the subclasses + 'note.transaction', 'sessions.session', ] @@ -63,7 +63,7 @@ def save_object(sender, instance, **kwargs): # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info ip = "127.0.0.1" username = Alias.normalize(getpass.getuser()) - note = NoteUser.objects.filter(alias__normalized_name="^" + username + "$") + note = NoteUser.objects.filter(alias__normalized_name=username) if not note.exists(): print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username) else: @@ -74,9 +74,14 @@ def save_object(sender, instance, **kwargs): if instance.last_login != previous.last_login: return - # Les modèles sont sauvegardés au format JSON - previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None - instance_json = serializers.serialize('json', [instance, ])[1:-1] + # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles + class CustomSerializer(ModelSerializer): + class Meta: + model = instance.__class__ + fields = '__all__' + + previous_json = JSONRenderer().render(CustomSerializer(previous).data) + instance_json = JSONRenderer().render(CustomSerializer(instance).data) if previous_json == instance_json: # Pas de log s'il n'y a pas de modification @@ -104,7 +109,14 @@ def delete_object(sender, instance, **kwargs): # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP user, ip = get_current_authenticated_user(), get_current_ip() - instance_json = serializers.serialize('json', [instance, ])[1:-1] + # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles + class CustomSerializer(ModelSerializer): + class Meta: + model = instance.__class__ + fields = '__all__' + + instance_json = JSONRenderer().render(CustomSerializer(instance).data) + Changelog.objects.create(user=user, ip=ip, model=ContentType.objects.get_for_model(instance), From 0f1d662d251fdb919d744358c4cc15c0fadf453e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 00:47:43 +0100 Subject: [PATCH 10/29] Anonymous users don't have any access to the API (fix it later with rights...), fix CI --- apps/note/tables.py | 2 +- note_kfet/settings/__init__.py | 4 ++-- note_kfet/settings/base.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/note/tables.py b/apps/note/tables.py index e85fcbae..d26ffedc 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -6,7 +6,7 @@ from django.db.models import F from django_tables2.utils import A from .models.notes import Alias -from .models.transactions import Transaction, TransactionTemplate +from .models.transactions import Transaction from .templatetags.pretty_money import pretty_money diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index b370a430..ce31b39f 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -79,5 +79,5 @@ if "logs" in INSTALLED_APPS: MIDDLEWARE += ('logs.middlewares.LogsMiddleware',) if "debug_toolbar" in INSTALLED_APPS: - MIDDLEWARE.insert(1,"debug_toolbar.middleware.DebugToolbarMiddleware") - INTERNAL_IPS = [ '127.0.0.1'] + MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") + INTERNAL_IPS = ['127.0.0.1'] diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 3a5c3200..737350e2 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -137,7 +137,7 @@ REST_FRAMEWORK = { # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ # TODO Maybe replace it with our custom permissions system - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + 'rest_framework.permissions.DjangoModelPermissions' ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', From 38ad8709390128af6fb0b657ab05588ecbc3262b Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 01:03:15 +0100 Subject: [PATCH 11/29] Add one missing model to the API --- apps/note/api/serializers.py | 13 ++++++++++++- apps/note/api/urls.py | 3 ++- apps/note/api/views.py | 14 ++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 1696bfee..61257ec4 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory class NoteSerializer(serializers.ModelSerializer): @@ -78,6 +78,17 @@ class NotePolymorphicSerializer(PolymorphicSerializer): } +class TemplateCategorySerializer(serializers.ModelSerializer): + """ + REST API Serializer for Transaction templates. + The djangorestframework plugin will analyse the model `TemplateCategory` and parse all fields in the API. + """ + + class Meta: + model = TemplateCategory + fields = '__all__' + + class TransactionTemplateSerializer(serializers.ModelSerializer): """ REST API Serializer for Transaction templates. diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 54218796..5e176ec5 100644 --- a/apps/note/api/urls.py +++ b/apps/note/api/urls.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from .views import NotePolymorphicViewSet, AliasViewSet, \ - TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet + TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet def register_note_urls(router, path): @@ -12,6 +12,7 @@ def register_note_urls(router, path): router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/alias', AliasViewSet) + router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/template', TransactionTemplateViewSet) router.register(path + '/transaction/membership', MembershipTransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index abcf915b..4fbb9481 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -6,9 +6,9 @@ from rest_framework import viewsets from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ NoteUserSerializer, AliasSerializer, \ - TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer + TemplateCategorySerializer, TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory class NoteViewSet(viewsets.ModelViewSet): @@ -131,6 +131,16 @@ class AliasViewSet(viewsets.ModelViewSet): return queryset +class TemplateCategoryViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer, + then render it on /api/note/transaction/category/ + """ + queryset = TemplateCategory.objects.all() + serializer_class = TemplateCategorySerializer + + class TransactionTemplateViewSet(viewsets.ModelViewSet): """ REST API View set. From bc97eb1eb4bb3943ad60bf3569c05255386953d0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 10:08:28 +0100 Subject: [PATCH 12/29] Add logs to the API (this right should only be given to superusers) --- apps/api/urls.py | 2 ++ apps/logs/api/__init__.py | 0 apps/logs/api/serializers.py | 17 +++++++++++++++++ apps/logs/api/urls.py | 11 +++++++++++ apps/logs/api/views.py | 17 +++++++++++++++++ apps/logs/models.py | 6 ++++++ apps/logs/signals.py | 7 ++++--- note_kfet/settings/base.py | 1 + 8 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 apps/logs/api/__init__.py create mode 100644 apps/logs/api/serializers.py create mode 100644 apps/logs/api/urls.py create mode 100644 apps/logs/api/views.py diff --git a/apps/api/urls.py b/apps/api/urls.py index c1b6bf48..bb1fdce2 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -7,6 +7,7 @@ from rest_framework import routers, serializers, viewsets from activity.api.urls import register_activity_urls from member.api.urls import register_members_urls from note.api.urls import register_note_urls +from logs.api.urls import register_logs_urls class UserSerializer(serializers.ModelSerializer): @@ -41,6 +42,7 @@ router.register('user', UserViewSet) register_members_urls(router, 'members') register_activity_urls(router, 'activity') register_note_urls(router, 'note') +register_logs_urls(router, 'logs') app_name = 'api' diff --git a/apps/logs/api/__init__.py b/apps/logs/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/logs/api/serializers.py b/apps/logs/api/serializers.py new file mode 100644 index 00000000..7de7bc1a --- /dev/null +++ b/apps/logs/api/serializers.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers + +from ..models import Changelog + + +class ChangelogSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Changelog types. + The djangorestframework plugin will analyse the model `Changelog` and parse all fields in the API. + """ + + class Meta: + model = Changelog + fields = '__all__' diff --git a/apps/logs/api/urls.py b/apps/logs/api/urls.py new file mode 100644 index 00000000..9a0ceaa8 --- /dev/null +++ b/apps/logs/api/urls.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import ChangelogViewSet + + +def register_logs_urls(router, path): + """ + Configure router for Activity REST API. + """ + router.register(path, ChangelogViewSet) diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py new file mode 100644 index 00000000..60da612b --- /dev/null +++ b/apps/logs/api/views.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import viewsets + +from .serializers import ChangelogSerializer +from ..models import Changelog + + +class ChangelogViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, + then render it on /api/logs/ + """ + queryset = Changelog.objects.all() + serializer_class = ChangelogSerializer diff --git a/apps/logs/models.py b/apps/logs/models.py index 9ab3cf6a..10e2651f 100644 --- a/apps/logs/models.py +++ b/apps/logs/models.py @@ -56,6 +56,12 @@ class Changelog(models.Model): max_length=16, null=False, blank=False, + choices=[ + ('create', _('create')), + ('edit', _('edit')), + ('delete', _('delete')), + ], + default='edit', verbose_name=_('action'), ) diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 1cd74067..41f87cda 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -69,6 +69,7 @@ def save_object(sender, instance, **kwargs): else: user = note.get().user + # noinspection PyProtectedMember if user is not None and instance._meta.label_lower == "auth.user" and previous: # On n'enregistre pas les connexions if instance.last_login != previous.last_login: @@ -80,8 +81,8 @@ def save_object(sender, instance, **kwargs): model = instance.__class__ fields = '__all__' - previous_json = JSONRenderer().render(CustomSerializer(previous).data) - instance_json = JSONRenderer().render(CustomSerializer(instance).data) + previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") + instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8") if previous_json == instance_json: # Pas de log s'il n'y a pas de modification @@ -115,7 +116,7 @@ def delete_object(sender, instance, **kwargs): model = instance.__class__ fields = '__all__' - instance_json = JSONRenderer().render(CustomSerializer(instance).data) + instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8") Changelog.objects.create(user=user, ip=ip, diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 737350e2..84f07b6c 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -140,6 +140,7 @@ REST_FRAMEWORK = { 'rest_framework.permissions.DjangoModelPermissions' ], 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ] } From 417cd5da04c1d74dc52f9f72e6b1ea00c33654a4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 11:15:03 +0100 Subject: [PATCH 13/29] Improve REST API with filters --- apps/activity/api/views.py | 9 ++++++- apps/api/urls.py | 5 ++++ apps/logs/api/views.py | 3 +++ apps/logs/signals.py | 2 +- apps/logs/urls.py | 8 ------- apps/member/api/views.py | 5 ++++ apps/note/api/serializers.py | 22 ++++++++++++++++- apps/note/api/urls.py | 3 +-- apps/note/api/views.py | 24 +++++++++---------- apps/note/fixtures/initial.json | 12 +++++----- note_kfet/settings/base.py | 6 +++-- note_kfet/urls.py | 3 +-- .../rest_framework/crispy_form.html | 5 ++++ .../django_filters/rest_framework/form.html | 6 +++++ .../django_filters/widgets/multiwidget.html | 1 + 15 files changed, 78 insertions(+), 36 deletions(-) delete mode 100644 apps/logs/urls.py create mode 100644 templates/django_filters/rest_framework/crispy_form.html create mode 100644 templates/django_filters/rest_framework/form.html create mode 100644 templates/django_filters/widgets/multiwidget.html diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 6a6c024e..4ee2194d 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -1,7 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer from ..models import ActivityType, Activity, Guest @@ -15,6 +16,8 @@ class ActivityTypeViewSet(viewsets.ModelViewSet): """ queryset = ActivityType.objects.all() serializer_class = ActivityTypeSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'can_invite', ] class ActivityViewSet(viewsets.ModelViewSet): @@ -25,6 +28,8 @@ class ActivityViewSet(viewsets.ModelViewSet): """ queryset = Activity.objects.all() serializer_class = ActivitySerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'description', 'activity_type', ] class GuestViewSet(viewsets.ModelViewSet): @@ -35,3 +40,5 @@ class GuestViewSet(viewsets.ModelViewSet): """ queryset = Guest.objects.all() serializer_class = GuestSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] diff --git a/apps/api/urls.py b/apps/api/urls.py index bb1fdce2..8e0f6415 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,7 +3,9 @@ from django.conf.urls import url, include from django.contrib.auth.models import User +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import routers, serializers, viewsets +from rest_framework.filters import SearchFilter from activity.api.urls import register_activity_urls from member.api.urls import register_members_urls from note.api.urls import register_note_urls @@ -33,6 +35,9 @@ class UserViewSet(viewsets.ModelViewSet): """ queryset = User.objects.all() serializer_class = UserSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] + search_fields = ['$username', '$first_name', '$last_name', ] # Routers provide an easy way of automatically determining the URL conf. diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py index 60da612b..5b1b3ff6 100644 --- a/apps/logs/api/views.py +++ b/apps/logs/api/views.py @@ -1,6 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from .serializers import ChangelogSerializer @@ -15,3 +16,5 @@ class ChangelogViewSet(viewsets.ModelViewSet): """ queryset = Changelog.objects.all() serializer_class = ChangelogSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip',] diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 41f87cda..ab196291 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -81,7 +81,7 @@ def save_object(sender, instance, **kwargs): model = instance.__class__ fields = '__all__' - previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") + previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8") if previous_json == instance_json: diff --git a/apps/logs/urls.py b/apps/logs/urls.py deleted file mode 100644 index 6d76674c..00000000 --- a/apps/logs/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -app_name = 'logs' - -# TODO User interface -urlpatterns = [ -] diff --git a/apps/member/api/views.py b/apps/member/api/views.py index 7e7dcd1d..c85df903 100644 --- a/apps/member/api/views.py +++ b/apps/member/api/views.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer from ..models import Profile, Club, Role, Membership @@ -25,6 +26,8 @@ class ClubViewSet(viewsets.ModelViewSet): """ queryset = Club.objects.all() serializer_class = ClubSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class RoleViewSet(viewsets.ModelViewSet): @@ -35,6 +38,8 @@ class RoleViewSet(viewsets.ModelViewSet): """ queryset = Role.objects.all() serializer_class = RoleSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class MembershipViewSet(viewsets.ModelViewSet): diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 61257ec4..73beead1 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -5,7 +5,8 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ + TemplateTransaction class NoteSerializer(serializers.ModelSerializer): @@ -111,6 +112,17 @@ class TransactionSerializer(serializers.ModelSerializer): fields = '__all__' +class TemplateTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Transactions. + The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API. + """ + + class Meta: + model = TemplateTransaction + fields = '__all__' + + class MembershipTransactionSerializer(serializers.ModelSerializer): """ REST API Serializer for Membership transactions. @@ -120,3 +132,11 @@ class MembershipTransactionSerializer(serializers.ModelSerializer): class Meta: model = MembershipTransaction fields = '__all__' + + +class TransactionPolymorphicSerializer(PolymorphicSerializer): + model_serializer_mapping = { + Transaction: TransactionSerializer, + TemplateTransaction: TemplateTransactionSerializer, + MembershipTransaction: MembershipTransactionSerializer, + } diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 5e176ec5..796a397f 100644 --- a/apps/note/api/urls.py +++ b/apps/note/api/urls.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from .views import NotePolymorphicViewSet, AliasViewSet, \ - TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet + TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet def register_note_urls(router, path): @@ -15,4 +15,3 @@ def register_note_urls(router, path): router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/transaction', TransactionViewSet) router.register(path + '/transaction/template', TransactionTemplateViewSet) - router.register(path + '/transaction/membership', MembershipTransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 4fbb9481..14f64003 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -2,13 +2,15 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.filters import SearchFilter from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ NoteUserSerializer, AliasSerializer, \ - TemplateCategorySerializer, TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer + TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory +from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory class NoteViewSet(viewsets.ModelViewSet): @@ -139,6 +141,8 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet): """ queryset = TemplateCategory.objects.all() serializer_class = TemplateCategorySerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] class TransactionTemplateViewSet(viewsets.ModelViewSet): @@ -149,6 +153,8 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet): """ queryset = TransactionTemplate.objects.all() serializer_class = TransactionTemplateSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['name', 'amount', 'display', 'category', ] class TransactionViewSet(viewsets.ModelViewSet): @@ -158,14 +164,6 @@ class TransactionViewSet(viewsets.ModelViewSet): then render it on /api/note/transaction/transaction/ """ queryset = Transaction.objects.all() - serializer_class = TransactionSerializer - - -class MembershipTransactionViewSet(viewsets.ModelViewSet): - """ - REST API View set. - The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer, - then render it on /api/note/transaction/membership/ - """ - queryset = MembershipTransaction.objects.all() - serializer_class = MembershipTransactionSerializer + serializer_class = TransactionPolymorphicSerializer + filter_backends = [SearchFilter] + search_fields = ['$reason', ] diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 01242b22..3654fa2f 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -3,7 +3,7 @@ "model": "note.note", "pk": 1, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 37, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 36, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 36, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 84f07b6c..0694390d 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -137,12 +137,14 @@ REST_FRAMEWORK = { # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ # TODO Maybe replace it with our custom permissions system - 'rest_framework.permissions.DjangoModelPermissions' + 'rest_framework.permissions.DjangoModelPermissions', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', - ] + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, } # Internationalization diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 407659f8..da2f9d6c 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -20,8 +20,7 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), - path('logs/', include('logs.urls')), - path('api/', include('api.urls')), + path('api/', include('api.urls')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/templates/django_filters/rest_framework/crispy_form.html b/templates/django_filters/rest_framework/crispy_form.html new file mode 100644 index 00000000..171767c0 --- /dev/null +++ b/templates/django_filters/rest_framework/crispy_form.html @@ -0,0 +1,5 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +

{% trans "Field filters" %}

+{% crispy filter.form %} diff --git a/templates/django_filters/rest_framework/form.html b/templates/django_filters/rest_framework/form.html new file mode 100644 index 00000000..b116e353 --- /dev/null +++ b/templates/django_filters/rest_framework/form.html @@ -0,0 +1,6 @@ +{% load i18n %} +

{% trans "Field filters" %}

+ + {{ filter.form.as_p }} + + diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html new file mode 100644 index 00000000..089ddb20 --- /dev/null +++ b/templates/django_filters/widgets/multiwidget.html @@ -0,0 +1 @@ +{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %} From 13797598033f7841e66f70fe934e0ff96a19bda6 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 11:25:40 +0100 Subject: [PATCH 14/29] Changelogs are read-only protected --- apps/logs/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/logs/api/serializers.py b/apps/logs/api/serializers.py index 7de7bc1a..108f2087 100644 --- a/apps/logs/api/serializers.py +++ b/apps/logs/api/serializers.py @@ -15,3 +15,5 @@ class ChangelogSerializer(serializers.ModelSerializer): class Meta: model = Changelog fields = '__all__' + # noinspection PyProtectedMember + read_only_fields = [f.name for f in model._meta.get_fields()] # Changelogs are read-only protected From 2c440062cec1b20c3823651b59e0a9a9ab53b0d7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 11:33:14 +0100 Subject: [PATCH 15/29] Changelogs are read-only protected --- apps/logs/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py index 5b1b3ff6..1ddfd7bc 100644 --- a/apps/logs/api/views.py +++ b/apps/logs/api/views.py @@ -8,7 +8,7 @@ from .serializers import ChangelogSerializer from ..models import Changelog -class ChangelogViewSet(viewsets.ModelViewSet): +class ChangelogViewSet(viewsets.ReadOnlyModelViewSet): """ REST API View set. The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, From b89028b3dca54a5bb67894878947ad59d6e936ff Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 11:37:47 +0100 Subject: [PATCH 16/29] Add models in the API --- apps/api/urls.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/api/urls.py b/apps/api/urls.py index 8e0f6415..9bbe1978 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import url, include from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django_filters.rest_framework import DjangoFilterBackend from rest_framework import routers, serializers, viewsets from rest_framework.filters import SearchFilter @@ -26,6 +27,16 @@ class UserSerializer(serializers.ModelSerializer): 'user_permissions', ) +class ContentTypeSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Users. + The djangorestframework plugin will analyse the model `User` and parse all fields in the API. + """ + + class Meta: + model = ContentType + fields = '__all__' + class UserViewSet(viewsets.ModelViewSet): """ @@ -40,9 +51,20 @@ class UserViewSet(viewsets.ModelViewSet): search_fields = ['$username', '$first_name', '$last_name', ] +class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, + then render it on /api/users/ + """ + queryset = ContentType.objects.all() + serializer_class = ContentTypeSerializer + + # Routers provide an easy way of automatically determining the URL conf. # Register each app API router and user viewset router = routers.DefaultRouter() +router.register('models', ContentTypeViewSet) router.register('user', UserViewSet) register_members_urls(router, 'members') register_activity_urls(router, 'activity') From f082f8523ad78c30f7e44b142421cca858728a8f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 11 Mar 2020 12:05:29 +0100 Subject: [PATCH 17/29] Update translations --- apps/note/models/notes.py | 2 +- apps/note/views.py | 2 +- locale/de/LC_MESSAGES/django.po | 161 ++++++++------ locale/fr/LC_MESSAGES/django.po | 216 +++++++++++-------- templates/base.html | 2 +- templates/member/club_form.html | 5 +- templates/member/club_list.html | 3 +- templates/member/signup.html | 6 +- templates/note/transactiontemplate_form.html | 3 +- 9 files changed, 234 insertions(+), 166 deletions(-) diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 74cda3ea..c53c0140 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -231,7 +231,7 @@ class Alias(models.Model): try: sim_alias = Alias.objects.get(normalized_name=normalized_name) if self != sim_alias: - raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)), + raise ValidationError(_('An alias with a similar name already exists: {} ').format(sim_alias), code="same_alias" ) except Alias.DoesNotExist: diff --git a/apps/note/views.py b/apps/note/views.py index ccba8c9f..7064370a 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -136,7 +136,7 @@ class ConsoView(LoginRequiredMixin, CreateView): context = super().get_context_data(**kwargs) context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \ .order_by('category') - context['title'] = _("Consommations") + context['title'] = _("Consumptions") # select2 compatibility context['no_cache'] = True diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index ce17f5de..6c60a9fe 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-07 18:01+0100\n" +"POT-Creation-Date: 2020-03-11 11:44+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -24,7 +24,7 @@ msgstr "" #: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/member/models.py:60 apps/member/models.py:111 -#: apps/note/models/notes.py:187 apps/note/models/transactions.py:24 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 msgid "name" msgstr "" @@ -49,7 +49,7 @@ msgstr "" msgid "description" msgstr "" -#: apps/activity/models.py:54 apps/note/models/notes.py:163 +#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/note/models/transactions.py:62 msgid "type" msgstr "" @@ -90,7 +90,7 @@ msgstr "" msgid "Logs" msgstr "" -#: apps/logs/models.py:21 apps/note/models/notes.py:116 +#: apps/logs/models.py:21 apps/note/models/notes.py:117 msgid "user" msgstr "" @@ -114,15 +114,27 @@ msgstr "" msgid "new data" msgstr "" -#: apps/logs/models.py:59 +#: apps/logs/models.py:60 +msgid "create" +msgstr "" + +#: apps/logs/models.py:61 +msgid "edit" +msgstr "" + +#: apps/logs/models.py:62 +msgid "delete" +msgstr "" + +#: apps/logs/models.py:65 msgid "action" msgstr "" -#: apps/logs/models.py:67 +#: apps/logs/models.py:73 msgid "timestamp" msgstr "" -#: apps/logs/models.py:71 +#: apps/logs/models.py:77 msgid "Logs cannot be destroyed." msgstr "" @@ -188,7 +200,7 @@ msgid "" "members can renew their membership." msgstr "" -#: apps/member/models.py:93 apps/note/models/notes.py:138 +#: apps/member/models.py:93 apps/note/models/notes.py:139 msgid "club" msgstr "" @@ -237,7 +249,7 @@ msgstr "" msgid "Account #%(id)s: %(username)s" msgstr "" -#: apps/member/views.py:200 +#: apps/member/views.py:202 msgid "Alias successfully deleted" msgstr "" @@ -250,127 +262,127 @@ msgstr "" msgid "destination" msgstr "" -#: apps/note/apps.py:14 apps/note/models/notes.py:57 +#: apps/note/apps.py:14 apps/note/models/notes.py:58 msgid "note" msgstr "" -#: apps/note/forms.py:26 +#: apps/note/forms.py:20 msgid "New Alias" msgstr "" -#: apps/note/forms.py:31 +#: apps/note/forms.py:25 msgid "select an image" msgstr "" -#: apps/note/forms.py:32 +#: apps/note/forms.py:26 msgid "Maximal size: 2MB" msgstr "" -#: apps/note/forms.py:77 +#: apps/note/forms.py:70 msgid "Source and destination must be different." msgstr "" -#: apps/note/models/notes.py:26 +#: apps/note/models/notes.py:27 msgid "account balance" msgstr "" -#: apps/note/models/notes.py:27 +#: apps/note/models/notes.py:28 msgid "in centimes, money credited for this instance" msgstr "" -#: apps/note/models/notes.py:31 +#: apps/note/models/notes.py:32 msgid "last negative date" msgstr "" -#: apps/note/models/notes.py:32 +#: apps/note/models/notes.py:33 msgid "last time the balance was negative" msgstr "" -#: apps/note/models/notes.py:37 +#: apps/note/models/notes.py:38 msgid "active" msgstr "" -#: apps/note/models/notes.py:40 +#: apps/note/models/notes.py:41 msgid "" "Designates whether this note should be treated as active. Unselect this " "instead of deleting notes." msgstr "" -#: apps/note/models/notes.py:44 +#: apps/note/models/notes.py:45 msgid "display image" msgstr "" -#: apps/note/models/notes.py:52 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 msgid "created at" msgstr "" -#: apps/note/models/notes.py:58 +#: apps/note/models/notes.py:59 msgid "notes" msgstr "" -#: apps/note/models/notes.py:66 +#: apps/note/models/notes.py:67 msgid "Note" msgstr "" -#: apps/note/models/notes.py:76 apps/note/models/notes.py:100 +#: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "" -#: apps/note/models/notes.py:120 +#: apps/note/models/notes.py:121 msgid "one's note" msgstr "" -#: apps/note/models/notes.py:121 +#: apps/note/models/notes.py:122 msgid "users note" msgstr "" -#: apps/note/models/notes.py:127 +#: apps/note/models/notes.py:128 #, python-format msgid "%(user)s's note" msgstr "" -#: apps/note/models/notes.py:142 +#: apps/note/models/notes.py:143 msgid "club note" msgstr "" -#: apps/note/models/notes.py:143 +#: apps/note/models/notes.py:144 msgid "clubs notes" msgstr "" -#: apps/note/models/notes.py:149 +#: apps/note/models/notes.py:150 #, python-format msgid "Note of %(club)s club" msgstr "" -#: apps/note/models/notes.py:169 +#: apps/note/models/notes.py:170 msgid "special note" msgstr "" -#: apps/note/models/notes.py:170 +#: apps/note/models/notes.py:171 msgid "special notes" msgstr "" -#: apps/note/models/notes.py:193 +#: apps/note/models/notes.py:194 msgid "Invalid alias" msgstr "" -#: apps/note/models/notes.py:209 +#: apps/note/models/notes.py:210 msgid "alias" msgstr "" -#: apps/note/models/notes.py:210 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 msgid "aliases" msgstr "" -#: apps/note/models/notes.py:228 +#: apps/note/models/notes.py:229 msgid "Alias is too long." msgstr "" -#: apps/note/models/notes.py:233 +#: apps/note/models/notes.py:234 msgid "An alias with a similar name already exists: {} " msgstr "" -#: apps/note/models/notes.py:242 +#: apps/note/models/notes.py:243 msgid "You can't delete your main alias." msgstr "" @@ -422,11 +434,11 @@ msgstr "" msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:184 +#: apps/note/models/transactions.py:185 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:186 msgid "membership transactions" msgstr "" @@ -434,34 +446,54 @@ msgstr "" msgid "Transfer money from your account to one or others" msgstr "" -#: apps/note/views.py:138 +#: apps/note/views.py:139 msgid "Consommations" msgstr "" -#: note_kfet/settings/base.py:162 -msgid "German" -msgstr "" - -#: note_kfet/settings/base.py:163 -msgid "English" -msgstr "" - -#: note_kfet/settings/base.py:164 -msgid "French" -msgstr "" - -#: note_kfet/settings/base.py:215 +#: note_kfet/settings/__init__.py:63 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" +#: note_kfet/settings/base.py:156 +msgid "German" +msgstr "" + +#: note_kfet/settings/base.py:157 +msgid "English" +msgstr "" + +#: note_kfet/settings/base.py:158 +msgid "French" +msgstr "" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/cas_server/base.html:7 templates/cas_server/base.html:26 +#: templates/base.html:70 +msgid "Consumptions" +msgstr "" + +#: templates/base.html:73 +msgid "Clubs" +msgstr "" + +#: templates/base.html:76 +msgid "Activities" +msgstr "" + +#: templates/base.html:79 +msgid "Button" +msgstr "" + +#: templates/base.html:82 templates/note/transaction_form.html:35 +msgid "Transfer" +msgstr "" + +#: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -511,6 +543,15 @@ msgstr "" msgid "Connect to the service" msgstr "" +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "" + +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "" + #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "" @@ -583,10 +624,6 @@ msgstr "" msgid "Sign Up" msgstr "" -#: templates/note/transaction_form.html:35 -msgid "Transfer" -msgstr "" - #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -596,7 +633,7 @@ msgid "Log in again" msgstr "" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:22 +#: templates/registration/login.html:26 #: templates/registration/password_reset_complete.html:10 msgid "Log in" msgstr "" @@ -608,7 +645,7 @@ msgid "" "page. Would you like to login to a different account?" msgstr "" -#: templates/registration/login.html:23 +#: templates/registration/login.html:27 msgid "Forgotten your password or username?" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 3a8cfb79..05836a54 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-07 18:01+0100\n" +"POT-Creation-Date: 2020-03-11 11:44+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,7 +19,7 @@ msgstr "activité" #: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/member/models.py:60 apps/member/models.py:111 -#: apps/note/models/notes.py:187 apps/note/models/transactions.py:24 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 msgid "name" msgstr "nom" @@ -44,7 +44,7 @@ msgstr "types d'activité" msgid "description" msgstr "description" -#: apps/activity/models.py:54 apps/note/models/notes.py:163 +#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/note/models/transactions.py:62 msgid "type" msgstr "type" @@ -85,15 +85,13 @@ msgstr "" msgid "Logs" msgstr "" -#: apps/logs/models.py:21 apps/note/models/notes.py:116 +#: apps/logs/models.py:21 apps/note/models/notes.py:117 msgid "user" msgstr "utilisateur" #: apps/logs/models.py:27 -#, fuzzy -#| msgid "address" msgid "IP Address" -msgstr "adresse" +msgstr "Adresse IP" #: apps/logs/models.py:35 msgid "model" @@ -108,22 +106,30 @@ msgid "previous data" msgstr "Données précédentes" #: apps/logs/models.py:52 -#, fuzzy -#| msgid "end date" msgid "new data" msgstr "Nouvelles données" -#: apps/logs/models.py:59 -#, fuzzy -#| msgid "section" +#: apps/logs/models.py:60 +msgid "create" +msgstr "Créer" + +#: apps/logs/models.py:61 +msgid "edit" +msgstr "Modifier" + +#: apps/logs/models.py:62 +msgid "delete" +msgstr "Supprimer" + +#: apps/logs/models.py:65 msgid "action" msgstr "Action" -#: apps/logs/models.py:67 +#: apps/logs/models.py:73 msgid "timestamp" msgstr "Date" -#: apps/logs/models.py:71 +#: apps/logs/models.py:77 msgid "Logs cannot be destroyed." msgstr "Les logs ne peuvent pas être détruits." @@ -193,10 +199,16 @@ msgstr "" "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "suivante avant que les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:93 apps/note/models/notes.py:138 +#: apps/member/models.py:93 apps/note/models/notes.py:139 msgid "club" msgstr "club" +msgid "New club" +msgstr "Nouveau club" + +msgid "Clubs list" +msgstr "Liste des clubs" + #: apps/member/models.py:94 msgid "clubs" msgstr "clubs" @@ -242,9 +254,9 @@ msgstr "Un alias avec un nom similaire existe déjà." msgid "Account #%(id)s: %(username)s" msgstr "Compte n°%(id)s : %(username)s" -#: apps/member/views.py:200 +#: apps/member/views.py:202 msgid "Alias successfully deleted" -msgstr "" +msgstr "L'alias a bien été supprimé" #: apps/note/admin.py:120 apps/note/models/transactions.py:93 msgid "source" @@ -255,132 +267,128 @@ msgstr "source" msgid "destination" msgstr "destination" -#: apps/note/apps.py:14 apps/note/models/notes.py:57 +#: apps/note/apps.py:14 apps/note/models/notes.py:58 msgid "note" msgstr "note" -#: apps/note/forms.py:26 +#: apps/note/forms.py:20 msgid "New Alias" -msgstr "" +msgstr "Nouvel alias" -#: apps/note/forms.py:31 -#, fuzzy -#| msgid "display image" +#: apps/note/forms.py:25 msgid "select an image" -msgstr "image affichée" +msgstr "Choisissez une image" -#: apps/note/forms.py:32 +#: apps/note/forms.py:26 msgid "Maximal size: 2MB" -msgstr "" +msgstr "Taille maximale : 2 Mo" -#: apps/note/forms.py:77 +#: apps/note/forms.py:70 msgid "Source and destination must be different." msgstr "La source et la destination doivent être différentes." -#: apps/note/models/notes.py:26 +#: apps/note/models/notes.py:27 msgid "account balance" msgstr "solde du compte" -#: apps/note/models/notes.py:27 +#: apps/note/models/notes.py:28 msgid "in centimes, money credited for this instance" msgstr "en centimes, argent crédité pour cette instance" -#: apps/note/models/notes.py:31 +#: apps/note/models/notes.py:32 msgid "last negative date" msgstr "dernier date de négatif" -#: apps/note/models/notes.py:32 +#: apps/note/models/notes.py:33 msgid "last time the balance was negative" msgstr "dernier instant où la note était en négatif" -#: apps/note/models/notes.py:37 +#: apps/note/models/notes.py:38 msgid "active" msgstr "actif" -#: apps/note/models/notes.py:40 +#: apps/note/models/notes.py:41 msgid "" "Designates whether this note should be treated as active. Unselect this " "instead of deleting notes." msgstr "" "Indique si la note est active. Désactiver cela plutôt que supprimer la note." -#: apps/note/models/notes.py:44 +#: apps/note/models/notes.py:45 msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:52 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 msgid "created at" msgstr "créée le" -#: apps/note/models/notes.py:58 +#: apps/note/models/notes.py:59 msgid "notes" msgstr "notes" -#: apps/note/models/notes.py:66 +#: apps/note/models/notes.py:67 msgid "Note" msgstr "Note" -#: apps/note/models/notes.py:76 apps/note/models/notes.py:100 +#: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "Cet alias est déjà pris." -#: apps/note/models/notes.py:120 +#: apps/note/models/notes.py:121 msgid "one's note" msgstr "note d'un utilisateur" -#: apps/note/models/notes.py:121 +#: apps/note/models/notes.py:122 msgid "users note" msgstr "notes des utilisateurs" -#: apps/note/models/notes.py:127 +#: apps/note/models/notes.py:128 #, python-format msgid "%(user)s's note" msgstr "Note de %(user)s" -#: apps/note/models/notes.py:142 +#: apps/note/models/notes.py:143 msgid "club note" msgstr "note d'un club" -#: apps/note/models/notes.py:143 +#: apps/note/models/notes.py:144 msgid "clubs notes" msgstr "notes des clubs" -#: apps/note/models/notes.py:149 +#: apps/note/models/notes.py:150 #, python-format msgid "Note of %(club)s club" msgstr "Note du club %(club)s" -#: apps/note/models/notes.py:169 +#: apps/note/models/notes.py:170 msgid "special note" msgstr "note spéciale" -#: apps/note/models/notes.py:170 +#: apps/note/models/notes.py:171 msgid "special notes" msgstr "notes spéciales" -#: apps/note/models/notes.py:193 +#: apps/note/models/notes.py:194 msgid "Invalid alias" msgstr "Alias invalide" -#: apps/note/models/notes.py:209 +#: apps/note/models/notes.py:210 msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:210 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 msgid "aliases" msgstr "alias" -#: apps/note/models/notes.py:228 +#: apps/note/models/notes.py:229 msgid "Alias is too long." msgstr "L'alias est trop long." -#: apps/note/models/notes.py:233 -#, fuzzy -#| msgid "An alias with a similar name already exists:" +#: apps/note/models/notes.py:234 msgid "An alias with a similar name already exists: {} " -msgstr "Un alias avec un nom similaire existe déjà." +msgstr "Un alias avec un nom similaire existe déjà : {}" -#: apps/note/models/notes.py:242 +#: apps/note/models/notes.py:243 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." @@ -393,7 +401,6 @@ msgid "transaction categories" msgstr "catégories de transaction" #: apps/note/models/transactions.py:47 -#, fuzzy msgid "A template with this name already exist" msgstr "Un modèle de transaction avec un nom similaire existe déjà." @@ -433,11 +440,11 @@ msgstr "transaction" msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:184 +#: apps/note/models/transactions.py:185 msgid "membership transaction" msgstr "transaction d'adhésion" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:186 msgid "membership transactions" msgstr "transactions d'adhésion" @@ -445,34 +452,53 @@ msgstr "transactions d'adhésion" msgid "Transfer money from your account to one or others" msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres" -#: apps/note/views.py:138 -msgid "Consommations" -msgstr "transactions" - -#: note_kfet/settings/base.py:162 -msgid "German" -msgstr "" - -#: note_kfet/settings/base.py:163 -msgid "English" -msgstr "" - -#: note_kfet/settings/base.py:164 -msgid "French" -msgstr "" - -#: note_kfet/settings/base.py:215 +#: note_kfet/settings/__init__.py:63 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" +#: note_kfet/settings/base.py:156 +msgid "German" +msgstr "" + +#: note_kfet/settings/base.py:157 +msgid "English" +msgstr "" + +#: note_kfet/settings/base.py:158 +msgid "French" +msgstr "" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/cas_server/base.html:7 templates/cas_server/base.html:26 +#: templates/base.html:70 +msgid "Consumptions" +msgstr "Consommations" + +#: templates/base.html:73 +msgid "Clubs" +msgstr "Clubs" + +#: templates/base.html:76 +msgid "Activities" +msgstr "Activités" + +#: templates/base.html:79 +msgid "Buttons" +msgstr "Boutons" + +msgid "Buttons list" +msgstr "Liste des boutons" + +#: templates/base.html:82 templates/note/transaction_form.html:35 +msgid "Transfer" +msgstr "Virement" + +#: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -510,11 +536,11 @@ msgstr "" #: templates/cas_server/login.html:11 msgid "" -"If you don't have any Note Kfet account, please follow this link to sign up." +"If you don't have any Note Kfet account, please follow this link to sign up." msgstr "" -"Si vous n'avez pas de compte Note Kfet, veuillez suivre ce lien pour vous inscrire." +"Si vous n'avez pas de compte Note Kfet, veuillez suivre ce lien pour vous inscrire." #: templates/cas_server/login.html:17 msgid "Login" @@ -524,6 +550,15 @@ msgstr "" msgid "Connect to the service" msgstr "" +#: templates/django_filters/rest_framework/crispy_form.html:4 +#: templates/django_filters/rest_framework/form.html:2 +msgid "Field filters" +msgstr "" + +#: templates/django_filters/rest_framework/form.html:5 +msgid "Submit" +msgstr "" + #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "L'adhésion commence le" @@ -557,10 +592,8 @@ msgid "Regenerate token" msgstr "Regénérer le jeton" #: templates/member/profile_alias.html:10 -#, fuzzy -#| msgid "alias" msgid "Add alias" -msgstr "alias" +msgstr "Ajouter un alias" #: templates/member/profile_detail.html:15 msgid "first name" @@ -583,10 +616,8 @@ msgid "Manage auth token" msgstr "Gérer les jetons d'authentification" #: templates/member/profile_detail.html:49 -#, fuzzy -#| msgid "Update Profile" msgid "View Profile" -msgstr "Modifier le profil" +msgstr "Voir le profil" #: templates/member/profile_detail.html:62 msgid "View my memberships" @@ -596,13 +627,10 @@ msgstr "Voir mes adhésions" msgid "Save Changes" msgstr "Sauvegarder les changements" +#: templates/member/signup.html:8 #: templates/member/signup.html:14 -msgid "Sign Up" -msgstr "" - -#: templates/note/transaction_form.html:35 -msgid "Transfer" -msgstr "Virement" +msgid "Sign up" +msgstr "Inscription" #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." @@ -613,7 +641,7 @@ msgid "Log in again" msgstr "" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:22 +#: templates/registration/login.html:26 #: templates/registration/password_reset_complete.html:10 msgid "Log in" msgstr "" @@ -625,7 +653,7 @@ msgid "" "page. Would you like to login to a different account?" msgstr "" -#: templates/registration/login.html:23 +#: templates/registration/login.html:27 msgid "Forgotten your password or username?" msgstr "" diff --git a/templates/base.html b/templates/base.html index 887bc970..94810b87 100644 --- a/templates/base.html +++ b/templates/base.html @@ -76,7 +76,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Activities' %}