diff --git a/.env_example b/.env_example new file mode 100644 index 00000000..5aba0d14 --- /dev/null +++ b/.env_example @@ -0,0 +1,13 @@ +DJANGO_APP_STAGE="dev" +# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev +DJANGO_DEV_STORE_METHOD="sqllite" +DJANGO_DB_HOST="localhost" +DJANGO_DB_NAME="note_db" +DJANGO_DB_USER="note" +DJANGO_DB_PASSWORD="CHANGE_ME" +DJANGO_DB_PORT="" +DJANGO_SECRET_KEY="CHANGE_ME" +DJANGO_SETTINGS_MODULE="note_kfet.settings" +DOMAIN="localhost" +CONTACT_EMAIL="tresorerie.bde@localhost" +NOTE_URL="localhost" 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 diff --git a/README.md b/README.md index 14ec5f42..91f2f17d 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,13 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n $ cp nginx_note.conf_example nginx_note.conf -***Modifier le fichier pour être en accord avec le reste de votre config*** + ***Modifier le fichier pour être en accord avec le reste de votre config*** On utilise uwsgi et Nginx pour gérer le coté serveur : - $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/ + $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/ - - Si l'on a un emperor (plusieurs instance uwsgi): + Si l'on a un emperor (plusieurs instance uwsgi): $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/ @@ -85,7 +84,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n postgres=# CREATE DATABASE note_db OWNER note; CREATE DATABASE - Si tout va bien: + Si tout va bien : postgres=#\list List of databases @@ -96,22 +95,29 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres (4 rows) - - Dans un fichier `.env` à la racine du projet on renseigne des secrets: - DJANGO_APP_STAGE='prod' - DJANGO_DB_PASSWORD='le_mot_de_passe_de_la_bdd' - DJANGO_SECRET_KEY='une_secret_key_longue_et_compliquee' - ALLOWED_HOSTS='le_ndd_de_votre_instance' - - 6. Variable d'environnement et Migrations + On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet + et on renseigne des secrets et des paramètres : + + DJANGO_APP_STAGE="dev" + DJANGO_DEV_STORE_METHOD="sqllite" + DJANGO_DB_HOST="localhost" + DJANGO_DB_NAME="note_db" + DJANGO_DB_USER="note" + DJANGO_DB_PASSWORD="CHANGE_ME" + DJANGO_DB_PORT="" + DJANGO_SECRET_KEY="CHANGE_ME" + DJANGO_SETTINGS_MODULE="note_kfet.settings" + DOMAIN="localhost" + CONTACT_EMAIL="tresorerie.bde@localhost" + NOTE_URL="localhost" -Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations + Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations $ source /env/bin/activate - (env)$ ./manage.py check # pas de bétise qui traine + (env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py makemigrations (env)$ ./manage.py migrate @@ -126,17 +132,21 @@ Il est possible de travailler sur une instance Docker. $ git clone git@gitlab.crans.org:bde/nk20.git -2. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré, +2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`, +et mettez à jour vos variables d'environnement + +3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré, ajouter les lignes suivantes, en les adaptant à la configuration voulue : nk20: build: /chemin/vers/nk20 volumes: - /chemin/vers/nk20:/code/ + env_file: /chemin/vers/nk20/.env restart: always labels: - - traefik.domain=ndd.exemple.com - - traefik.frontend.rule=Host:ndd.exemple.com + - traefik.domain=ndd.example.com + - traefik.frontend.rule=Host:ndd.example.com - traefik.port=8000 3. Enjoy : @@ -159,17 +169,20 @@ un serveur de développement par exemple sur son ordinateur. $ source venv/bin/activate (env)$ pip install -r requirements.txt -3. Migrations et chargement des données initiales : +3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour +ce qu'il faut + +4. Migrations et chargement des données initiales : (env)$ ./manage.py makemigrations (env)$ ./manage.py migrate (env)$ ./manage.py loaddata initial -4. Créer un super-utilisateur : +5. Créer un super-utilisateur : (env)$ ./manage.py createsuperuser -5. Enjoy : +6. Enjoy : (env)$ ./manage.py runserver 0.0.0.0:8000 @@ -184,4 +197,4 @@ Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC). ## Documentation La documentation est générée par django et son module admindocs. -**Commenter votre code !** +**Commentez votre code !** 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 c1b6bf48..95ed5f99 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -3,10 +3,14 @@ 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 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): @@ -24,6 +28,17 @@ class UserSerializer(serializers.ModelSerializer): ) +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): """ REST API View set. @@ -32,15 +47,30 @@ 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', ] + + +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') 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..c76e3a5d --- /dev/null +++ b/apps/logs/api/serializers.py @@ -0,0 +1,19 @@ +# 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__' + # noinspection PyProtectedMember + read_only_fields = [f.name for f in model._meta.get_fields()] # Changelogs are read-only protected 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..2c47b7a2 --- /dev/null +++ b/apps/logs/api/views.py @@ -0,0 +1,23 @@ +# 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 OrderingFilter + +from .serializers import ChangelogSerializer +from ..models import Changelog + + +class ChangelogViewSet(viewsets.ReadOnlyModelViewSet): + """ + 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 + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ] + ordering_fields = ['timestamp', ] + ordering = ['-timestamp', ] diff --git a/apps/logs/apps.py b/apps/logs/apps.py index f48820c7..239f86cf 100644 --- a/apps/logs/apps.py +++ b/apps/logs/apps.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig +from django.db.models.signals import pre_save, post_save, post_delete from django.utils.translation import gettext_lazy as _ @@ -11,4 +12,7 @@ class LogsConfig(AppConfig): def ready(self): # noinspection PyUnresolvedReferences - import logs.signals + from . import signals + pre_save.connect(signals.pre_save_object) + post_save.connect(signals.save_object) + post_delete.connect(signals.delete_object) diff --git a/apps/logs/middlewares.py b/apps/logs/middlewares.py new file mode 100644 index 00000000..77f749b9 --- /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): + 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): + """ + This middleware get the current user with his or her IP address on each request. + """ + + 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) + _set_current_user_and_ip(None, None) + + return response 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 415e7c1c..fb17157a 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -1,67 +1,40 @@ # 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 rest_framework.renderers import JSONRenderer +from rest_framework.serializers import ModelSerializer +import getpass + +from note.models import NoteUser, Alias + +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 - - +# Ces modèles ne nécessitent pas de logs EXCLUDED = [ 'admin.logentry', 'authtoken.token', + 'cas_server.proxygrantingticket', + 'cas_server.proxyticket', + 'cas_server.serviceticket', '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', - 'reversion.revision', - 'reversion.version', ] -@receiver(pre_save) def pre_save_object(sender, instance, **kwargs): + """ + Before a model get saved, we get the previous instance that is currently in the database + """ qs = sender.objects.filter(pk=instance.pk).all() if qs.exists(): instance._previous = qs.get() @@ -69,30 +42,51 @@ def pre_save_object(sender, instance, **kwargs): instance._previous = None -@receiver(post_save) def save_object(sender, instance, **kwargs): + """ + Each time a model is saved, an entry in the table `Changelog` is added in the database + in order to store each modification made + """ # noinspection PyProtectedMember if instance._meta.label_lower in EXCLUDED: return + # noinspection PyProtectedMember previous = instance._previous - user, ip = get_user_and_ip(sender) + # 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() - from django.contrib.auth.models import AnonymousUser - if isinstance(user, AnonymousUser): - user = None + 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=username) + # if not note.exists(): + # print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username) + # else: + if note.exists(): + user = note.get().user + # noinspection PyProtectedMember 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 - 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).decode("UTF-8") if previous else None + instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8") if previous_json == instance_json: - # No modification + # Pas de log s'il n'y a pas de modification return Changelog.objects.create(user=user, @@ -105,15 +99,38 @@ def save_object(sender, instance, **kwargs): ).save() -@receiver(post_delete) def delete_object(sender, instance, **kwargs): + """ + Each time a model is deleted, an entry in the table `Changelog` is added in the database + """ # noinspection PyProtectedMember if instance._meta.label_lower in EXCLUDED: return - user, ip = get_user_and_ip(sender) + # 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=username) + # if not note.exists(): + # print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username) + # else: + if note.exists(): + user = note.get().user + + # 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).decode("UTF-8") - instance_json = serializers.serialize('json', [instance, ])[1:-1] Changelog.objects.create(user=user, ip=ip, model=ContentType.objects.get_for_model(instance), 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/member/models.py b/apps/member/models.py index 5cdc4c77..b6d17a08 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -46,7 +46,7 @@ class Profile(models.Model): class Meta: verbose_name = _('user profile') verbose_name_plural = _('user profile') - indexes = [ models.Index(fields=['user']) ] + indexes = [models.Index(fields=['user'])] def get_absolute_url(self): return reverse('user_detail', args=(self.pk,)) @@ -153,7 +153,7 @@ class Membership(models.Model): class Meta: verbose_name = _('membership') verbose_name_plural = _('memberships') - indexes = [ models.Index(fields=['user']) ] + indexes = [models.Index(fields=['user'])] # @receiver(post_save, sender=settings.AUTH_USER_MODEL) # def save_user_profile(instance, created, **_kwargs): 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/serializers.py b/apps/note/api/serializers.py index 1696bfee..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 +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ + TemplateTransaction class NoteSerializer(serializers.ModelSerializer): @@ -78,6 +79,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. @@ -100,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. @@ -109,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 54218796..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, \ - TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet + TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet def register_note_urls(router, path): @@ -12,6 +12,6 @@ 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 cf0136f2..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, \ - TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer + TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction +from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory class NoteViewSet(viewsets.ModelViewSet): @@ -69,8 +71,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 +109,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: @@ -131,6 +133,18 @@ 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 + filter_backends = [SearchFilter] + search_fields = ['$name', ] + + class TransactionTemplateViewSet(viewsets.ModelViewSet): """ REST API View set. @@ -139,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): @@ -148,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 f80332c0..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": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 39, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 38, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 38, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 70810ad8..b6b00aa8 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -235,7 +235,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/templatetags/getenv.py b/apps/note/templatetags/getenv.py new file mode 100644 index 00000000..c133cb8f --- /dev/null +++ b/apps/note/templatetags/getenv.py @@ -0,0 +1,14 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django import template + +import os + + +def getenv(value): + return os.getenv(value) + + +register = template.Library() +register.filter('getenv', getenv) diff --git a/apps/note/views.py b/apps/note/views.py index fb5e98c5..16e2e39b 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -69,7 +69,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) @@ -141,7 +141,7 @@ class ConsoView(LoginRequiredMixin, SingleTableView): 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/entrypoint.sh b/entrypoint.sh index f05e962a..e5a22a5a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,12 +2,17 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +if [ -z ${NOTE_URL+x} ]; then + echo "Warning: your env files are not configurated." +else + sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json + sed -i -e "s/localhost/$NOTE_URL/g" /code/note_kfet/fixtures/initial.json + sed -i -e "s/\.\*/https?:\/\/$NOTE_URL\/.*/g" /code/note_kfet/fixtures/cas.json + sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json +fi + python manage.py compilemessages python manage.py makemigrations - -# Wait for database -sleep 5 python manage.py migrate -# TODO: use uwsgi in production python manage.py runserver 0.0.0.0:8000 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/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 6d871599..28935deb 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 * @@ -30,10 +35,6 @@ read_env() app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev') if app_stage == 'prod': from .production import * - - DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS') - SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS') - ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', 'localhost')] else: from .development import * @@ -46,12 +47,14 @@ except ImportError: if "cas" in INSTALLED_APPS: MIDDLEWARE += ['cas.middleware.CASMiddleware'] # CAS Settings + CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/" CAS_AUTO_CREATE_USER = False CAS_LOGO_URL = "/static/img/Saperlistpopette.png" CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png" CAS_SHOW_SERVICE_MESSAGES = True CAS_SHOW_POWERED = False CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False + CAS_PROVIDE_URL_TO_LOGOUT = True CAS_INFO_MESSAGES = { "cas_explained": { "message": _( @@ -68,7 +71,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'] + 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 4fe12fbf..0694390d 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', @@ -135,11 +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.DjangoModelPermissionsOrAnonReadOnly' + '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/settings/development.py b/note_kfet/settings/development.py index cf738f33..66ad4fd4 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -17,12 +17,24 @@ import os # https://docs.djangoproject.com/en/2.2/ref/settings/#databases from . import * -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +if os.getenv("DJANGO_DEV_STORE_METHOD", "sqllite") == "postgresql": + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': os.environ.get('DJANGO_DB_NAME', 'note_db'), + 'USER': os.environ.get('DJANGO_DB_USER', 'note'), + 'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'), + 'HOST': os.environ.get('DJANGO_DB_HOST', 'localhost'), + 'PORT': os.environ.get('DJANGO_DB_PORT', ''), # Use default port + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } } -} # Break it, fix it! DEBUG = True @@ -39,7 +51,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # EMAIL_HOST_USER = 'change_me' # EMAIL_HOST_PASSWORD = 'change_me' -SERVER_EMAIL = 'no-reply@example.org' +SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com") # Security settings SECURE_CONTENT_TYPE_NOSNIFF = False diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 4512dc85..5be8a3b8 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -1,6 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import os + ######################## # Production Settings # ######################## @@ -14,11 +16,11 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'note_db', - 'USER': 'note', - 'PASSWORD': 'update_in_env_variable', - 'HOST': '127.0.0.1', - 'PORT': '', + 'NAME': os.environ.get('DJANGO_DB_NAME', 'note_db'), + 'USER': os.environ.get('DJANGO_DB_USER', 'note'), + 'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'), + 'HOST': os.environ.get('DJANGO_DB_HOST', 'localhost'), + 'PORT': os.environ.get('DJANGO_DB_PORT', ''), # Use default port } } @@ -26,7 +28,9 @@ DATABASES = { DEBUG = True # Mandatory ! -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')] + +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS') # Emails EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -37,7 +41,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # EMAIL_HOST_USER = 'change_me' # EMAIL_HOST_PASSWORD = 'change_me' -SERVER_EMAIL = 'no-reply@example.org' +SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com") # Security settings SECURE_CONTENT_TYPE_NOSNIFF = False @@ -49,4 +53,4 @@ X_FRAME_OPTIONS = 'DENY' SESSION_COOKIE_AGE = 60 * 60 * 3 # CAS Client settings -CAS_SERVER_URL = "https://note.crans.org/cas/" +CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/" diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 896c0655..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) @@ -37,8 +36,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/base.html b/templates/base.html index dbe59c41..d57dab89 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,4 @@ -{% load static i18n pretty_money static %} +{% load static i18n pretty_money static getenv %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} @@ -75,6 +75,9 @@ SPDX-License-Identifier: GPL-3.0-or-later + @@ -125,7 +128,7 @@ SPDX-License-Identifier: GPL-3.0-or-later class="form-inline"> NoteKfet2020 — - Nous contacter {% csrf_token %} 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 %} diff --git a/templates/member/club_form.html b/templates/member/club_form.html index 3fc2dd8b..577297bb 100644 --- a/templates/member/club_form.html +++ b/templates/member/club_form.html @@ -1,11 +1,12 @@ {% extends "base.html" %} {% load static %} +{% load i18n %} {% load crispy_forms_tags %} {% block content %} -

Template Listing

+

{% trans "Clubs list" %}

{% csrf_token %} {{form|crispy}} - +
{% endblock %} diff --git a/templates/member/club_list.html b/templates/member/club_list.html index f807c25c..16571113 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -1,10 +1,11 @@ {% extends "base.html" %} {% load render_table from django_tables2 %} +{% load i18n %} {% block content %} {% render_table table %} -New Club +{% trans "New club" %} {% endblock %} {% block extrajavascript %} diff --git a/templates/member/signup.html b/templates/member/signup.html index e682bd9b..d7b3c23e 100644 --- a/templates/member/signup.html +++ b/templates/member/signup.html @@ -2,16 +2,16 @@ {% extends 'base.html' %} {% load crispy_forms_tags %} {% load i18n %} -{% block title %}Sign Up{% endblock %} +{% block title %}{% trans "Sign up" %}{% endblock %} {% block content %} -

Sign up

+

{% trans "Sign up" %}

{% csrf_token %} {{ form|crispy }} {{ profile_form|crispy }}
{% endblock %} diff --git a/templates/note/transactiontemplate_form.html b/templates/note/transactiontemplate_form.html index 3fc2dd8b..1f9a574a 100644 --- a/templates/note/transactiontemplate_form.html +++ b/templates/note/transactiontemplate_form.html @@ -1,8 +1,9 @@ {% extends "base.html" %} {% load static %} +{% load i18n %} {% load crispy_forms_tags %} {% block content %} -

Template Listing

+

{% trans "Buttons list" %}

{% csrf_token %} {{form|crispy}} 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 }} diff --git a/tox.ini b/tox.ini index 7c432d55..2217b6bf 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ deps = pep8-naming pyflakes commands = - flake8 apps/activity apps/api apps/member apps/note + flake8 apps/activity apps/api apps/logs apps/member apps/note [flake8] # Ignore too many errors, should be reduced in the future