diff --git a/.env_example b/.env_example new file mode 100644 index 0000000000000000000000000000000000000000..5aba0d14603ad3939f8bd439de4b49f57e29c016 --- /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/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..94cf1be69e8cbd2701c78623485f8f508ac64c9c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "apps/scripts"] + path = apps/scripts + url = git@gitlab.crans.org:bde/nk20-scripts.git diff --git a/Dockerfile b/Dockerfile index 2c840829ae86929bace6c9c68396c7bd808a5679..d42bdd1f479c7155f33160d75271f54c1bd5ee6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,13 @@ RUN apt update && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ rm -rf /var/lib/apt/lists/* -COPY requirements.txt /code/ -RUN pip install -r requirements.txt - 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 + ENTRYPOINT ["/code/entrypoint.sh"] EXPOSE 8000 diff --git a/README.md b/README.md index 5ae8a3967704fa1d128b6375f5990d51a3a943d4..91f2f17d48de7d9e4c1077fca7826318745ec7a4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n $ python3 -m venv env $ source env/bin/activate - (env)$ pip3 install -r requirements.txt + (env)$ pip3 install -r requirements/base.txt (env)$ deactivate 4. uwsgi et Nginx @@ -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é serveu : + 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 - -Ensuite on (re)bascule dans l'environement virtuel et on lance les 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 $ 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/admin.py b/apps/activity/admin.py index 5ceb4e8146bcf1c1392059117c34e2497d04c73a..0529d3064436cd00b2cf40b2ec6101f6e0e2bb27 100644 --- a/apps/activity/admin.py +++ b/apps/activity/admin.py @@ -11,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin): Admin customisation for Activity """ list_display = ('name', 'activity_type', 'organizer') - list_filter = ('activity_type', ) + list_filter = ('activity_type',) search_fields = ['name', 'organizer__name'] # Organize activities by start date diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py index 0b9302f17aa24b961be3f792a7afb7a87c6b5e8a..514515ef7a94c0081f39e5374aae42c1b86f6433 100644 --- a/apps/activity/api/serializers.py +++ b/apps/activity/api/serializers.py @@ -11,6 +11,7 @@ class ActivityTypeSerializer(serializers.ModelSerializer): REST API Serializer for Activity types. The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API. """ + class Meta: model = ActivityType fields = '__all__' @@ -21,6 +22,7 @@ class ActivitySerializer(serializers.ModelSerializer): REST API Serializer for Activities. The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API. """ + class Meta: model = Activity fields = '__all__' @@ -31,6 +33,7 @@ class GuestSerializer(serializers.ModelSerializer): REST API Serializer for Guests. The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API. """ + class Meta: model = Guest fields = '__all__' diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 5683d458011f3acba0e06bda1b0e6dc575ea513b..4ee2194d06c38cad2726feb8536c149248aa336b 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -1,10 +1,11 @@ # 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 ..models import ActivityType, Activity, Guest from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer +from ..models import ActivityType, Activity, Guest class ActivityTypeViewSet(viewsets.ModelViewSet): @@ -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 7e59a8c0acfc87ff928d4392fd06f46be41ac769..95ed5f99e9005850fed2822d3ef081bf5ec55b85 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): @@ -14,6 +18,7 @@ class UserSerializer(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 = User exclude = ( @@ -23,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. @@ -31,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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/logs/api/serializers.py b/apps/logs/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..c76e3a5d8d43c0f52ef17f14971d88f864cb6580 --- /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 0000000000000000000000000000000000000000..9a0ceaa8cec01b81f5bf5646bec374ab16199a42 --- /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 0000000000000000000000000000000000000000..2c47b7a2a5daf50ef5d56d1a487d1b9b4050a98c --- /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 f48820c7b91f2e9818aa179ce94ff2960d07884e..239f86cf45cd58326ae68e2349b0c1e6421dd307 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 0000000000000000000000000000000000000000..77f749b9da0663a4a3fd4e1b17aa8c8967fc5372 --- /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 337315bb1b215e840b581bcc157bb9f9cab0ac3b..10e2651f2f8475c4241c05a37f9c56b37c8d5bb5 100644 --- a/apps/logs/models.py +++ b/apps/logs/models.py @@ -1,16 +1,16 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext_lazy as _ from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models +from django.utils.translation import gettext_lazy as _ class Changelog(models.Model): """ - Store each modification on the database (except sessions and logging), + Store each modification in the database (except sessions and logging), including creating, editing and deleting models. """ @@ -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 13194e5b3b175029561488daf8f460cee35d8726..fb17157a9fc14f8b22dc99bab586b95b60e6f834 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -1,66 +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 .models import Changelog - +from rest_framework.renderers import JSONRenderer +from rest_framework.serializers import ModelSerializer -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 +import getpass - if not req: - print("WARNING: Attempt to save " + str(sender) + " with no user") +from note.models import NoteUser, Alias - return req +from .middlewares import get_current_authenticated_user, get_current_ip +from .models import Changelog -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', # Never remove this line + 'migrations.migration', + 'note.note' # We only store the subclasses + 'note.transaction', + 'sessions.session', +] -EXCLUDED = [ - 'admin.logentry', - 'authtoken.token', - 'cas_server.user', - 'cas_server.userattributes', - 'contenttypes.contenttype', - 'logs.changelog', - 'migrations.migration', - 'note.noteuser', - 'note.noteclub', - 'note.notespecial', - '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() @@ -68,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) - - from django.contrib.auth.models import AnonymousUser - if isinstance(user, AnonymousUser): - user = None + # 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 + # 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, @@ -104,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/member/admin.py b/apps/member/admin.py index 70b004594e4a20c60c63d076857ba5e51173d466..48fbc035054c22ed01e900381766570359fb5ead 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -18,9 +18,9 @@ class ProfileInline(admin.StackedInline): class CustomUserAdmin(UserAdmin): - inlines = (ProfileInline, ) + inlines = (ProfileInline,) list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_select_related = ('profile', ) + list_select_related = ('profile',) form = ProfileForm def get_inline_instances(self, request, obj=None): diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py index f4df67993dcfd3b4728d02d240d0e59aeb853a12..962841aebfac6d361c07dc87ebb61dcd0f253b53 100644 --- a/apps/member/api/serializers.py +++ b/apps/member/api/serializers.py @@ -11,6 +11,7 @@ class ProfileSerializer(serializers.ModelSerializer): REST API Serializer for Profiles. The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API. """ + class Meta: model = Profile fields = '__all__' @@ -21,6 +22,7 @@ class ClubSerializer(serializers.ModelSerializer): REST API Serializer for Clubs. The djangorestframework plugin will analyse the model `Club` and parse all fields in the API. """ + class Meta: model = Club fields = '__all__' @@ -31,6 +33,7 @@ class RoleSerializer(serializers.ModelSerializer): REST API Serializer for Roles. The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. """ + class Meta: model = Role fields = '__all__' @@ -41,6 +44,7 @@ class MembershipSerializer(serializers.ModelSerializer): REST API Serializer for Memberships. The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API. """ + class Meta: model = Membership fields = '__all__' diff --git a/apps/member/api/views.py b/apps/member/api/views.py index 79ba4c12afca3dee2c00445bd280d33b46934101..c85df90330ae00463306d30b6bec3da8c22abc97 100644 --- a/apps/member/api/views.py +++ b/apps/member/api/views.py @@ -2,9 +2,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later from rest_framework import viewsets +from rest_framework.filters import SearchFilter -from ..models import Profile, Club, Role, Membership from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer +from ..models import Profile, Club, Role, Membership class ProfileViewSet(viewsets.ModelViewSet): @@ -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/filters.py b/apps/member/filters.py index 418e52fc680fb7888574924eb06623176a03680c..951723e86b6d0fecd3c6c728836cef43331c42fc 100644 --- a/apps/member/filters.py +++ b/apps/member/filters.py @@ -1,11 +1,11 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django_filters import FilterSet, CharFilter -from django.contrib.auth.models import User -from django.db.models import CharField from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit +from django.contrib.auth.models import User +from django.db.models import CharField +from django_filters import FilterSet, CharFilter class UserFilter(FilterSet): diff --git a/apps/member/forms.py b/apps/member/forms.py index abb35cd9cb2d4493e849ffcfd9315a5f14230c4a..d2134cddfda74bf1825b4d672c2c0d921744a961 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -1,23 +1,22 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from crispy_forms.bootstrap import Div +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout from dal import autocomplete +from django import forms from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User -from django import forms from .models import Profile, Club, Membership -from crispy_forms.helper import FormHelper -from crispy_forms.bootstrap import Div -from crispy_forms.layout import Layout - class SignUpForm(UserCreationForm): - def __init__(self,*args,**kwargs): - super().__init__(*args,**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.fields['username'].widget.attrs.pop("autofocus", None) - self.fields['first_name'].widget.attrs.update({"autofocus":"autofocus"}) + self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"}) class Meta: model = User @@ -28,6 +27,7 @@ class ProfileForm(forms.ModelForm): """ A form for the extras field provided by the :model:`member.Profile` model. """ + class Meta: model = Profile fields = '__all__' @@ -42,7 +42,7 @@ class ClubForm(forms.ModelForm): class AddMembersForm(forms.Form): class Meta: - fields = ('', ) + fields = ('',) class MembershipForm(forms.ModelForm): @@ -54,13 +54,13 @@ class MembershipForm(forms.ModelForm): # et récupère les noms d'utilisateur valides widgets = { 'user': - autocomplete.ModelSelect2( - url='member:user_autocomplete', - attrs={ - 'data-placeholder': 'Nom ...', - 'data-minimum-input-length': 1, - }, - ), + autocomplete.ModelSelect2( + url='member:user_autocomplete', + attrs={ + 'data-placeholder': 'Nom ...', + 'data-minimum-input-length': 1, + }, + ), } diff --git a/apps/member/models.py b/apps/member/models.py index 1ca82af0c7525a893f5df325e67349a78211e767..24e58830f6772249ae5481a1481358aea8dc920c 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -5,8 +5,8 @@ import datetime from django.conf import settings from django.db import models -from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ class Profile(models.Model): @@ -48,9 +48,10 @@ class Profile(models.Model): class Meta: verbose_name = _('user profile') verbose_name_plural = _('user profile') + indexes = [models.Index(fields=['user'])] def get_absolute_url(self): - return reverse('user_detail', args=(self.pk, )) + return reverse('user_detail', args=(self.pk,)) @@ -100,7 +101,7 @@ class Club(models.Model): return self.name def get_absolute_url(self): - return reverse_lazy('member:club_detail', args=(self.pk, )) + return reverse_lazy('member:club_detail', args=(self.pk,)) class Role(models.Model): @@ -161,7 +162,7 @@ class Membership(models.Model): class Meta: verbose_name = _('membership') verbose_name_plural = _('memberships') - + indexes = [models.Index(fields=['user'])] class RolePermissions(models.Model): """ diff --git a/apps/member/signals.py b/apps/member/signals.py index b17b3ae84dfdee1105204a863f1df53ed64c8cc1..2b03e3ced7fac6f67efaa9f509d978c0df8e7987 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -1,6 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later + def save_user_profile(instance, created, raw, **_kwargs): """ Hook to create and save a profile when an user is updated if it is not registered with the signup form diff --git a/apps/member/views.py b/apps/member/views.py index 870079cc4a387d2b61d4795512f8e8b731fcc657..dacfde3331439473a9182abf49bf01bb6ffc24a2 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -1,33 +1,33 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import io + +from PIL import Image +from dal import autocomplete +from django.conf import settings +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.http import HttpResponseRedirect from django.shortcuts import redirect +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView, UpdateView, TemplateView,DeleteView +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView, DeleteView from django.views.generic.edit import FormMixin -from django.contrib.auth.models import User -from django.contrib import messages -from django.urls import reverse_lazy -from django.http import HttpResponseRedirect -from django.db.models import Q -from django.core.exceptions import ValidationError -from django.conf import settings from django_tables2.views import SingleTableView from rest_framework.authtoken.models import Token -from dal import autocomplete -from PIL import Image -import io - +from note.forms import AliasForm, ImageForm from note.models import Alias, NoteUser from note.models.transactions import Transaction from note.tables import HistoryTable, AliasTable -from note.forms import AliasForm, ImageForm -from .models import Profile, Club, Membership +from .filters import UserFilter, UserFilterFormHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper +from .models import Club, Membership from .tables import ClubTable, UserTable -from .filters import UserFilter, UserFilterFormHelper class UserCreateView(CreateView): @@ -49,10 +49,10 @@ class UserCreateView(CreateView): def form_valid(self, form): profile_form = ProfileForm(self.request.POST) if form.is_valid() and profile_form.is_valid(): - user = form.save() - profile = profile_form.save(commit=False) - profile.user = user - profile.save() + user = form.save(commit=False) + user.profile = profile_form.save(commit=False) + user.save() + user.profile.save() return super().form_valid(form) @@ -109,7 +109,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): return reverse_lazy('member:user_detail', kwargs={'pk': kwargs['id']}) else: - return reverse_lazy('member:user_detail', args=(self.object.id, )) + return reverse_lazy('member:user_detail', args=(self.object.id,)) class UserDetailView(LoginRequiredMixin, DetailView): @@ -124,7 +124,7 @@ class UserDetailView(LoginRequiredMixin, DetailView): context = super().get_context_data(**kwargs) user = context['user_object'] history_list = \ - Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)) + Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id") context['history_list'] = HistoryTable(history_list) club_list = \ Membership.objects.all().filter(user=user).only("club") @@ -157,13 +157,14 @@ class UserListView(LoginRequiredMixin, SingleTableView): context["filter"] = self.filter return context -class AliasView(LoginRequiredMixin,FormMixin,DetailView): + +class AliasView(LoginRequiredMixin, FormMixin, DetailView): model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' form_class = AliasForm - def get_context_data(self,**kwargs): + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) note = context['user_object'].note context["aliases"] = AliasTable(note.alias_set.all()) @@ -172,7 +173,7 @@ class AliasView(LoginRequiredMixin,FormMixin,DetailView): def get_success_url(self): return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id}) - def post(self,request,*args,**kwargs): + def post(self, request, *args, **kwargs): self.object = self.get_object() form = self.get_form() if form.is_valid(): @@ -186,42 +187,45 @@ class AliasView(LoginRequiredMixin,FormMixin,DetailView): alias.save() return super().form_valid(form) + class DeleteAliasView(LoginRequiredMixin, DeleteView): model = Alias - def delete(self,request,*args,**kwargs): + def delete(self, request, *args, **kwargs): try: self.object = self.get_object() self.object.delete() except ValidationError as e: # TODO: pass message to redirected view. - messages.error(self.request,str(e)) + messages.error(self.request, str(e)) else: - messages.success(self.request,_("Alias successfully deleted")) + messages.success(self.request, _("Alias successfully deleted")) return HttpResponseRedirect(self.get_success_url()) - + def get_success_url(self): print(self.request) - return reverse_lazy('member:user_alias',kwargs={'pk':self.object.note.user.pk}) + return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk}) def get(self, request, *args, **kwargs): return self.post(request, *args, **kwargs) + class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): model = User template_name = 'member/profile_picture_update.html' context_object_name = 'user_object' form_class = ImageForm - def get_context_data(self,*args,**kwargs): - context = super().get_context_data(*args,**kwargs) - context['form'] = self.form_class(self.request.POST,self.request.FILES) + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context['form'] = self.form_class(self.request.POST, self.request.FILES) return context - + def get_success_url(self): return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id}) - def post(self,request,*args,**kwargs): - form = self.get_form() + def post(self, request, *args, **kwargs): + form = self.get_form() self.object = self.get_object() if form.is_valid(): return self.form_valid(form) @@ -230,7 +234,7 @@ class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): print(form) return self.form_invalid(form) - def form_valid(self,form): + def form_valid(self, form): image_field = form.cleaned_data['image'] x = form.cleaned_data['x'] y = form.cleaned_data['y'] @@ -238,23 +242,24 @@ class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): h = form.cleaned_data['height'] # image crop and resize image_file = io.BytesIO(image_field.read()) - ext = image_field.name.split('.')[-1] + # ext = image_field.name.split('.')[-1].lower() + # TODO: support GIF format image = Image.open(image_file) - image = image.crop((x, y, x+w, y+h)) + image = image.crop((x, y, x + w, y + h)) image_clean = image.resize((settings.PIC_WIDTH, - settings.PIC_RATIO*settings.PIC_WIDTH), - Image.ANTIALIAS) + settings.PIC_RATIO * settings.PIC_WIDTH), + Image.ANTIALIAS) image_file = io.BytesIO() - image_clean.save(image_file,ext) + image_clean.save(image_file, "PNG") image_field.file = image_file # renaming - filename = "{}_pic.{}".format(self.object.note.pk, ext) + filename = "{}_pic.png".format(self.object.note.pk) image_field.name = filename self.object.note.display_image = image_field self.object.note.save() return super().form_valid(form) - + class ManageAuthTokens(LoginRequiredMixin, TemplateView): """ Affiche le jeton d'authentification, et permet de le regénérer @@ -282,6 +287,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView): """ Auto complete users by usernames """ + def get_queryset(self): """ Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion. @@ -294,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 @@ -330,7 +336,7 @@ class ClubDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = context["club"] - club_transactions = \ + club_transactions = \ Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) context['history_list'] = HistoryTable(club_transactions) club_member = \ diff --git a/apps/note/admin.py b/apps/note/admin.py index 52c1cc1752066221c27782a303e975e497d278cf..a09286412591248b9d2fefb27f7715f70b7dbe4d 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -47,11 +47,11 @@ class NoteClubAdmin(PolymorphicChildModelAdmin): """ Child for a club note, see NoteAdmin """ - inlines = (AliasInlines, ) + inlines = (AliasInlines,) # We can't change club after creation or the balance readonly_fields = ('club', 'balance') - search_fields = ('club', ) + search_fields = ('club',) def has_add_permission(self, request): """ @@ -71,7 +71,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin): """ Child for a special note, see NoteAdmin """ - readonly_fields = ('balance', ) + readonly_fields = ('balance',) @admin.register(NoteUser) @@ -79,7 +79,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin): """ Child for an user note, see NoteAdmin """ - inlines = (AliasInlines, ) + inlines = (AliasInlines,) # We can't change user after creation or the balance readonly_fields = ('user', 'balance') @@ -133,7 +133,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin): Else the amount of money would not be transferred """ if obj: # user is editing an existing object - return 'created_at', 'source', 'destination', 'quantity',\ + return 'created_at', 'source', 'destination', 'quantity', \ 'amount' return [] @@ -143,9 +143,9 @@ class TransactionTemplateAdmin(admin.ModelAdmin): """ Admin customisation for TransactionTemplate """ - list_display = ('name', 'poly_destination', 'amount', 'category', 'display', ) + list_display = ('name', 'poly_destination', 'amount', 'category', 'display',) list_filter = ('category', 'display') - autocomplete_fields = ('destination', ) + autocomplete_fields = ('destination',) def poly_destination(self, obj): """ @@ -161,5 +161,5 @@ class TemplateCategoryAdmin(admin.ModelAdmin): """ Admin customisation for TransactionTemplate """ - list_display = ('name', ) - list_filter = ('name', ) + list_display = ('name',) + list_filter = ('name',) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index db0e35318ac4ca71b5902a22231ba11d098bd790..85f500ed6a1bd5c98fd12d8779ab01e2d73ac17f 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, SpecialTransaction class NoteSerializer(serializers.ModelSerializer): @@ -13,15 +14,10 @@ class NoteSerializer(serializers.ModelSerializer): REST API Serializer for Notes. The djangorestframework plugin will analyse the model `Note` and parse all fields in the API. """ + class Meta: model = Note fields = '__all__' - extra_kwargs = { - 'url': { - 'view_name': 'project-detail', - 'lookup_field': 'pk' - }, - } class NoteClubSerializer(serializers.ModelSerializer): @@ -29,40 +25,60 @@ class NoteClubSerializer(serializers.ModelSerializer): REST API Serializer for Club's notes. The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API. """ + name = serializers.SerializerMethodField() + class Meta: model = NoteClub fields = '__all__' + def get_name(self, obj): + return str(obj) + class NoteSpecialSerializer(serializers.ModelSerializer): """ REST API Serializer for special notes. The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API. """ + name = serializers.SerializerMethodField() + class Meta: model = NoteSpecial fields = '__all__' + def get_name(self, obj): + return str(obj) + class NoteUserSerializer(serializers.ModelSerializer): """ REST API Serializer for User's notes. The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API. """ + name = serializers.SerializerMethodField() + class Meta: model = NoteUser fields = '__all__' + def get_name(self, obj): + return str(obj) + class AliasSerializer(serializers.ModelSerializer): """ REST API Serializer for Aliases. The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. """ + note = serializers.SerializerMethodField() + class Meta: model = Alias fields = '__all__' + def get_note(self, alias): + return NotePolymorphicSerializer().to_representation(alias.note) + class NotePolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { @@ -73,11 +89,23 @@ 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. The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API. """ + class Meta: model = TransactionTemplate fields = '__all__' @@ -88,16 +116,49 @@ class TransactionSerializer(serializers.ModelSerializer): REST API Serializer for Transactions. The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API. """ + class Meta: model = Transaction 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. The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API. """ + class Meta: model = MembershipTransaction fields = '__all__' + + +class SpecialTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Special transactions. + The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API. + """ + + class Meta: + model = SpecialTransaction + fields = '__all__' + + +class TransactionPolymorphicSerializer(PolymorphicSerializer): + model_serializer_mapping = { + Transaction: TransactionSerializer, + TemplateTransaction: TemplateTransactionSerializer, + MembershipTransaction: MembershipTransactionSerializer, + SpecialTransaction: SpecialTransactionSerializer, + } diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 54218796211fb21173c9501b38f5cacff85f65dd..796a397f746aefd02c2e97b9aaf73bb25454f65a 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 94b4a47a287eec3d481ed3f5dc9d48f8a6cbb535..29c79bd8ba2b09d32344f5f6111470da26d62cc2 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 OrderingFilter, SearchFilter -from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction 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, TemplateCategory class NoteViewSet(viewsets.ModelViewSet): @@ -59,6 +61,9 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): """ queryset = Note.objects.all() serializer_class = NotePolymorphicSerializer + filter_backends = [SearchFilter, OrderingFilter] + search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ] + ordering_fields = ['alias__name', 'alias__normalized_name'] def get_queryset(self): """ @@ -69,8 +74,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: @@ -80,12 +85,11 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): elif "club" in types: queryset = queryset.filter(polymorphic_ctype__model="noteclub") elif "special" in types: - queryset = queryset.filter( - polymorphic_ctype__model="notespecial") + queryset = queryset.filter(polymorphic_ctype__model="notespecial") else: queryset = queryset.none() - return queryset + return queryset.distinct() class AliasViewSet(viewsets.ModelViewSet): @@ -96,6 +100,9 @@ class AliasViewSet(viewsets.ModelViewSet): """ queryset = Alias.objects.all() serializer_class = AliasSerializer + filter_backends = [SearchFilter, OrderingFilter] + search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] + ordering_fields = ['name', 'normalized_name'] def get_queryset(self): """ @@ -107,7 +114,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 +138,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 +158,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 +169,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 c0e92bda3aa9c6d2d7be0193b3b70b0df3300c0b..3654fa2f5f24c17498de4452295d1a899d798f1b 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": 22, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -14,7 +14,7 @@ "model": "note.note", "pk": 2, "fields": { - "polymorphic_ctype": 22, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -25,7 +25,7 @@ "model": "note.note", "pk": 3, "fields": { - "polymorphic_ctype": 22, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -36,7 +36,7 @@ "model": "note.note", "pk": 4, "fields": { - "polymorphic_ctype": 22, + "polymorphic_ctype": 40, "balance": 0, "is_active": true, "display_image": "", @@ -47,7 +47,7 @@ "model": "note.note", "pk": 5, "fields": { - "polymorphic_ctype": 21, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", @@ -58,7 +58,7 @@ "model": "note.note", "pk": 6, "fields": { - "polymorphic_ctype": 21, + "polymorphic_ctype": 39, "balance": 0, "is_active": true, "display_image": "", diff --git a/apps/note/forms.py b/apps/note/forms.py index 819ed97a45aa2654646c666c7d767f318a951a10..ac6adaaf5d10a0f8157182062f8afd5d60412c5e 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -3,31 +3,25 @@ from dal import autocomplete from django import forms -from django.conf import settings from django.utils.translation import gettext_lazy as _ -import os +from .models import Alias +from .models import TransactionTemplate -from crispy_forms.helper import FormHelper -from crispy_forms.bootstrap import Div -from crispy_forms.layout import Layout, HTML - -from .models import Transaction, TransactionTemplate, TemplateTransaction -from .models import Note, Alias class AliasForm(forms.ModelForm): class Meta: model = Alias fields = ("name",) - def __init__(self,*args,**kwargs): - super().__init__(*args,**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.fields["name"].label = False - self.fields["name"].widget.attrs={"placeholder":_('New Alias')} - + self.fields["name"].widget.attrs = {"placeholder": _('New Alias')} + class ImageForm(forms.Form): - image = forms.ImageField(required = False, + image = forms.ImageField(required=False, label=_('select an image'), help_text=_('Maximal size: 2MB')) x = forms.FloatField(widget=forms.HiddenInput()) @@ -35,7 +29,7 @@ class ImageForm(forms.Form): width = forms.FloatField(widget=forms.HiddenInput()) height = forms.FloatField(widget=forms.HiddenInput()) - + class TransactionTemplateForm(forms.ModelForm): class Meta: model = TransactionTemplate @@ -48,92 +42,11 @@ class TransactionTemplateForm(forms.ModelForm): # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} widgets = { 'destination': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), - } - - -class TransactionForm(forms.ModelForm): - def save(self, commit=True): - super().save(commit) - - - def clean(self): - """ - If the user has no right to transfer funds, then it will be the source of the transfer by default. - Transactions between a note and the same note are not authorized. - """ - - cleaned_data = super().clean() - if not "source" in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds" - cleaned_data["source"] = self.user.note - - if cleaned_data["source"].pk == cleaned_data["destination"].pk: - self.add_error("destination", _("Source and destination must be different.")) - - return cleaned_data - - - class Meta: - model = Transaction - fields = ( - 'source', - 'destination', - 'reason', - 'amount', - ) - - # Voir ci-dessus - widgets = { - 'source': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), - 'destination': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), - } - - -class ConsoForm(forms.ModelForm): - def save(self, commit=True): - button: TransactionTemplate = TransactionTemplate.objects.filter( - name=self.data['button']).get() - self.instance.destination = button.destination - self.instance.amount = button.amount - self.instance.reason = '{} ({})'.format(button.name, button.category) - self.instance.name = button.name - self.instance.category = button.category - super().save(commit) - - class Meta: - model = TemplateTransaction - fields = ('source', ) - - # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. - # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion - # et récupère les aliases de note valides - widgets = { - 'source': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), + autocomplete.ModelSelect2( + url='note:note_autocomplete', + attrs={ + 'data-placeholder': 'Note ...', + 'data-minimum-input-length': 1, + }, + ), } diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 4b06c93adaba324d9faaf40825ea0ded8add3db4..b6b00aa8ec91fedda1449a20948625f4ea8256e1 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -9,6 +9,7 @@ from django.core.validators import RegexValidator from django.db import models from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel + """ Defines each note types """ @@ -27,7 +28,7 @@ class Note(PolymorphicModel): help_text=_('in centimes, money credited for this instance'), default=0, ) - last_negative= models.DateTimeField( + last_negative = models.DateTimeField( verbose_name=_('last negative date'), help_text=_('last time the balance was negative'), null=True, @@ -98,7 +99,7 @@ class Note(PolymorphicModel): # Alias exists, so check if it is linked to this note if aliases.first().note != self: raise ValidationError(_('This alias is already taken.'), - code="same_alias",) + code="same_alias", ) else: # Alias does not exist yet, so check if it can exist a = Alias(name=str(self)) @@ -208,6 +209,10 @@ class Alias(models.Model): class Meta: verbose_name = _("alias") verbose_name_plural = _("aliases") + indexes = [ + models.Index(fields=['name']), + models.Index(fields=['normalized_name']), + ] def __str__(self): return self.name @@ -230,13 +235,13 @@ 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)), - code="same_alias" - ) + raise ValidationError(_('An alias with a similar name already exists: {} ').format(sim_alias), + code="same_alias" + ) except Alias.DoesNotExist: pass self.normalized_name = normalized_name - + def delete(self, using=None, keep_parents=False): if self.name == str(self.note): raise ValidationError(_("You can't delete your main alias."), diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 598c119bc5e441413aaeefd2b5ddda4a004721b9..86c0073749fa3ad7e07deb0c20a07daa79d61bea 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -2,12 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models +from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from django.urls import reverse from polymorphic.models import PolymorphicModel -from .notes import Note, NoteClub +from .notes import Note, NoteClub, NoteSpecial """ Defines transactions @@ -44,7 +44,7 @@ class TransactionTemplate(models.Model): verbose_name=_('name'), max_length=255, unique=True, - error_messages={'unique':_("A template with this name already exist")}, + error_messages={'unique': _("A template with this name already exist")}, ) destination = models.ForeignKey( NoteClub, @@ -63,11 +63,12 @@ class TransactionTemplate(models.Model): max_length=31, ) display = models.BooleanField( - default = True, + default=True, ) description = models.CharField( verbose_name=_('description'), max_length=255, + blank=True, ) class Meta: @@ -75,7 +76,7 @@ class TransactionTemplate(models.Model): verbose_name_plural = _("transaction templates") def get_absolute_url(self): - return reverse('note:template_update', args=(self.pk, )) + return reverse('note:template_update', args=(self.pk,)) class Transaction(PolymorphicModel): @@ -106,7 +107,10 @@ class Transaction(PolymorphicModel): verbose_name=_('quantity'), default=1, ) - amount = models.PositiveIntegerField(verbose_name=_('amount'), ) + amount = models.PositiveIntegerField( + verbose_name=_('amount'), + ) + reason = models.CharField( verbose_name=_('reason'), max_length=255, @@ -119,6 +123,11 @@ class Transaction(PolymorphicModel): class Meta: verbose_name = _("transaction") verbose_name_plural = _("transactions") + indexes = [ + models.Index(fields=['created_at']), + models.Index(fields=['source']), + models.Index(fields=['destination']), + ] def save(self, *args, **kwargs): """ @@ -127,6 +136,7 @@ class Transaction(PolymorphicModel): if self.source.pk == self.destination.pk: # When source == destination, no money is transfered + super().save(*args, **kwargs) return created = self.pk is None @@ -151,11 +161,14 @@ class Transaction(PolymorphicModel): def total(self): return self.amount * self.quantity + @property + def type(self): + return _('Transfer') + class TemplateTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. - """ template = models.ForeignKey( @@ -168,6 +181,37 @@ class TemplateTransaction(Transaction): on_delete=models.PROTECT, ) + @property + def type(self): + return _('Template') + + +class SpecialTransaction(Transaction): + """ + Special type of :model:`note.Transaction` associated to transactions with special notes + """ + + last_name = models.CharField( + max_length=255, + verbose_name=_("name"), + ) + + first_name = models.CharField( + max_length=255, + verbose_name=_("first_name"), + ) + + bank = models.CharField( + max_length=255, + verbose_name=_("bank"), + blank=True, + ) + + @property + def type(self): + return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit") + + class MembershipTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. @@ -183,3 +227,7 @@ class MembershipTransaction(Transaction): class Meta: verbose_name = _("membership transaction") verbose_name_plural = _("membership transactions") + + @property + def type(self): + return _('membership transaction') diff --git a/apps/note/tables.py b/apps/note/tables.py index 20476cb664460502b4daf1e500b22d29f7e015df..b9dac051f7bc902a232479b7bcef8fbd9c9cbb74 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -1,45 +1,77 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import html + import django_tables2 as tables from django.db.models import F from django_tables2.utils import A -from .models.transactions import Transaction +from django.utils.translation import gettext_lazy as _ + from .models.notes import Alias +from .models.transactions import Transaction +from .templatetags.pretty_money import pretty_money + class HistoryTable(tables.Table): class Meta: attrs = { 'class': - 'table table-condensed table-striped table-hover' + 'table table-condensed table-striped table-hover' } model = Transaction + exclude = ("id", "polymorphic_ctype", ) template_name = 'django_tables2/bootstrap4.html' - sequence = ('...', 'total', 'valid') + sequence = ('...', 'type', 'total', 'valid', ) + orderable = False + + type = tables.Column() total = tables.Column() # will use Transaction.total() !! + valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id), + "class": lambda record: str(record.valid).lower() + ' validate', + "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + + str(record.valid).lower() + ')'}}) + def order_total(self, queryset, is_descending): # needed for rendering queryset = queryset.annotate(total=F('amount') * F('quantity')) \ .order_by(('-' if is_descending else '') + 'total') - return (queryset, True) + return queryset, True + + def render_amount(self, value): + return pretty_money(value) + + def render_total(self, value): + return pretty_money(value) + + def render_type(self, value): + return _(value) + + # Django-tables escape strings. That's a wrong thing. + def render_reason(self, value): + return html.unescape(value) + + def render_valid(self, value): + return "✔" if value else "✖" + class AliasTable(tables.Table): class Meta: attrs = { 'class': - 'table table condensed table-striped table-hover' + 'table table condensed table-striped table-hover' } model = Alias - fields =('name',) + fields = ('name',) template_name = 'django_tables2/bootstrap4.html' show_header = False - name = tables.Column(attrs={'td':{'class':'text-center'}}) + name = tables.Column(attrs={'td': {'class': 'text-center'}}) delete = tables.LinkColumn('member:user_alias_delete', args=[A('pk')], attrs={ - 'td': {'class':'col-sm-2'}, - 'a': {'class': 'btn btn-danger'} }, - text='delete',accessor='pk') + 'td': {'class': 'col-sm-2'}, + 'a': {'class': 'btn btn-danger'}}, + text='delete', accessor='pk') diff --git a/apps/note/templatetags/getenv.py b/apps/note/templatetags/getenv.py new file mode 100644 index 0000000000000000000000000000000000000000..c133cb8ff4df0a1bfe11ec7726d604907ecde3d4 --- /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/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index 12530c6e22d4ea0eb86b46d018ffbcddb0c44f8c..265870a85aadd70d949f13cdeb990db5c0955a18 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -11,7 +11,7 @@ def pretty_money(value): abs(value) // 100, ) else: - return "{:s}{:d} € {:02d}".format( + return "{:s}{:d}.{:02d} €".format( "- " if value < 0 else "", abs(value) // 100, abs(value) % 100, diff --git a/apps/note/views.py b/apps/note/views.py index 5038df164c02555fa4b7083953537f38eaac1fdd..31a79be7c992c8a70899d5b6f4d2d19480fdc1da 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -3,56 +3,49 @@ from dal import autocomplete from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, ListView, UpdateView +from django_tables2 import SingleTableView -from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction -from .forms import TransactionForm, TransactionTemplateForm, ConsoForm +from .forms import TransactionTemplateForm +from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial +from .models.transactions import SpecialTransaction +from .tables import HistoryTable -class TransactionCreate(LoginRequiredMixin, CreateView): +class TransactionCreate(LoginRequiredMixin, SingleTableView): """ Show transfer page TODO: If user have sufficient rights, they can transfer from an other note """ - model = Transaction - form_class = TransactionForm + queryset = Transaction.objects.order_by("-id").all()[:50] + template_name = "note/transaction_form.html" + + # Transaction history table + table_class = HistoryTable + table_pagination = {"per_page": 50} def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['title'] = _('Transfer money from your account ' - 'to one or others') - - context['no_cache'] = True + context['title'] = _('Transfer money') + context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk + context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk + context['special_types'] = NoteSpecial.objects.order_by("special_type").all() return context - def get_form(self, form_class=None): - """ - If the user has no right to transfer funds, then it won't have the choice of the source of the transfer. - """ - form = super().get_form(form_class) - - if False: # TODO: fix it with "if %user has no right to transfer funds" - del form.fields['source'] - form.user = self.request.user - - return form - - def get_success_url(self): - return reverse('note:transfer') - class NoteAutocomplete(autocomplete.Select2QuerySetView): """ Auto complete note by aliases """ + def get_queryset(self): """ Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. @@ -66,7 +59,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) @@ -120,31 +113,31 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): form_class = TransactionTemplateForm -class ConsoView(LoginRequiredMixin, CreateView): +class ConsoView(LoginRequiredMixin, SingleTableView): """ Consume """ - model = TemplateTransaction + queryset = Transaction.objects.order_by("-id").all()[:50] template_name = "note/conso_form.html" - form_class = ConsoForm + + # Transaction history table + table_class = HistoryTable + table_pagination = {"per_page": 50} def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \ - .order_by('category') - context['title'] = _("Consommations") + from django.db.models import Count + buttons = TransactionTemplate.objects.filter(display=True) \ + .annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') + context['transaction_templates'] = buttons + context['most_used'] = buttons.order_by('-clicks', 'name')[:10] + context['title'] = _("Consumptions") + context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk # select2 compatibility context['no_cache'] = True return context - - def get_success_url(self): - """ - When clicking a button, reload the same page - """ - return reverse('note:consos') - diff --git a/entrypoint.sh b/entrypoint.sh index f05e962ae116b4831ea506704b828ab9c5988748..e5a22a5a53f2f25df533f9370a89f7e77205cb93 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 386db34c4f0fda73222f1ac61f9743c07c9265e8..e61efb2a6f2e68626aefb660983d2931fba657ac 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-02-27 17:39+0100\n" +"POT-Creation-Date: 2020-03-16 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -23,9 +23,10 @@ msgid "activity" 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:184 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11 +#: apps/member/models.py:61 apps/member/models.py:112 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: templates/member/profile_detail.html:15 msgid "name" msgstr "" @@ -49,8 +50,8 @@ msgstr "" msgid "description" msgstr "" -#: apps/activity/models.py:54 apps/note/models/notes.py:160 -#: apps/note/models/transactions.py:62 +#: apps/activity/models.py:54 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 msgid "type" msgstr "" @@ -86,43 +87,59 @@ msgstr "" msgid "API" msgstr "" -#: apps/logs/apps.py:10 +#: apps/logs/apps.py:11 msgid "Logs" msgstr "" -#: apps/logs/models.py:20 apps/note/models/notes.py:105 +#: apps/logs/models.py:21 apps/note/models/notes.py:117 msgid "user" msgstr "" #: apps/logs/models.py:27 +msgid "IP Address" +msgstr "" + +#: apps/logs/models.py:35 msgid "model" msgstr "" -#: apps/logs/models.py:34 +#: apps/logs/models.py:42 msgid "identifier" msgstr "" -#: apps/logs/models.py:39 +#: apps/logs/models.py:47 msgid "previous data" msgstr "" -#: apps/logs/models.py:44 +#: apps/logs/models.py:52 msgid "new data" msgstr "" -#: apps/logs/models.py:51 +#: 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:59 +#: apps/logs/models.py:73 msgid "timestamp" msgstr "" -#: apps/logs/models.py:63 +#: apps/logs/models.py:77 msgid "Logs cannot be destroyed." msgstr "" -#: apps/member/apps.py:10 +#: apps/member/apps.py:14 msgid "member" msgstr "" @@ -130,7 +147,7 @@ msgstr "" msgid "phone number" msgstr "" -#: apps/member/models.py:29 templates/member/profile_detail.html:24 +#: apps/member/models.py:29 templates/member/profile_detail.html:28 msgid "section" msgstr "" @@ -138,7 +155,7 @@ msgstr "" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:36 templates/member/profile_detail.html:27 +#: apps/member/models.py:36 templates/member/profile_detail.html:31 msgid "address" msgstr "" @@ -150,199 +167,207 @@ msgstr "" msgid "user profile" msgstr "" -#: apps/member/models.py:65 +#: apps/member/models.py:66 msgid "email" msgstr "" -#: apps/member/models.py:70 +#: apps/member/models.py:71 msgid "membership fee" msgstr "" -#: apps/member/models.py:74 +#: apps/member/models.py:75 msgid "membership duration" msgstr "" -#: apps/member/models.py:75 +#: apps/member/models.py:76 msgid "The longest time a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:80 +#: apps/member/models.py:81 msgid "membership start" msgstr "" -#: apps/member/models.py:81 +#: apps/member/models.py:82 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:86 +#: apps/member/models.py:87 msgid "membership end" msgstr "" -#: apps/member/models.py:87 +#: apps/member/models.py:88 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." msgstr "" -#: apps/member/models.py:93 apps/note/models/notes.py:135 +#: apps/member/models.py:94 apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:94 +#: apps/member/models.py:95 msgid "clubs" msgstr "" -#: apps/member/models.py:117 +#: apps/member/models.py:118 msgid "role" msgstr "" -#: apps/member/models.py:118 +#: apps/member/models.py:119 msgid "roles" msgstr "" -#: apps/member/models.py:142 +#: apps/member/models.py:143 msgid "membership starts on" msgstr "" -#: apps/member/models.py:145 +#: apps/member/models.py:146 msgid "membership ends on" msgstr "" -#: apps/member/models.py:149 +#: apps/member/models.py:150 msgid "fee" msgstr "" -#: apps/member/models.py:153 +#: apps/member/models.py:154 msgid "membership" msgstr "" -#: apps/member/models.py:154 +#: apps/member/models.py:155 msgid "memberships" msgstr "" -#: apps/member/views.py:63 templates/member/profile_detail.html:42 +#: apps/member/views.py:69 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "" -#: apps/member/views.py:79 +#: apps/member/views.py:82 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:130 +#: apps/member/views.py:132 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:93 +#: apps/member/views.py:202 +msgid "Alias successfully deleted" +msgstr "" + +#: apps/note/admin.py:120 apps/note/models/transactions.py:94 msgid "source" msgstr "" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 msgid "destination" msgstr "" -#: apps/note/apps.py:14 apps/note/models/notes.py:54 +#: apps/note/apps.py:14 apps/note/models/notes.py:58 msgid "note" msgstr "" -#: apps/note/forms.py:49 -msgid "Source and destination must be different." +#: apps/note/forms.py:20 +msgid "New Alias" msgstr "" -#: apps/note/models/notes.py:26 -msgid "account balance" +#: apps/note/forms.py:25 +msgid "select an image" +msgstr "" + +#: apps/note/forms.py:26 +msgid "Maximal size: 2MB" msgstr "" #: apps/note/models/notes.py:27 +msgid "account balance" +msgstr "" + +#: 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:49 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 msgid "created at" msgstr "" -#: apps/note/models/notes.py:55 +#: apps/note/models/notes.py:59 msgid "notes" msgstr "" -#: apps/note/models/notes.py:63 +#: apps/note/models/notes.py:67 msgid "Note" msgstr "" -#: apps/note/models/notes.py:73 apps/note/models/notes.py:97 +#: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "" -#: apps/note/models/notes.py:113 -msgid "user" -msgstr "" - -#: apps/note/models/notes.py:117 +#: apps/note/models/notes.py:121 msgid "one's note" msgstr "" -#: apps/note/models/notes.py:118 +#: apps/note/models/notes.py:122 msgid "users note" msgstr "" -#: apps/note/models/notes.py:124 +#: apps/note/models/notes.py:128 #, python-format msgid "%(user)s's note" msgstr "" -#: apps/note/models/notes.py:139 +#: apps/note/models/notes.py:143 msgid "club note" msgstr "" -#: apps/note/models/notes.py:140 +#: apps/note/models/notes.py:144 msgid "clubs notes" msgstr "" -#: apps/note/models/notes.py:146 +#: apps/note/models/notes.py:150 #, python-format msgid "Note of %(club)s club" msgstr "" -#: apps/note/models/notes.py:166 +#: apps/note/models/notes.py:170 msgid "special note" msgstr "" -#: apps/note/models/notes.py:167 +#: apps/note/models/notes.py:171 msgid "special notes" msgstr "" -#: apps/note/models/notes.py:190 +#: apps/note/models/notes.py:194 msgid "Invalid alias" msgstr "" -#: apps/note/models/notes.py:206 +#: apps/note/models/notes.py:210 msgid "alias" msgstr "" -#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33 +#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 msgid "aliases" msgstr "" @@ -351,10 +376,10 @@ msgid "Alias is too long." msgstr "" #: apps/note/models/notes.py:238 -msgid "An alias with a similar name already exists:" +msgid "An alias with a similar name already exists: {} " msgstr "" -#: apps/note/models/notes.py:246 +#: apps/note/models/notes.py:247 msgid "You can't delete your main alias." msgstr "" @@ -370,7 +395,7 @@ msgstr "" msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 msgid "amount" msgstr "" @@ -378,59 +403,96 @@ msgstr "" msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:74 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:106 +#: apps/note/models/transactions.py:107 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:111 +#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "" + +#: apps/note/models/transactions.py:118 templates/base.html:90 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:126 +msgid "Transfer" +msgstr "" + +#: apps/note/models/transactions.py:119 +msgid "Template" +msgstr "" + +#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +msgid "Credit" +msgstr "" + +#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 +msgid "Debit" +msgstr "" + +#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 +msgid "membership transaction" +msgstr "" + +#: apps/note/models/transactions.py:129 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:133 msgid "valid" msgstr "" -#: apps/note/models/transactions.py:120 +#: apps/note/models/transactions.py:138 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:121 +#: apps/note/models/transactions.py:139 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:184 -msgid "membership transaction" +#: apps/note/models/transactions.py:207 +msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:212 +msgid "bank" +msgstr "" + +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "" -#: apps/note/views.py:29 -msgid "Transfer money from your account to one or others" +#: apps/note/views.py:31 +msgid "Transfer money" msgstr "" -#: apps/note/views.py:138 -msgid "Consommations" +#: apps/note/views.py:132 templates/base.html:78 +msgid "Consumptions" msgstr "" -#: note_kfet/settings/base.py:155 -msgid "German" +#: note_kfet/settings/__init__.py:61 +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 "English" +msgid "German" msgstr "" #: note_kfet/settings/base.py:157 +msgid "English" +msgstr "" + +#: note_kfet/settings/base.py:158 msgid "French" msgstr "" @@ -438,6 +500,78 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "" +#: templates/base.html:81 +msgid "Clubs" +msgstr "" + +#: templates/base.html:84 +msgid "Activities" +msgstr "" + +#: templates/base.html:87 +msgid "Buttons" +msgstr "" + +#: templates/cas_server/base.html:7 +msgid "Central Authentication Service" +msgstr "" + +#: templates/cas_server/base.html:43 +#, python-format +msgid "" +"A new version of the application is available. This instance runs " +"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " +"upgrading." +msgstr "" + +#: templates/cas_server/logged.html:4 +msgid "" +"<h3>Log In Successful</h3>You have successfully logged into the Central " +"Authentication Service.<br/>For security reasons, please Log Out and Exit " +"your web browser when you are done accessing services that require " +"authentication!" +msgstr "" + +#: templates/cas_server/logged.html:8 +msgid "Log me out from all my sessions" +msgstr "" + +#: templates/cas_server/logged.html:14 +msgid "Forget the identity provider" +msgstr "" + +#: templates/cas_server/logged.html:18 +msgid "Logout" +msgstr "" + +#: templates/cas_server/login.html:6 +msgid "Please log in" +msgstr "" + +#: templates/cas_server/login.html:11 +msgid "" +"If you don't have any Note Kfet account, please follow <a href='/accounts/" +"signup'>this link to sign up</a>." +msgstr "" + +#: templates/cas_server/login.html:17 +msgid "Login" +msgstr "" + +#: templates/cas_server/warn.html:9 +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 +#: templates/member/club_form.html:10 +msgid "Submit" +msgstr "" + #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "" @@ -450,10 +584,22 @@ msgstr "" msgid "Membership duration" msgstr "" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 +#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 msgid "balance" msgstr "" +#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 +msgid "Transaction history" +msgstr "" + +#: templates/member/club_form.html:6 +msgid "Clubs list" +msgstr "" + +#: templates/member/club_list.html:8 +msgid "New club" +msgstr "" + #: templates/member/manage_auth_tokens.html:16 msgid "Token" msgstr "" @@ -466,27 +612,35 @@ msgstr "" msgid "Regenerate token" msgstr "" -#: templates/member/profile_detail.html:11 +#: templates/member/profile_alias.html:10 +msgid "Add alias" +msgstr "" + +#: templates/member/profile_detail.html:15 msgid "first name" msgstr "" -#: templates/member/profile_detail.html:14 +#: templates/member/profile_detail.html:18 msgid "username" msgstr "" -#: templates/member/profile_detail.html:17 +#: templates/member/profile_detail.html:21 msgid "password" msgstr "" -#: templates/member/profile_detail.html:20 +#: templates/member/profile_detail.html:24 msgid "Change password" msgstr "" -#: templates/member/profile_detail.html:38 +#: templates/member/profile_detail.html:42 msgid "Manage auth token" msgstr "" -#: templates/member/profile_detail.html:54 +#: templates/member/profile_detail.html:49 +msgid "View Profile" +msgstr "" + +#: templates/member/profile_detail.html:62 msgid "View my memberships" msgstr "" @@ -494,12 +648,87 @@ msgstr "" msgid "Save Changes" msgstr "" +#: templates/member/signup.html:5 templates/member/signup.html:8 #: templates/member/signup.html:14 -msgid "Sign Up" +msgid "Sign up" msgstr "" -#: templates/note/transaction_form.html:35 -msgid "Transfer" +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +msgid "Select emitters" +msgstr "" + +#: templates/note/conso_form.html:45 +msgid "Select consumptions" +msgstr "" + +#: templates/note/conso_form.html:51 +msgid "Consume!" +msgstr "" + +#: templates/note/conso_form.html:64 +msgid "Most used buttons" +msgstr "" + +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "" + +#: templates/note/conso_form.html:126 +msgid "Single consumptions" +msgstr "" + +#: templates/note/conso_form.html:130 +msgid "Double consumptions" +msgstr "" + +#: templates/note/conso_form.html:141 +msgid "Recent transactions history" +msgstr "" + +#: templates/note/transaction_form.html:55 +msgid "External payment" +msgstr "" + +#: templates/note/transaction_form.html:63 +msgid "Transfer type" +msgstr "" + +#: templates/note/transaction_form.html:73 +msgid "Name" +msgstr "" + +#: templates/note/transaction_form.html:79 +msgid "First name" +msgstr "" + +#: templates/note/transaction_form.html:85 +msgid "Bank" +msgstr "" + +#: templates/note/transaction_form.html:97 +#: templates/note/transaction_form.html:179 +#: templates/note/transaction_form.html:186 +msgid "Select receivers" +msgstr "" + +#: templates/note/transaction_form.html:114 +msgid "Amount" +msgstr "" + +#: templates/note/transaction_form.html:119 +msgid "Reason" +msgstr "" + +#: templates/note/transaction_form.html:193 +msgid "Credit note" +msgstr "" + +#: templates/note/transaction_form.html:200 +msgid "Debit note" +msgstr "" + +#: templates/note/transactiontemplate_form.html:6 +msgid "Buttons list" msgstr "" #: templates/registration/logged_out.html:8 @@ -511,7 +740,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 "" @@ -523,7 +752,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 e73417406cf66d2ac35eaa003746c10b05a618ee..5e6e94704655dc49e1fb27ae2552aab43dc21301 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-02-27 17:39+0100\n" +"POT-Creation-Date: 2020-03-16 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,9 +18,10 @@ msgid "activity" 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:184 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11 +#: apps/member/models.py:61 apps/member/models.py:112 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: templates/member/profile_detail.html:15 msgid "name" msgstr "nom" @@ -44,8 +45,8 @@ msgstr "types d'activité" msgid "description" msgstr "description" -#: apps/activity/models.py:54 apps/note/models/notes.py:160 -#: apps/note/models/transactions.py:62 +#: apps/activity/models.py:54 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 msgid "type" msgstr "type" @@ -81,47 +82,59 @@ msgstr "invités" msgid "API" msgstr "" -#: apps/logs/apps.py:10 +#: apps/logs/apps.py:11 msgid "Logs" msgstr "" -#: apps/logs/models.py:20 apps/note/models/notes.py:105 +#: apps/logs/models.py:21 apps/note/models/notes.py:117 msgid "user" msgstr "utilisateur" #: apps/logs/models.py:27 +msgid "IP Address" +msgstr "Adresse IP" + +#: apps/logs/models.py:35 msgid "model" msgstr "Modèle" -#: apps/logs/models.py:34 +#: apps/logs/models.py:42 msgid "identifier" msgstr "Identifiant" -#: apps/logs/models.py:39 +#: apps/logs/models.py:47 msgid "previous data" msgstr "Données précédentes" -#: apps/logs/models.py:44 -#, fuzzy -#| msgid "end date" +#: apps/logs/models.py:52 msgid "new data" msgstr "Nouvelles données" -#: apps/logs/models.py:51 -#, 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:59 +#: apps/logs/models.py:73 msgid "timestamp" msgstr "Date" -#: apps/logs/models.py:63 +#: apps/logs/models.py:77 msgid "Logs cannot be destroyed." msgstr "Les logs ne peuvent pas être détruits." -#: apps/member/apps.py:10 +#: apps/member/apps.py:14 msgid "member" msgstr "adhérent" @@ -129,7 +142,7 @@ msgstr "adhérent" msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:29 templates/member/profile_detail.html:24 +#: apps/member/models.py:29 templates/member/profile_detail.html:28 msgid "section" msgstr "section" @@ -137,7 +150,7 @@ msgstr "section" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:36 templates/member/profile_detail.html:27 +#: apps/member/models.py:36 templates/member/profile_detail.html:31 msgid "address" msgstr "adresse" @@ -149,37 +162,37 @@ msgstr "payé" msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:65 +#: apps/member/models.py:66 msgid "email" msgstr "courriel" -#: apps/member/models.py:70 +#: apps/member/models.py:71 msgid "membership fee" msgstr "cotisation pour adhérer" -#: apps/member/models.py:74 +#: apps/member/models.py:75 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:75 +#: apps/member/models.py:76 msgid "The longest time a membership can last (NULL = infinite)." msgstr "La durée maximale d'une adhésion (NULL = infinie)." -#: apps/member/models.py:80 +#: apps/member/models.py:81 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:81 +#: apps/member/models.py:82 msgid "How long after January 1st the members can renew their membership." msgstr "" "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "adhésion." -#: apps/member/models.py:86 +#: apps/member/models.py:87 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:87 +#: apps/member/models.py:88 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -187,166 +200,174 @@ 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:135 +#: apps/member/models.py:94 apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:94 +#: apps/member/models.py:95 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:117 +#: apps/member/models.py:118 msgid "role" msgstr "rôle" -#: apps/member/models.py:118 +#: apps/member/models.py:119 msgid "roles" msgstr "rôles" -#: apps/member/models.py:142 +#: apps/member/models.py:143 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:145 +#: apps/member/models.py:146 msgid "membership ends on" msgstr "l'adhésion finie le" -#: apps/member/models.py:149 +#: apps/member/models.py:150 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:153 +#: apps/member/models.py:154 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:154 +#: apps/member/models.py:155 msgid "memberships" msgstr "adhésions" -#: apps/member/views.py:63 templates/member/profile_detail.html:42 +#: apps/member/views.py:69 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:79 +#: apps/member/views.py:82 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà ." -#: apps/member/views.py:130 +#: apps/member/views.py:132 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "Compte n°%(id)s : %(username)s" -#: apps/note/admin.py:120 apps/note/models/transactions.py:93 +#: apps/member/views.py:202 +msgid "Alias successfully deleted" +msgstr "L'alias a bien été supprimé" + +#: apps/note/admin.py:120 apps/note/models/transactions.py:94 msgid "source" msgstr "source" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 msgid "destination" msgstr "destination" -#: apps/note/apps.py:14 apps/note/models/notes.py:54 +#: apps/note/apps.py:14 apps/note/models/notes.py:58 msgid "note" msgstr "note" -#: apps/note/forms.py:49 -msgid "Source and destination must be different." -msgstr "La source et la destination doivent être différentes." +#: apps/note/forms.py:20 +msgid "New Alias" +msgstr "Nouvel alias" + +#: apps/note/forms.py:25 +msgid "select an image" +msgstr "Choisissez une image" -#: apps/note/models/notes.py:26 +#: apps/note/forms.py:26 +msgid "Maximal size: 2MB" +msgstr "Taille maximale : 2 Mo" + +#: 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:49 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 msgid "created at" msgstr "créée le" -#: apps/note/models/notes.py:55 +#: apps/note/models/notes.py:59 msgid "notes" msgstr "notes" -#: apps/note/models/notes.py:63 +#: apps/note/models/notes.py:67 msgid "Note" msgstr "Note" -#: apps/note/models/notes.py:73 apps/note/models/notes.py:97 +#: 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:113 -msgid "user" -msgstr "utilisateur" - -#: apps/note/models/notes.py:117 +#: apps/note/models/notes.py:121 msgid "one's note" msgstr "note d'un utilisateur" -#: apps/note/models/notes.py:118 +#: apps/note/models/notes.py:122 msgid "users note" msgstr "notes des utilisateurs" -#: apps/note/models/notes.py:124 +#: apps/note/models/notes.py:128 #, python-format msgid "%(user)s's note" msgstr "Note de %(user)s" -#: apps/note/models/notes.py:139 +#: apps/note/models/notes.py:143 msgid "club note" msgstr "note d'un club" -#: apps/note/models/notes.py:140 +#: apps/note/models/notes.py:144 msgid "clubs notes" msgstr "notes des clubs" -#: apps/note/models/notes.py:146 +#: 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:166 +#: apps/note/models/notes.py:170 msgid "special note" msgstr "note spéciale" -#: apps/note/models/notes.py:167 +#: apps/note/models/notes.py:171 msgid "special notes" msgstr "notes spéciales" -#: apps/note/models/notes.py:190 +#: apps/note/models/notes.py:194 msgid "Invalid alias" msgstr "Alias invalide" -#: apps/note/models/notes.py:206 +#: apps/note/models/notes.py:210 msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33 +#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 msgid "aliases" msgstr "alias" @@ -355,10 +376,10 @@ msgid "Alias is too long." msgstr "L'alias est trop long." #: apps/note/models/notes.py:238 -msgid "An alias with a similar name already exists:" -msgstr "Un alias avec un nom similaire existe déjà ." +msgid "An alias with a similar name already exists: {} " +msgstr "Un alias avec un nom similaire existe déjà : {}" -#: apps/note/models/notes.py:246 +#: apps/note/models/notes.py:247 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." @@ -371,11 +392,10 @@ 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à ." -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 msgid "amount" msgstr "montant" @@ -383,59 +403,96 @@ msgstr "montant" msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:74 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:106 +#: apps/note/models/transactions.py:107 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:111 +#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "Don" + +#: apps/note/models/transactions.py:118 templates/base.html:90 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:126 +msgid "Transfer" +msgstr "Virement" + +#: apps/note/models/transactions.py:119 +msgid "Template" +msgstr "Bouton" + +#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +msgid "Credit" +msgstr "Crédit" + +#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 +msgid "Debit" +msgstr "Retrait" + +#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 +msgid "membership transaction" +msgstr "transaction d'adhésion" + +#: apps/note/models/transactions.py:129 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:133 msgid "valid" msgstr "valide" -#: apps/note/models/transactions.py:120 +#: apps/note/models/transactions.py:138 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:121 +#: apps/note/models/transactions.py:139 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:184 -msgid "membership transaction" -msgstr "transaction d'adhésion" +#: apps/note/models/transactions.py:207 +msgid "first_name" +msgstr "Prénom" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:212 +msgid "bank" +msgstr "Banque" + +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/views.py:29 -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:31 +msgid "Transfer money" +msgstr "Transferts d'argent" -#: apps/note/views.py:138 -msgid "Consommations" -msgstr "transactions" +#: apps/note/views.py:132 templates/base.html:78 +msgid "Consumptions" +msgstr "Consommations" -#: note_kfet/settings/base.py:155 -msgid "German" +#: note_kfet/settings/__init__.py:61 +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 "English" +msgid "German" msgstr "" #: note_kfet/settings/base.py:157 +msgid "English" +msgstr "" + +#: note_kfet/settings/base.py:158 msgid "French" msgstr "" @@ -443,6 +500,80 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." +#: templates/base.html:81 +msgid "Clubs" +msgstr "Clubs" + +#: templates/base.html:84 +msgid "Activities" +msgstr "Activités" + +#: templates/base.html:87 +msgid "Buttons" +msgstr "Boutons" + +#: templates/cas_server/base.html:7 +msgid "Central Authentication Service" +msgstr "" + +#: templates/cas_server/base.html:43 +#, python-format +msgid "" +"A new version of the application is available. This instance runs " +"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " +"upgrading." +msgstr "" + +#: templates/cas_server/logged.html:4 +msgid "" +"<h3>Log In Successful</h3>You have successfully logged into the Central " +"Authentication Service.<br/>For security reasons, please Log Out and Exit " +"your web browser when you are done accessing services that require " +"authentication!" +msgstr "" + +#: templates/cas_server/logged.html:8 +msgid "Log me out from all my sessions" +msgstr "" + +#: templates/cas_server/logged.html:14 +msgid "Forget the identity provider" +msgstr "" + +#: templates/cas_server/logged.html:18 +msgid "Logout" +msgstr "" + +#: templates/cas_server/login.html:6 +msgid "Please log in" +msgstr "" + +#: templates/cas_server/login.html:11 +msgid "" +"If you don't have any Note Kfet account, please follow <a href='/accounts/" +"signup'>this link to sign up</a>." +msgstr "" +"Si vous n'avez pas de compte Note Kfet, veuillez suivre <a href='/accounts/" +"signup'>ce lien pour vous inscrire</a>." + +#: templates/cas_server/login.html:17 +msgid "Login" +msgstr "" + +#: templates/cas_server/warn.html:9 +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 +#: templates/member/club_form.html:10 +msgid "Submit" +msgstr "Envoyer" + #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "L'adhésion commence le" @@ -455,10 +586,22 @@ msgstr "L'adhésion finie le" msgid "Membership duration" msgstr "Durée de l'adhésion" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 +#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 msgid "balance" msgstr "solde du compte" +#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 +msgid "Transaction history" +msgstr "Historique des transactions" + +#: templates/member/club_form.html:6 +msgid "Clubs list" +msgstr "Liste des clubs" + +#: templates/member/club_list.html:8 +msgid "New club" +msgstr "Nouveau club" + #: templates/member/manage_auth_tokens.html:16 msgid "Token" msgstr "Jeton" @@ -471,33 +614,35 @@ msgstr "Créé le" msgid "Regenerate token" msgstr "Regénérer le jeton" -#: templates/member/profile_detail.html:11 +#: templates/member/profile_alias.html:10 +msgid "Add alias" +msgstr "Ajouter un alias" + +#: templates/member/profile_detail.html:15 msgid "first name" msgstr "" -#: templates/member/profile_detail.html:14 +#: templates/member/profile_detail.html:18 msgid "username" msgstr "" -#: templates/member/profile_detail.html:17 -#, fuzzy -#| msgid "Change password" +#: templates/member/profile_detail.html:21 msgid "password" msgstr "" -#: templates/member/profile_detail.html:20 +#: templates/member/profile_detail.html:24 msgid "Change password" msgstr "Changer le mot de passe" -#: templates/member/profile_detail.html:38 +#: templates/member/profile_detail.html:42 msgid "Manage auth token" msgstr "Gérer les jetons d'authentification" -#: templates/member/profile_detail.html:51 -msgid "Transaction history" -msgstr "Historique des transactions" +#: templates/member/profile_detail.html:49 +msgid "View Profile" +msgstr "Voir le profil" -#: templates/member/profile_detail.html:54 +#: templates/member/profile_detail.html:62 msgid "View my memberships" msgstr "Voir mes adhésions" @@ -505,13 +650,88 @@ msgstr "Voir mes adhésions" msgid "Save Changes" msgstr "Sauvegarder les changements" +#: templates/member/signup.html:5 templates/member/signup.html:8 #: templates/member/signup.html:14 -msgid "Sign Up" -msgstr "" +msgid "Sign up" +msgstr "Inscription" -#: templates/note/transaction_form.html:35 -msgid "Transfer" -msgstr "Virement" +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +msgid "Select emitters" +msgstr "Sélection des émetteurs" + +#: templates/note/conso_form.html:45 +msgid "Select consumptions" +msgstr "Consommations" + +#: templates/note/conso_form.html:51 +msgid "Consume!" +msgstr "Consommer !" + +#: templates/note/conso_form.html:64 +msgid "Most used buttons" +msgstr "Boutons les plus utilisés" + +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "Éditer" + +#: templates/note/conso_form.html:126 +msgid "Single consumptions" +msgstr "Consos simples" + +#: templates/note/conso_form.html:130 +msgid "Double consumptions" +msgstr "Consos doubles" + +#: templates/note/conso_form.html:141 +msgid "Recent transactions history" +msgstr "Historique des transactions récentes" + +#: templates/note/transaction_form.html:55 +msgid "External payment" +msgstr "Paiement extérieur" + +#: templates/note/transaction_form.html:63 +msgid "Transfer type" +msgstr "Type de transfert" + +#: templates/note/transaction_form.html:73 +msgid "Name" +msgstr "Nom" + +#: templates/note/transaction_form.html:79 +msgid "First name" +msgstr "Prénom" + +#: templates/note/transaction_form.html:85 +msgid "Bank" +msgstr "Banque" + +#: templates/note/transaction_form.html:97 +#: templates/note/transaction_form.html:179 +#: templates/note/transaction_form.html:186 +msgid "Select receivers" +msgstr "Sélection des destinataires" + +#: templates/note/transaction_form.html:114 +msgid "Amount" +msgstr "Montant" + +#: templates/note/transaction_form.html:119 +msgid "Reason" +msgstr "Raison" + +#: templates/note/transaction_form.html:193 +msgid "Credit note" +msgstr "Note à créditer" + +#: templates/note/transaction_form.html:200 +msgid "Debit note" +msgstr "Note à débiter" + +#: templates/note/transactiontemplate_form.html:6 +msgid "Buttons list" +msgstr "Liste des boutons" #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." @@ -522,7 +742,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 "" @@ -534,7 +754,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/nginx_note.conf_example b/nginx_note.conf_example index 1f7ce4caf3c4f90371bfbd017cfed32e761abcca..204784d06ff0d4d6b0c35f0c5ff426f2b726e325 100644 --- a/nginx_note.conf_example +++ b/nginx_note.conf_example @@ -9,7 +9,7 @@ server { # the port your site will be served on listen 80; # the domain name it will serve for - server_name note.comby.xyz; # substitute your machine's IP address or FQDN + server_name note.example.org; # substitute your machine's IP address or FQDN charset utf-8; # max upload size diff --git a/note_kfet/fixtures/cas.json b/note_kfet/fixtures/cas.json new file mode 100644 index 0000000000000000000000000000000000000000..c3109d19d402cb277797dd5a1fbaf1b34bf75cbc --- /dev/null +++ b/note_kfet/fixtures/cas.json @@ -0,0 +1,11 @@ +[ + { + "model": "cas_server.servicepattern", + "pk": 1, + "fields": { + "pos": 1, + "pattern": ".*", + "name": "REPLACEME" + } + } +] diff --git a/note_kfet/fixtures/initial.json b/note_kfet/fixtures/initial.json index 1b7799807cb835ced23a44adf79ee72dfdeae398..72e472340120c572d90d5306b1942b32b48d71e9 100644 --- a/note_kfet/fixtures/initial.json +++ b/note_kfet/fixtures/initial.json @@ -6,14 +6,5 @@ "domain": "localhost", "name": "La Note Kfet \ud83c\udf7b" } - }, - { - "model": "cas_server.servicepattern", - "pk": 1, - "fields": { - "pos": 1, - "pattern": ".*", - "name": "REPLACEME" - } } -] \ No newline at end of file +] diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py index 73b87e363c32faf1d0fa836122fb2a2674347894..b034e2bee3453486a1fb10fbabd687de9c70227f 100644 --- a/note_kfet/middlewares.py +++ b/note_kfet/middlewares.py @@ -1,10 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.http import HttpResponseRedirect - -from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit - class TurbolinksMiddleware(object): """ @@ -35,4 +31,3 @@ class TurbolinksMiddleware(object): location = request.session.pop('_turbolinks_redirect_to') response['Turbolinks-Location'] = location return response - diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 68a40b887c39d6dde8bd55514cbea624048772e3..28935deba3ee7ba6fc7aeb9fa6d7f5ea383a2623 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -1,8 +1,12 @@ -import os +# 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 * + def read_env(): """Pulled from Honcho code with minor updates, reads local default environment variables from a .env file located in the project root @@ -25,22 +29,53 @@ def read_env(): val = re.sub(r'\\(.)', r'\1', m3.group(1)) os.environ.setdefault(key, val) + 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.append(os.environ.get('ALLOWED_HOSTS','localhost')) else: from .development import * try: + #in secrets.py defines everything you want from .secrets import * except ImportError: pass -# env variables set at the of in /env/bin/activate -# don't forget to unset in deactivate ! +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": _( + u"The Central Authentication Service grants you access to most of our websites by " + u"authenticating only once, so you don't need to type your credentials again unless " + u"your session expires or you logout." + ), + "discardable": True, + "type": "info", # one of info, success, info, warning, danger + }, + } + + CAS_INFO_MESSAGES_ORDER = [ + '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'] diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 20937fac17fb198afbf7a5f3d015d0bf483ba6d5..29ff49c52446915b157675df15cc53d23e3bc4bc 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -37,9 +37,10 @@ INSTALLED_APPS = [ # External apps 'polymorphic', - 'reversion', 'crispy_forms', 'django_tables2', + 'cas_server', + 'cas', # Django contrib 'django.contrib.admin', 'django.contrib.admindocs', @@ -55,9 +56,6 @@ INSTALLED_APPS = [ # Autocomplete 'dal', 'dal_select2', - # CAS - 'cas_server', - 'cas', # Note apps 'activity', @@ -81,7 +79,6 @@ MIDDLEWARE = [ 'django.middleware.locale.LocaleMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', 'note_kfet.middlewares.TurbolinksMiddleware', - 'cas.middleware.CASMiddleware', ] ROOT_URLCONF = 'note_kfet.urls' @@ -98,7 +95,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.request', - # 'django.template.context_processors.media', + # 'django.template.context_processors.media', ], }, }, @@ -133,7 +130,7 @@ PASSWORD_HASHERS = [ # Django Guardian object permissions AUTHENTICATION_BACKENDS = ( - #'django.contrib.auth.backends.ModelBackend', # this is default + # 'django.contrib.auth.backends.ModelBackend', # this is default 'member.backends.PermissionBackend', 'cas.backends.CASBackend', ) @@ -146,12 +143,13 @@ REST_FRAMEWORK = { 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ], 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', - ] + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, } -ANONYMOUS_USER_NAME = None # Disable guardian anonymous user - # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ @@ -182,7 +180,7 @@ FIXTURE_DIRS = [os.path.join(BASE_DIR, "note_kfet/fixtures")] # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/var/www/example.com/static/" -STATIC_ROOT = os.path.join(BASE_DIR,"static/") +STATIC_ROOT = os.path.join(BASE_DIR, "static/") # STATICFILES_DIRS = [ # os.path.join(BASE_DIR, 'static')] STATICFILES_DIRS = [] @@ -194,15 +192,9 @@ STATIC_URL = '/static/' ALIAS_VALIDATOR_REGEX = r'' -MEDIA_ROOT=os.path.join(BASE_DIR,"media") -MEDIA_URL='/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = '/media/' # Profile Picture Settings PIC_WIDTH = 200 PIC_RATIO = 1 - -# CAS Settings -CAS_AUTO_CREATE_USER = False -CAS_LOGO_URL = "/static/img/Saperlistpopette.png" -CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png" - diff --git a/note_kfet/settings/development.py b/note_kfet/settings/development.py index ad2cd2f1028d10e3fd2044e19f0e153c487e5832..66ad4fd44ed80b718cb1b430d9ad97b6692b6ba5 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -11,17 +11,30 @@ # - and more ... +import os + # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases from . import * -import os -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 @@ -38,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 @@ -51,4 +64,8 @@ SESSION_COOKIE_AGE = 60 * 60 * 3 # CAS Client settings # Can be modified in secrets.py -CAS_SERVER_URL = "https://note.comby.xyz/cas/" +CAS_SERVER_URL = "http://localhost:8000/cas/" + +STATIC_ROOT = '' # not needed in development settings +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'static')] diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 353d7b8a39f8a844c60f7376f30586508b6882e7..5be8a3b899c5093272b2db12c4aaab44060c233c 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 = ['127.0.0.1','note.comby.xyz'] +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/apps/logs/urls.py b/note_kfet/settings/secrets_example.py similarity index 56% rename from apps/logs/urls.py rename to note_kfet/settings/secrets_example.py index 6d76674c0b0835042b4c6ae15d843d29dd070612..70d17ad4330565837b440b177722e336cc0b6c58 100644 --- a/apps/logs/urls.py +++ b/note_kfet/settings/secrets_example.py @@ -1,8 +1,9 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -app_name = 'logs' - -# TODO User interface -urlpatterns = [ +# CAS +OPTIONAL_APPS = [ +# 'cas_server', +# 'cas', +# 'debug_toolbar' ] diff --git a/note_kfet/urls.py b/note_kfet/urls.py index a261a9eb9891d9430989d46dfe2f2613e3c77c84..da2f9d6c246833c3962b8b0727869a9585b5c4f9 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -1,13 +1,11 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include from django.views.generic import RedirectView -from django.conf.urls.static import static -from django.conf import settings - -from cas import views as cas_views urlpatterns = [ # Dev so redirect to something random @@ -16,25 +14,34 @@ urlpatterns = [ # Include project routers path('note/', include('note.urls')), - # Include CAS Client routers - path('accounts/login/', cas_views.login, name='login'), - path('accounts/logout/', cas_views.logout, name='logout'), - # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), path('accounts/', include('member.urls')), path('accounts/', include('django.contrib.auth.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), - - # Include CAS Server routers - path('cas/', include('cas_server.urls', namespace="cas_server")), - - # Include Django REST API path('api/', include('api.urls')), - - path('logs/', include('logs.urls')), ] -urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) -urlpatterns += static(settings.STATIC_URL,document_root=settings.STATIC_ROOT) +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + +if "cas_server" in settings.INSTALLED_APPS: + urlpatterns += [ + # Include CAS Server routers + path('cas/', include('cas_server.urls', namespace="cas_server")), + ] +if "cas" in settings.INSTALLED_APPS: + from cas import views as cas_views + urlpatterns += [ + # Include CAS Client routers + 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: + import debug_toolbar + urlpatterns = [ + path('__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/requirements/api.txt b/requirements/api.txt new file mode 100644 index 0000000000000000000000000000000000000000..8dd9f5f2e0a50d42949b2647a6184f9bd77598a8 --- /dev/null +++ b/requirements/api.txt @@ -0,0 +1,3 @@ +djangorestframework==3.9.0 +django-rest-polymorphic==0.1.8 + diff --git a/requirements.txt b/requirements/base.txt similarity index 73% rename from requirements.txt rename to requirements/base.txt index 9a5eaa22ac63b20ff4021533bdc6ed668679d3ea..e9dc7635ed8444cc48f0ce6c615cbe812622a9ba 100644 --- a/requirements.txt +++ b/requirements/base.txt @@ -4,18 +4,12 @@ defusedxml==0.6.0 Django~=2.2 django-allauth==0.39.1 django-autocomplete-light==3.5.1 -django-cas-client==1.5.3 -django-cas-server==1.1.0 django-crispy-forms==1.7.2 django-extensions==2.1.9 django-filter==2.2.0 django-polymorphic==2.0.3 -djangorestframework==3.9.0 -django-rest-polymorphic==0.1.8 -django-reversion==3.0.3 django-tables2==2.1.0 docutils==0.14 -psycopg2==2.8.4 idna==2.8 oauthlib==3.1.0 Pillow==6.1.0 diff --git a/requirements/cas.txt b/requirements/cas.txt new file mode 100644 index 0000000000000000000000000000000000000000..d468d2d5077580bf004e625a136f31a1734dcef0 --- /dev/null +++ b/requirements/cas.txt @@ -0,0 +1,2 @@ +django-cas-client==1.5.3 +django-cas-server==1.1.0 diff --git a/requirements/production.txt b/requirements/production.txt new file mode 100644 index 0000000000000000000000000000000000000000..f0b5222826649e71d629934444582238919500d9 --- /dev/null +++ b/requirements/production.txt @@ -0,0 +1 @@ +psycopg2==2.8.4 diff --git a/static/js/base.js b/static/js/base.js new file mode 100644 index 0000000000000000000000000000000000000000..2362375bf7c6f89fc7995f5764f47f7ef9113003 --- /dev/null +++ b/static/js/base.js @@ -0,0 +1,281 @@ +// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +// SPDX-License-Identifier: GPL-3.0-or-later + + +/** + * Convert balance in cents to a human readable amount + * @param value the balance, in cents + * @returns {string} + */ +function pretty_money(value) { + if (value % 100 === 0) + return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €"; + else + return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "." + + (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €"; +} + +/** + * Add a message on the top of the page. + * @param msg The message to display + * @param alert_type The type of the alert. Choices: info, success, warning, danger + */ +function addMsg(msg, alert_type) { + let msgDiv = $("#messages"); + let html = msgDiv.html(); + html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" + + "<button class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>" + + msg + "</div>\n"; + msgDiv.html(html); +} + +/** + * Reload the balance of the user on the right top corner + */ +function refreshBalance() { + $("#user_balance").load("/ #user_balance"); +} + +/** + * Query the 20 first matched notes with a given pattern + * @param pattern The pattern that is queried + * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. + */ +function getMatchedNotes(pattern, fun) { + $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); +} + +/** + * Generate a <li> entry with a given id and text + */ +function li(id, text) { + return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" + + " id=\"" + id + "\">" + text + "</li>\n"; +} + +/** + * Render note name and picture + * @param note The note to render + * @param alias The alias to be displayed + * @param user_note_field + * @param profile_pic_field + */ +function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { + let img = note == null ? null : note.display_image; + if (img == null) + img = '/media/pic/default.png'; + if (note !== null && alias !== note.name) + alias += " (aka. " + note.name + ")"; + if (note !== null && user_note_field !== null) + $("#" + user_note_field).text(alias + " : " + pretty_money(note.balance)); + if (profile_pic_field != null) + $("#" + profile_pic_field).attr('src', img); +} + +/** + * Remove a note from the emitters. + * @param d The note to remove + * @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters + * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] + * @param note_list_id The div block identifier where the notes of the buyers are displayed + * @param user_note_field The identifier of the field that display the note of the hovered note (useful in + * consumptions, put null if not used) + * @param profile_pic_field The identifier of the field that display the profile picture of the hovered note + * (useful in consumptions, put null if not used) + * @returns an anonymous function to be compatible with jQuery events + */ +function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) { + return (function() { + let new_notes_display = []; + let html = ""; + notes_display.forEach(function (disp) { + if (disp.quantity > 1 || disp.id !== d.id) { + disp.quantity -= disp.id === d.id ? 1 : 0; + new_notes_display.push(disp); + html += li(note_prefix + "_" + disp.id, disp.name + + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); + } + }); + + notes_display.length = 0; + new_notes_display.forEach(function(disp) { + notes_display.push(disp); + }); + + $("#" + note_list_id).html(html); + notes_display.forEach(function (disp) { + let obj = $("#" + note_prefix + "_" + disp.id); + obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); + obj.hover(function() { + if (disp.note) + displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + }); + }); + }); +} + +/** + * Generate an auto-complete field to query a note with its alias + * @param field_id The identifier of the text field where the alias is typed + * @param alias_matched_id The div block identifier where the matched aliases are displayed + * @param note_list_id The div block identifier where the notes of the buyers are displayed + * @param notes An array containing the note objects of the buyers + * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] + * @param alias_prefix The prefix of the <li> blocks for the matched aliases + * @param note_prefix The prefix of the <li> blocks for the notes of the buyers + * @param user_note_field The identifier of the field that display the note of the hovered note (useful in + * consumptions, put null if not used) + * @param profile_pic_field The identifier of the field that display the profile picture of the hovered note + * (useful in consumptions, put null if not used) + * @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true, + * the associated note is not displayed. + * Useful for a consumption if the item is selected before. + */ +function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias", + note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) { + let field = $("#" + field_id); + // When the user clicks on the search field, it is immediately cleared + field.click(function() { + field.val(""); + }); + + let old_pattern = null; + + // When the user type "Enter", the first alias is clicked + field.keypress(function(event) { + if (event.originalEvent.charCode === 13) + $("#" + alias_matched_id + " li").first().trigger("click"); + }); + + // When the user type something, the matched aliases are refreshed + field.keyup(function(e) { + if (e.originalEvent.charCode === 13) + return; + + let pattern = field.val(); + // If the pattern is not modified, we don't query the API + if (pattern === old_pattern || pattern === "") + return; + + old_pattern = pattern; + + // Clear old matched notes + notes.length = 0; + + let aliases_matched_obj = $("#" + alias_matched_id); + let aliases_matched_html = ""; + + // Get matched notes with the given pattern + getMatchedNotes(pattern, function(aliases) { + // The response arrived too late, we stop the request + if (pattern !== $("#" + field_id).val()) + return; + + aliases.results.forEach(function (alias) { + let note = alias.note; + aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name); + note.alias = alias; + notes.push(note); + }); + + // Display the list of matched aliases + aliases_matched_obj.html(aliases_matched_html); + + notes.forEach(function (note) { + let alias = note.alias; + let alias_obj = $("#" + alias_prefix + "_" + alias.id); + // When an alias is hovered, the profile picture and the balance are displayed at the right place + alias_obj.hover(function () { + displayNote(note, alias.name, user_note_field, profile_pic_field); + }); + + // When the user click on an alias, the associated note is added to the emitters + alias_obj.click(function () { + field.val(""); + // If the note is already an emitter, we increase the quantity + var disp = null; + notes_display.forEach(function (d) { + // We compare the note ids + if (d.id === note.id) { + d.quantity += 1; + disp = d; + } + }); + // In the other case, we add a new emitter + if (disp == null) { + disp = { + name: alias.name, + id: note.id, + note: note, + quantity: 1 + }; + notes_display.push(disp); + } + + // If the function alias_click exists, it is called. If it doesn't return true, then the notes are + // note displayed. Useful for a consumption when a button is already clicked + if (alias_click && !alias_click()) + return; + + let note_list = $("#" + note_list_id); + let html = ""; + notes_display.forEach(function (disp) { + html += li(note_prefix + "_" + disp.id, disp.name + + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); + }); + + // Emitters are displayed + note_list.html(html); + + notes_display.forEach(function (disp) { + let line_obj = $("#" + note_prefix + "_" + disp.id); + // Hover an emitter display also the profile picture + line_obj.hover(function () { + displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + }); + + // When an emitter is clicked, it is removed + line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, + profile_pic_field)); + }); + }); + }); + }); + }); +} + +// When a validate button is clicked, we switch the validation status +function de_validate(id, validated) { + $("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>"); + + // Perform a PATCH request to the API in order to update the transaction + // If the user has insuffisent rights, an error message will appear + $.ajax({ + "url": "/api/note/transaction/transaction/" + id + "/", + type: "PATCH", + dataType: "json", + headers: { + "X-CSRFTOKEN": CSRF_TOKEN + }, + data: { + "resourcetype": "TemplateTransaction", + valid: !validated + }, + success: function () { + // Refresh jQuery objects + $(".validate").click(de_validate); + + refreshBalance(); + // error if this method doesn't exist. Please define it. + refreshHistory(); + }, + error: function(err) { + addMsg("Une erreur est survenue lors de la validation/dévalidation " + + "de cette transaction : " + err.responseText, "danger"); + + refreshBalance(); + // error if this method doesn't exist. Please define it. + refreshHistory(); + } + }); +} diff --git a/static/js/consos.js b/static/js/consos.js new file mode 100644 index 0000000000000000000000000000000000000000..5f7a314a949cc600ff02b15e14da9a9fdb91de81 --- /dev/null +++ b/static/js/consos.js @@ -0,0 +1,205 @@ +// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Refresh the history table on the consumptions page. + */ +function refreshHistory() { + $("#history").load("/note/consos/ #history"); + $("#most_used").load("/note/consos/ #most_used"); +} + +$(document).ready(function() { + // If hash of a category in the URL, then select this category + // else select the first one + if (location.hash) { + $("a[href='" + location.hash + "']").tab("show"); + } else { + $("a[data-toggle='tab']").first().tab("show"); + } + + // When selecting a category, change URL + $(document.body).on("click", "a[data-toggle='tab']", function() { + location.hash = this.getAttribute("href"); + }); + + // Switching in double consumptions mode should update the layout + let double_conso_obj = $("#double_conso"); + double_conso_obj.click(function() { + $("#consos_list_div").show(); + $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); + $("#note_infos_div").attr('class', 'col-xl-3'); + $("#user_select_div").attr('class', 'col-xl-4'); + $("#buttons_div").attr('class', 'col-sm-7 col-xl-6'); + + let note_list_obj = $("#note_list"); + if (buttons.length > 0 && note_list_obj.text().length > 0) { + $("#consos_list").html(note_list_obj.html()); + note_list_obj.html(""); + + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, + "consos_list")); + }); + } + }); + + let single_conso_obj = $("#single_conso"); + single_conso_obj.click(function() { + $("#consos_list_div").hide(); + $("#infos_div").attr('class', 'col-sm-5 col-md-4'); + $("#note_infos_div").attr('class', 'col-xl-5'); + $("#user_select_div").attr('class', 'col-xl-7'); + $("#buttons_div").attr('class', 'col-sm-7 col-md-8'); + + let consos_list_obj = $("#consos_list"); + if (buttons.length > 0) { + if (notes_display.length === 0 && consos_list_obj.text().length > 0) { + $("#note_list").html(consos_list_obj.html()); + consos_list_obj.html(""); + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, + "note_list")); + }); + } + else { + buttons.length = 0; + consos_list_obj.html(""); + } + } + }); + + // Ensure we begin in single consumption. Removing these lines may cause problems when reloading. + single_conso_obj.prop('checked', 'true'); + double_conso_obj.removeAttr('checked'); + $("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary'); + + $("#consos_list_div").hide(); + + $("#consume_all").click(consumeAll); +}); + +notes = []; +notes_display = []; +buttons = []; + +// When the user searches an alias, we update the auto-completion +autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display, + "alias", "note", "user_note", "profile_pic", function() { + if (buttons.length > 0 && $("#single_conso").is(":checked")) { + consumeAll(); + return false; + } + return true; + }); + +/** + * Add a transaction from a button. + * @param dest Where the money goes + * @param amount The price of the item + * @param type The type of the transaction (content type id for TemplateTransaction) + * @param category_id The category identifier + * @param category_name The category name + * @param template_id The identifier of the button + * @param template_name The name of the button + */ +function addConso(dest, amount, type, category_id, category_name, template_id, template_name) { + var button = null; + buttons.forEach(function(b) { + if (b.id === template_id) { + b.quantity += 1; + button = b; + } + }); + if (button == null) { + button = { + id: template_id, + name: template_name, + dest: dest, + quantity: 1, + amount: amount, + type: type, + category_id: category_id, + category_name: category_name + }; + buttons.push(button); + } + + let dc_obj = $("#double_conso"); + if (dc_obj.is(":checked") || notes_display.length === 0) { + let list = dc_obj.is(":checked") ? "consos_list" : "note_list"; + let html = ""; + buttons.forEach(function(button) { + html += li("conso_button_" + button.id, button.name + + "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>"); + }); + + $("#" + list).html(html); + + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, list)); + }); + } + else + consumeAll(); +} + +/** + * Reset the page as its initial state. + */ +function reset() { + notes_display.length = 0; + notes.length = 0; + buttons.length = 0; + $("#note_list").html(""); + $("#alias_matched").html(""); + $("#consos_list").html(""); + displayNote(null, ""); + refreshHistory(); + refreshBalance(); +} + + +/** + * Apply all transactions: all notes in `notes` buy each item in `buttons` + */ +function consumeAll() { + notes_display.forEach(function(note_display) { + buttons.forEach(function(button) { + consume(note_display.id, button.dest, button.quantity * note_display.quantity, button.amount, + button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); + }); + }); +} + +/** + * Create a new transaction from a button through the API. + * @param source The note that paid the item (type: int) + * @param dest The note that sold the item (type: int) + * @param quantity The quantity sold (type: int) + * @param amount The price of one item, in cents (type: int) + * @param reason The transaction details (type: str) + * @param type The type of the transaction (content type id for TemplateTransaction) + * @param category The category id of the button (type: int) + * @param template The button id (type: int) + */ +function consume(source, dest, quantity, amount, reason, type, category, template) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": quantity, + "amount": amount, + "reason": reason, + "valid": true, + "polymorphic_ctype": type, + "resourcetype": "TemplateTransaction", + "source": source, + "destination": dest, + "category": category, + "template": template + }, reset).fail(function (e) { + reset(); + + addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger"); + }); +} diff --git a/static/js/transfer.js b/static/js/transfer.js new file mode 100644 index 0000000000000000000000000000000000000000..a0c2d88ae2bf65e545df8b53b45b1d5f2f0188e8 --- /dev/null +++ b/static/js/transfer.js @@ -0,0 +1,157 @@ +sources = []; +sources_notes_display = []; +dests = []; +dests_notes_display = []; + +function refreshHistory() { + $("#history").load("/note/transfer/ #history"); +} + +function reset() { + sources_notes_display.length = 0; + sources.length = 0; + dests_notes_display.length = 0; + dests.length = 0; + $("#source_note_list").html(""); + $("#dest_note_list").html(""); + $("#source_alias_matched").html(""); + $("#dest_alias_matched").html(""); + $("#amount").val(""); + $("#reason").val(""); + $("#last_name").val(""); + $("#first_name").val(""); + $("#bank").val(""); + refreshBalance(); + refreshHistory(); +} + +$(document).ready(function() { + autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display, + "source_alias", "source_note", "user_note", "profile_pic"); + autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display, + "dest_alias", "dest_note", "user_note", "profile_pic", function() { + let last = dests_notes_display[dests_notes_display.length - 1]; + dests_notes_display.length = 0; + dests_notes_display.push(last); + + last.quantity = 1; + + $.getJSON("/api/user/" + last.note.user + "/", function(user) { + $("#last_name").val(user.last_name); + $("#first_name").val(user.first_name); + }); + + return true; + }); + + + // Ensure we begin in gift mode. Removing these lines may cause problems when reloading. + $("#type_gift").prop('checked', 'true'); + $("#type_transfer").removeAttr('checked'); + $("#type_credit").removeAttr('checked'); + $("#type_debit").removeAttr('checked'); + $("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary'); + $("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary'); + $("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary'); +}); + +$("#transfer").click(function() { + if ($("#type_gift").is(':checked')) { + dests_notes_display.forEach(function (dest) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": true, + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": user_id, + "destination": dest.id + }, function () { + addMsg("Le transfert de " + + pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + + " vers la note " + dest.name + " a été fait avec succès !", "success"); + + reset(); + }).fail(function (err) { + addMsg("Le transfert de " + + pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); + + reset(); + }); + }); + } + else if ($("#type_transfer").is(':checked')) { + sources_notes_display.forEach(function (source) { + dests_notes_display.forEach(function (dest) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": source.quantity * dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": true, + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": source.id, + "destination": dest.id + }, function () { + addMsg("Le transfert de " + + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + " vers la note " + dest.name + " a été fait avec succès !", "success"); + + reset(); + }).fail(function (err) { + addMsg("Le transfert de " + + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); + + reset(); + }); + }); + }); + } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { + let special_note = $("#credit_type").val(); + let user_note = dests_notes_display[0].id; + let given_reason = $("#reason").val(); + let source, dest, reason; + if ($("#type_credit").is(':checked')) { + source = special_note; + dest = user_note; + reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); + if (given_reason.length > 0) + reason += " (" + given_reason + ")"; + } + else { + source = user_note; + dest = special_note; + reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); + if (given_reason.length > 0) + reason += " (" + given_reason + ")"; + } + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": 1, + "amount": 100 * $("#amount").val(), + "reason": reason, + "valid": true, + "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "SpecialTransaction", + "source": source, + "destination": dest, + "last_name": $("#last_name").val(), + "first_name": $("#first_name").val(), + "bank": $("#bank").val() + }, function () { + addMsg("Le crédit/retrait a bien été effectué !", "success"); + reset(); + }).fail(function (err) { + addMsg("Le crédit/transfert a échoué : " + err.responseText, "danger"); + reset(); + }); + } +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 6814bedfba9b3783255ffe7df3fd3b1a826889db..e61937021c6f566f9b3125cc00dac9de9e66634c 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 %} @@ -46,12 +46,20 @@ SPDX-License-Identifier: GPL-3.0-or-later crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js" crossorigin="anonymous"></script> + <script src="/static/js/base.js"></script> {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {% if form.media %} {{ form.media }} {% endif %} + <style> + .validate:hover { + cursor: pointer; + text-decoration: underline; + } + </style> + {% block extracss %}{% endblock %} </head> <body class="d-flex w-100 h-100 flex-column"> @@ -67,23 +75,27 @@ SPDX-License-Identifier: GPL-3.0-or-later <div class="collapse navbar-collapse" id="navbarNavDropdown"> <ul class="navbar-nav"> <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> Consos</a> + <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> + </li> + <li class="nav-item active"> + <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> </li> <li class="nav-item active"> - <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> Clubs</a> + <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> </li> <li class="nav-item active"> - <a class="nav-link" href="#"><i class="fa fa-calendar"></i> Activités</a> + <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> </li> <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> Bouton</a> + <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> </li> </ul> <ul class="navbar-nav ml-auto"> {% if user.is_authenticated %} <li class="dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> - <i class="fa fa-user"></i> {{ user.username }} ({{ user.note.balance | pretty_money }}) + <i class="fa fa-user"></i> + <span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink"> @@ -112,6 +124,7 @@ SPDX-License-Identifier: GPL-3.0-or-later </nav> <div class="container-fluid my-3" style="max-width: 1600px;"> {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} + <div id="messages"></div> {% block content %} <p>Default content...</p> {% endblock content %} @@ -125,7 +138,7 @@ SPDX-License-Identifier: GPL-3.0-or-later class="form-inline"> <span class="text-muted mr-1"> NoteKfet2020 — - <a href="mailto:tresorie.bde@lists.crans.org" + <a href="mailto:{{ "CONTACT_EMAIL" | getenv }}" class="text-muted">Nous contacter</a> — </span> {% csrf_token %} @@ -155,6 +168,10 @@ SPDX-License-Identifier: GPL-3.0-or-later </div> </footer> +<script> + CSRF_TOKEN = "{{ csrf_token }}"; +</script> + {% block extrajavascript %} {% endblock extrajavascript %} </body> diff --git a/templates/cas_server/base.html b/templates/cas_server/base.html new file mode 100644 index 0000000000000000000000000000000000000000..4e93cee08f82590ca0f08977a24bf33efd260fe1 --- /dev/null +++ b/templates/cas_server/base.html @@ -0,0 +1,99 @@ +{% load i18n %}{% load static %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html> +<html{% if LANGUAGE_CODE %} lang="{{LANGUAGE_CODE}}"{% endif %}> + <head> + <meta charset="utf-8"> + <!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]--> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>{% block title %}{% trans "Central Authentication Service" %}{% endblock %}</title> + <link href="{{settings.CAS_COMPONENT_URLS.bootstrap3_css}}" rel="stylesheet"> + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> + <!--[if lt IE 9]> + <script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script> + <script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script> + <![endif]--> + {% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %} + <link href="{% static "cas_server/styles.css" %}" rel="stylesheet"> + </head> + <body> + <div id="wrap"> + <div class="container"> + {% if auto_submit %}<noscript>{% endif %} + <div class="row"> + <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> + <h1 id="app-name"> + {% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}" alt="cas-logo" />{% endif %} + Authentification Note Kfet 2020</h1> + </div> + </div> + {% if auto_submit %}</noscript>{% endif %} + <div class="row"> + <div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div> + <div class="col-lg-6 col-md-6 col-sm-8 col-xs-12"> + {% if auto_submit %}<noscript>{% endif %} + {% for msg in CAS_INFO_RENDER %} + <div class="alert alert-{{msg.type}}{% if msg.discardable %} alert-dismissable{% endif %}"> + {% if msg.discardable %}<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="info-{{msg.name}}">×</button>{% endif %} + <p>{{msg.message}}</p> + </div> + {% endfor %} + {% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %} + <div class="alert alert-info alert-dismissable"> + <button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">×</button> + <p>{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}</p> + </div> + {% endif %} + {% block ante_messages %}{% endblock %} + {% for message in messages %} + <div {% spaceless %} + {% if message.level == message_levels.DEBUG %} + class="alert alert-warning" + {% elif message.level == message_levels.INFO %} + class="alert alert-info" + {% elif message.level == message_levels.SUCCESS %} + class="alert alert-success" + {% elif message.level == message_levels.WARNING %} + class="alert alert-warning" + {% else %} + class="alert alert-danger" + {% endif %} + {% endspaceless %}> + <p>{{message}}</p> + </div> + {% endfor %} + {% if auto_submit %}</noscript>{% endif %} + {% block content %}{% endblock %} + </div> + <div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div> + </div> + </div> <!-- /container --> + </div> + <div style="clear: both;"></div> + {% if settings.CAS_SHOW_POWERED %} + <div id="footer"> + <p><a class="text-muted" href="https://pypi.org/project/django-cas-server/">django-cas-server powered</a></p> + </div> + {% endif %} + <script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script> + <script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script> + <script src="{% static "cas_server/functions.js" %}"></script> + <script type="text/javascript"> +{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %} +discard_and_remember("#alert-version", "cas-alert-version", "{{LAST_VERSION}}"); +{% endif %} +{% for msg in CAS_INFO_RENDER %} +{% if msg.discardable %} +discard_and_remember("#info-{{msg.name}}", "cas-info-{{msg.name}}", "{{msg.hash}}"); +{% endif %} +{% endfor %} +{% block javascript_inline %}{% endblock %} +</script> + {% block javascript %}{% endblock %} + </body> +</html> +<!-- +Powered by django-cas-server version {{VERSION}} + +Pypi: https://pypi.org/project/django-cas-server/ +github: https://github.com/nitmir/django-cas-server +--> diff --git a/templates/cas_server/form.html b/templates/cas_server/form.html new file mode 100644 index 0000000000000000000000000000000000000000..405dedd12fc1c35ca35827cc456aea95b0d23f17 --- /dev/null +++ b/templates/cas_server/form.html @@ -0,0 +1,26 @@ +{% load cas_server %} +{% for error in form.non_field_errors %} +<div class="alert alert-danger alert-dismissable"> + <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> + {{error}} +</div> +{% endfor %} +{% for field in form %}{% if not field|is_hidden %} +<div class="form-group + {% if not form.non_field_errors %} + {% if field.errors %} has-error + {% elif form.cleaned_data %} has-success + {% endif %} + {% endif %}" +>{% spaceless %} + {% if field|is_checkbox %} + <div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div> + {% else %} + <label class="control-label" for="{{field.auto_id}}">{{field.label}}</label> + {{field}} + {% endif %} + {% for error in field.errors %} + <span class="help-block">{{error}}</span> + {% endfor %} +{% endspaceless %}</div> +{% else %}{{field}}{% endif %}{% endfor %} diff --git a/templates/cas_server/logged.html b/templates/cas_server/logged.html new file mode 100644 index 0000000000000000000000000000000000000000..46e1c9a8824ba03c0796cb1f263a58b6a8f99e38 --- /dev/null +++ b/templates/cas_server/logged.html @@ -0,0 +1,21 @@ +{% extends "cas_server/base.html" %} +{% load i18n %} +{% block content %} +<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div> +<form class="form-signin" method="get" action="logout"> + <div class="checkbox"> + <label> + <input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %} + </label> + </div> + {% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %} + <div class="checkbox"> + <label> + <input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %} + </label> + </div> + {% endif %} + <button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button> +</form> +{% endblock %} + diff --git a/templates/cas_server/login.html b/templates/cas_server/login.html new file mode 100644 index 0000000000000000000000000000000000000000..ddc2eb32aa80727c714e0f7b87c8fd912de0bc39 --- /dev/null +++ b/templates/cas_server/login.html @@ -0,0 +1,33 @@ +{% extends "cas_server/base.html" %} +{% load i18n %} + +{% block ante_messages %} +{% if auto_submit %}<noscript>{% endif %} +<h2 class="form-signin-heading">{% trans "Please log in" %}</h2> +{% if auto_submit %}</noscript>{% endif %} +{% endblock %} +{% block content %} + <div class="alert alert-warning"> + {% trans "If you don't have any Note Kfet account, please follow <a href='/accounts/signup'>this link to sign up</a>." %} + </div> +<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}> + {% csrf_token %} + {% include "cas_server/form.html" %} + {% if auto_submit %}<noscript>{% endif %} + <button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button> + {% if auto_submit %}</noscript>{% endif %} +</form> +{% endblock %} +{% block javascript_inline %} +jQuery(function( $ ){ + $("#id_warn").click(function(e){ + if($("#id_warn").is(':checked')){ + createCookie("warn", "on", 10 * 365); + } else { + eraseCookie("warn"); + } + }); +});{% if auto_submit %} +document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %} +{% endblock %} + diff --git a/templates/cas_server/logout.html b/templates/cas_server/logout.html new file mode 100644 index 0000000000000000000000000000000000000000..8069337678aa7272d2d10ab066647774f52a9720 --- /dev/null +++ b/templates/cas_server/logout.html @@ -0,0 +1,7 @@ +{% extends "cas_server/base.html" %} +{% load static %} +{% load i18n %} +{% block content %} +<div class="alert alert-success" role="alert">{{logout_msg}}</div> +{% endblock %} + diff --git a/templates/cas_server/proxy.xml b/templates/cas_server/proxy.xml new file mode 100644 index 0000000000000000000000000000000000000000..ab51d89a89c9f59f9a57d7ecf421db644d5dc0b3 --- /dev/null +++ b/templates/cas_server/proxy.xml @@ -0,0 +1,5 @@ +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:proxySuccess> + <cas:proxyTicket>{{ticket}}</cas:proxyTicket> + </cas:proxySuccess> + </cas:serviceResponse> diff --git a/templates/cas_server/samlValidate.xml b/templates/cas_server/samlValidate.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b130fd2c7cf2d47c62e1793dcc55d8292d6f040 --- /dev/null +++ b/templates/cas_server/samlValidate.xml @@ -0,0 +1,59 @@ +<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header /> + <SOAP-ENV:Body> + <Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" + xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" + IssueInstant="{{ IssueInstant }}" + MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}" + ResponseID="{{ ResponseID }}"> + <Status> + <StatusCode Value="samlp:Success"> + </StatusCode> + </Status> + <Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="{{ResponseID}}" + IssueInstant="{{IssueInstant}}" Issuer="localhost" MajorVersion="1" + MinorVersion="1"> + <Conditions NotBefore="{{IssueInstant}}" NotOnOrAfter="{{expireInstant}}"> + <AudienceRestrictionCondition> + <Audience> + {{Recipient}} + </Audience> + </AudienceRestrictionCondition> + </Conditions> + <AttributeStatement> + <Subject> + <NameIdentifier>{{username}}</NameIdentifier> + <SubjectConfirmation> + <ConfirmationMethod> + urn:oasis:names:tc:SAML:1.0:cm:artifact + </ConfirmationMethod> + </SubjectConfirmation> + </Subject> + <Attribute AttributeName="authenticationDate" AttributeNamespace="http://www.ja-sig.org/products/cas/"> + <AttributeValue>{{auth_date}}</AttributeValue> + </Attribute> + <Attribute AttributeName="longTermAuthenticationRequestTokenUsed" AttributeNamespace="http://www.ja-sig.org/products/cas/"> + <AttributeValue>false</AttributeValue>{# we do not support long-term (Remember-Me) auth #} + </Attribute> + <Attribute AttributeName="isFromNewLogin" AttributeNamespace="http://www.ja-sig.org/products/cas/"> + <AttributeValue>{{is_new_login}}</AttributeValue> + </Attribute> +{% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/"> + <AttributeValue>{{value}}</AttributeValue> + </Attribute> +{% endfor %} </AttributeStatement> + <AuthenticationStatement AuthenticationInstant="{{IssueInstant}}" + AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password"> + <Subject> + <NameIdentifier>{{username}}</NameIdentifier> + <SubjectConfirmation> + <ConfirmationMethod> + urn:oasis:names:tc:SAML:1.0:cm:artifact + </ConfirmationMethod> + </SubjectConfirmation> + </Subject> + </AuthenticationStatement> + </Assertion> + </Response> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> diff --git a/templates/cas_server/samlValidateError.xml b/templates/cas_server/samlValidateError.xml new file mode 100644 index 0000000000000000000000000000000000000000..c72daba1d01329b2f72d7f4a39c2ca1c70d88bda --- /dev/null +++ b/templates/cas_server/samlValidateError.xml @@ -0,0 +1,14 @@ +<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Header /> + <SOAP-ENV:Body> + <Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" + xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" + IssueInstant="{{ IssueInstant }}" + MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}" + ResponseID="{{ ResponseID }}"> + <Status> + <StatusCode Value="samlp:{{code}}">{{msg}}</StatusCode> + </Status> + </Response> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> diff --git a/templates/cas_server/serviceValidate.xml b/templates/cas_server/serviceValidate.xml new file mode 100644 index 0000000000000000000000000000000000000000..f583dbeace3f43da2a04a89b40e0cadaf5868d32 --- /dev/null +++ b/templates/cas_server/serviceValidate.xml @@ -0,0 +1,19 @@ +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationSuccess> + <cas:user>{{username}}</cas:user> + <cas:attributes> + <cas:authenticationDate>{{auth_date}}</cas:authenticationDate> + <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>{# we do not support long-term (Remember-Me) auth #} + <cas:isFromNewLogin>{{is_new_login}}</cas:isFromNewLogin> +{% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}> +{% endfor %} </cas:attributes> + <cas:attribute name="authenticationDate" value="{{auth_date}}"/> + <cas:attribute name="longTermAuthenticationRequestTokenUsed" value="false"/> + <cas:attribute name="isFromNewLogin" value="{{is_new_login}}"/> +{% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/> +{% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket> +{% endif %}{% if proxies %} <cas:proxies> +{% for proxy in proxies %} <cas:proxy>{{proxy}}</cas:proxy> +{% endfor %} </cas:proxies> +{% endif %} </cas:authenticationSuccess> +</cas:serviceResponse> diff --git a/templates/cas_server/serviceValidateError.xml b/templates/cas_server/serviceValidateError.xml new file mode 100644 index 0000000000000000000000000000000000000000..cab8d9bdd82b288b6a79f44acafff24c6c4f0d1c --- /dev/null +++ b/templates/cas_server/serviceValidateError.xml @@ -0,0 +1,3 @@ +<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> + <cas:authenticationFailure code="{{code}}">{{msg}}</cas:authenticationFailure> +</cas:serviceResponse> diff --git a/templates/cas_server/warn.html b/templates/cas_server/warn.html new file mode 100644 index 0000000000000000000000000000000000000000..4f80b15a2c460304034f877b565986681eb530a1 --- /dev/null +++ b/templates/cas_server/warn.html @@ -0,0 +1,11 @@ +{% extends "cas_server/base.html" %} +{% load static %} +{% load i18n %} + +{% block content %} + <form class="form-signin" method="post"> +{% csrf_token %} +{% include "cas_server/form.html" %} +<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button> + </form> +{% endblock %} 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 0000000000000000000000000000000000000000..171767c086cc5fe7f96816d6b0a2c2bfc50b1d08 --- /dev/null +++ b/templates/django_filters/rest_framework/crispy_form.html @@ -0,0 +1,5 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +<h2>{% trans "Field filters" %}</h2> +{% 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 0000000000000000000000000000000000000000..b116e35317537ecf43046b79a4ca7525d1dc80c0 --- /dev/null +++ b/templates/django_filters/rest_framework/form.html @@ -0,0 +1,6 @@ +{% load i18n %} +<h2>{% trans "Field filters" %}</h2> +<form class="form" action="" method="get"> + {{ filter.form.as_p }} + <button type="submit" class="btn btn-primary">{% trans "Submit" %}</button> +</form> diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html new file mode 100644 index 0000000000000000000000000000000000000000..089ddb20c9fccebb7562d4cb0c400ba0a6f3020c --- /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 3fc2dd8be1aa4916e6425659c2b79decad67e3b9..577297bbc12eb39b0f0457ae8c65945ffc71750f 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 %} -<p><a class="btn btn-default" href="{% url 'note:template_list' %}">Template Listing</a></p> +<p><a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Clubs list" %}</a></p> <form method="post"> {% csrf_token %} {{form|crispy}} -<button class="btn btn-primary" type="submit">Submit</button> +<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> </form> {% endblock %} diff --git a/templates/member/club_list.html b/templates/member/club_list.html index f807c25ce8fd252ecf0a06b3d93e5312b351a6a3..165711136329c1a8ccbdf12880c33499541c0436 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 %} -<a class="btn btn-primary" href="{% url 'member:club_create' %}">New Club</a> +<a class="btn btn-primary" href="{% url 'member:club_create' %}">{% trans "New club" %}</a> {% endblock %} {% block extrajavascript %} diff --git a/templates/member/profile_detail.html b/templates/member/profile_detail.html index e997b333005d051cd33b371666ff5c6b5fd4d773..31510acfb8f8d71511880fa3126cb62f0665dcf5 100644 --- a/templates/member/profile_detail.html +++ b/templates/member/profile_detail.html @@ -10,7 +10,7 @@ <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" > </a> </div> - <div class="card-body"> + <div class="card-body" id="profile_infos"> <dl class="row"> <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd> @@ -76,7 +76,9 @@ </a> </div> <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> - {% render_table history_list %} + <div id="history_list"> + {% render_table history_list %} + </div> </div> </div> </div> @@ -84,3 +86,12 @@ </div> </div> {% endblock %} + +{% block extrajavascript %} + <script> + function refreshHistory() { + $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); + $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); + } + </script> +{% endblock %} diff --git a/templates/member/signup.html b/templates/member/signup.html index e682bd9b8be5c6c327f8b17cdf4d7a7f07a3da96..d7b3c23ef8bb0fba2baebc04e178ecf1bc59ab78 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 %} - <h2>Sign up</h2> + <h2>{% trans "Sign up" %}</h2> <form method="post"> {% csrf_token %} {{ form|crispy }} {{ profile_form|crispy }} <button class="btn btn-success" type="submit"> - {% trans "Sign Up" %} + {% trans "Sign up" %} </button> </form> {% endblock %} diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html index 10b06589cfd4434aa6bc79100307901d017b1547..b108a96f83f81df9588c1c992c719b152bc7a0d8 100644 --- a/templates/note/conso_form.html +++ b/templates/note/conso_form.html @@ -1,97 +1,171 @@ {% extends "base.html" %} -{% load i18n static pretty_money %} +{% load i18n static pretty_money django_tables2 %} {# Remove page title #} {% block contenttitle %}{% endblock %} {% block content %} - {# Regroup buttons under categories #} - {% regroup transaction_templates by category as categories %} + <div class="row mt-4"> + <div class="col-sm-5 col-md-4" id="infos_div"> + <div class="row"> + {# User details column #} + <div class="col-xl-5" id="note_infos_div"> + <div class="card border-success shadow mb-4"> + <img src="/media/pic/default.png" + id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> + <div class="card-body text-center"> + <span id="user_note"></span> + </div> + </div> + </div> - <form method="post" onsubmit="window.onbeforeunload=null"> - {% csrf_token %} + {# User selection column #} + <div class="col-xl-7" id="user_select_div"> + <div class="card border-success shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Select emitters" %} + </p> + </div> + <ul class="list-group list-group-flush" id="note_list"> + </ul> + <div class="card-body"> + <input class="form-control mx-auto d-block" type="text" id="note" /> + <ul class="list-group list-group-flush" id="alias_matched"> + </ul> + </div> + </div> + </div> - <div class="row"> - <div class="col-sm-5 mb-4"> - {% if form.non_field_errors %} - <p class="errornote"> - {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} - </p> - {% endif %} - {% for field in form %} - <div class="form-row{% if field.errors %} errors{% endif %}"> - {{ field.errors }} - <div> - {{ field.label_tag }} - {% if field.is_readonly %} - <div class="readonly">{{ field.contents }}</div> - {% else %} - {{ field }} - {% endif %} - {% if field.field.help_text %} - <div class="help">{{ field.field.help_text|safe }}</div> - {% endif %} + <div class="col-xl-5" id="consos_list_div"> + <div class="card border-info shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Select consumptions" %} + </p> </div> + <ul class="list-group list-group-flush" id="consos_list"> + </ul> + <button id="consume_all" class="form-control btn btn-primary"> + {% trans "Consume!" %} + </button> </div> - {% endfor %} + </div> </div> + </div> - <div class="col-sm-7"> - <div class="card text-center shadow"> - {# Tabs for button categories #} - <div class="card-header"> - <ul class="nav nav-tabs nav-fill card-header-tabs"> - {% for category in categories %} - <li class="nav-item"> - <a class="nav-link" data-toggle="tab" href="#{{ category.grouper|slugify }}"> - {{ category.grouper }} - </a> - </li> - {% endfor %} - </ul> + {# Buttons column #} + <div class="col-sm-7 col-md-8" id="buttons_div"> + {# Show last used buttons #} + <div class="card shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Most used buttons" %} + </p> + </div> + <div class="card-body text-nowrap" style="overflow:auto hidden"> + <div class="d-inline-flex flex-wrap justify-content-center" id="most_used"> + {% for button in most_used %} + {% if button.display %} + <button class="btn btn-outline-dark rounded-0 flex-fill" + id="most_used_button{{ button.id }}" name="button" value="{{ button.name }}"> + {{ button.name }} ({{ button.amount | pretty_money }}) + </button> + {% endif %} + {% endfor %} </div> + </div> + </div> + + {# Regroup buttons under categories #} + {% regroup transaction_templates by category as categories %} + + <div class="card border-primary text-center shadow mb-4"> + {# Tabs for button categories #} + <div class="card-header"> + <ul class="nav nav-tabs nav-fill card-header-tabs"> + {% for category in categories %} + <li class="nav-item"> + <a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.grouper|slugify }}"> + {{ category.grouper }} + </a> + </li> + {% endfor %} + </ul> + </div> - {# Tabs content #} - <div class="card-body"> - <div class="tab-content"> - {% for category in categories %} - <div class="tab-pane" id="{{ category.grouper|slugify }}"> - <div class="d-inline-flex flex-wrap justify-content-center"> - {% for button in category.list %} + {# Tabs content #} + <div class="card-body"> + <div class="tab-content"> + {% for category in categories %} + <div class="tab-pane" id="{{ category.grouper|slugify }}"> + <div class="d-inline-flex flex-wrap justify-content-center"> + {% for button in category.list %} + {% if button.display %} <button class="btn btn-outline-dark rounded-0 flex-fill" - name="button" value="{{ button.name }}"> + id="button{{ button.id }}" name="button" value="{{ button.name }}"> {{ button.name }} ({{ button.amount | pretty_money }}) </button> - {% endfor %} - </div> + {% endif %} + {% endfor %} </div> - {% endfor %} - </div> + </div> + {% endfor %} + </div> + </div> + + {# Mode switch #} + <div class="card-footer border-primary"> + <a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}"> + <i class="fa fa-edit"></i> {% trans "Edit" %} + </a> + <div class="btn-group btn-group-toggle float-right" data-toggle="buttons"> + <label for="single_conso" class="btn btn-sm btn-outline-primary active"> + <input type="radio" name="conso_type" id="single_conso" checked> + {% trans "Single consumptions" %} + </label> + <label for="double_conso" class="btn btn-sm btn-outline-primary"> + <input type="radio" name="conso_type" id="double_conso"> + {% trans "Double consumptions" %} + </label> </div> </div> </div> </div> - </form> + </div> + + <div class="card shadow mb-4" id="history"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Recent transactions history" %} + </p> + </div> + {% render_table table %} + </div> {% endblock %} {% block extrajavascript %} + <script type="text/javascript" src="/static/js/consos.js"></script> <script type="text/javascript"> - $(document).ready(function() { - // If hash of a category in the URL, then select this category - // else select the first one - if (location.hash) { - $("a[href='" + location.hash + "']").tab("show"); - } else { - $("a[data-toggle='tab']").first().tab("show"); - } + {% for button in most_used %} + {% if button.display %} + $("#most_used_button{{ button.id }}").click(function() { + addConso({{ button.destination.id }}, {{ button.amount }}, + {{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}", + {{ button.id }}, "{{ button.name }}"); + }); + {% endif %} + {% endfor %} - // When selecting a category, change URL - $(document.body).on("click", "a[data-toggle='tab']", function(event) { - location.hash = this.getAttribute("href"); - }); - }); + {% for button in transaction_templates %} + {% if button.display %} + $("#button{{ button.id }}").click(function() { + addConso({{ button.destination.id }}, {{ button.amount }}, + {{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}", + {{ button.id }}, "{{ button.name }}"); + }); + {% endif %} + {% endfor %} </script> {% endblock %} diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index ff8504bc157666e021a4698c430db81163ebcd28..f320083e08dca5c1151d7bd0036a185328349746 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -3,35 +3,188 @@ SPDX-License-Identifier: GPL-2.0-or-later {% endcomment %} -{% load i18n static %} +{% load i18n static django_tables2 %} {% block content %} - <form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %} - {% if form.non_field_errors %} - <p class="errornote"> - {% for error in form.non_field_errors %} - {{ error }} - {% endfor %} - </p> - {% endif %} - <fieldset class="module aligned"> - {% for field in form %} - <div class="form-row{% if field.errors %} errors{% endif %}"> - {{ field.errors }} - <div> - {{ field.label_tag }} - {% if field.is_readonly %} - <div class="readonly">{{ field.contents }}</div> - {% else %} - {{ field }} - {% endif %} - {% if field.field.help_text %} - <div class="help">{{ field.field.help_text|safe }}</div> - {% endif %} + + <div class="row"> + <div class="col-xl-12"> + <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons"> + <label for="type_gift" class="btn btn-sm btn-outline-primary active"> + <input type="radio" name="transaction_type" id="type_gift" checked> + {% trans "Gift" %} + </label> + <label for="type_transfer" class="btn btn-sm btn-outline-primary"> + <input type="radio" name="transaction_type" id="type_transfer"> + {% trans "Transfer" %} + </label> + <label for="type_credit" class="btn btn-sm btn-outline-primary"> + <input type="radio" name="transaction_type" id="type_credit"> + {% trans "Credit" %} + </label> + <label type="type_debit" class="btn btn-sm btn-outline-primary"> + <input type="radio" name="transaction_type" id="type_debit"> + {% trans "Debit" %} + </label> + </div> + </div> + </div> + + <div class="row"> + <div class="col-md-4" id="emitters_div" style="display: none;"> + <div class="card border-success shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Select emitters" %} + </p> + </div> + <ul class="list-group list-group-flush" id="source_note_list"> + </ul> + <div class="card-body"> + <input class="form-control mx-auto d-block" type="text" id="source_note" /> + <ul class="list-group list-group-flush" id="source_alias_matched"> + </ul> + </div> + </div> + </div> + + <div class="col-xl-4" id="note_infos_div"> + <div class="card border-success shadow mb-4"> + <img src="/media/pic/default.png" + id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> + <div class="card-body text-center"> + <span id="user_note"></span> + </div> + </div> + </div> + + <div class="col-md-4" id="external_div" style="display: none;"> + <div class="card border-success shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "External payment" %} + </p> + </div> + <ul class="list-group list-group-flush" id="source_note_list"> + </ul> + <div class="card-body"> + <div class="form-row"> + <div class="col-md-12"> + <label for="credit_type">{% trans "Transfer type" %} :</label> + <select id="credit_type" class="custom-select"> + {% for special_type in special_types %} + <option value="{{ special_type.id }}">{{ special_type.special_type }}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-row"> + <div class="col-md-12"> + <label for="last_name">{% trans "Name" %} :</label> + <input type="text" id="last_name" class="form-control" /> + </div> + </div> + <div class="form-row"> + <div class="col-md-12"> + <label for="first_name">{% trans "First name" %} :</label> + <input type="text" id="first_name" class="form-control" /> + </div> </div> + <div class="form-row"> + <div class="col-md-12"> + <label for="bank">{% trans "Bank" %} :</label> + <input type="text" id="bank" class="form-control" /> + </div> + </div> + </div> + </div> + </div> + + <div class="col-md-8" id="dests_div"> + <div class="card border-info shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold" id="dest_title"> + {% trans "Select receivers" %} + </p> </div> - {% endfor %} - </fieldset> - <input type="submit" value="{% trans 'Transfer' %}"> - </form> + <ul class="list-group list-group-flush" id="dest_note_list"> + </ul> + <div class="card-body"> + <input class="form-control mx-auto d-block" type="text" id="dest_note" /> + <ul class="list-group list-group-flush" id="dest_alias_matched"> + </ul> + </div> + </div> + </div> + </div> + + + <div class="form-row"> + <div class="form-group col-md-6"> + <label for="amount">{% trans "Amount" %} :</label> + <div class="input-group"> + <input class="form-control mx-auto d-block" type="number" min="0" step="0.01" id="amount" /> + <div class="input-group-append"> + <span class="input-group-text">€</span> + </div> + </div> + </div> + + <div class="form-group col-md-6"> + <label for="reason">{% trans "Reason" %} :</label> + <input class="form-control mx-auto d-block" type="text" id="reason" required /> + </div> + </div> + + <div class="form-row"> + <div class="col-md-12"> + <button id="transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button> + </div> + </div> + + <div class="card shadow mb-4" id="history"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Recent transactions history" %} + </p> + </div> + {% render_table table %} + </div> +{% endblock %} + +{% block extrajavascript %} + <script> + TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }}; + SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }}; + user_id = {{ user.note.pk }}; + + $("#type_gift").click(function() { + $("#emitters_div").hide(); + $("#external_div").hide(); + $("#dests_div").attr('class', 'col-md-8'); + $("#dest_title").text("{% trans "Select receivers" %}"); + }); + + $("#type_transfer").click(function() { + $("#emitters_div").show(); + $("#external_div").hide(); + $("#dests_div").attr('class', 'col-md-4'); + $("#dest_title").text("{% trans "Select receivers" %}"); + }); + + $("#type_credit").click(function() { + $("#emitters_div").hide(); + $("#external_div").show(); + $("#dests_div").attr('class', 'col-md-4'); + $("#dest_title").text("{% trans "Credit note" %}"); + }); + + $("#type_debit").click(function() { + $("#emitters_div").hide(); + $("#external_div").show(); + $("#dests_div").attr('class', 'col-md-4'); + $("#dest_title").text("{% trans "Debit note" %}"); + }); + </script> + <script src="/static/js/transfer.js"></script> {% endblock %} diff --git a/templates/note/transactiontemplate_form.html b/templates/note/transactiontemplate_form.html index 3fc2dd8be1aa4916e6425659c2b79decad67e3b9..1f9a574a050ff49056f3a6931a00636cb951b747 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 %} -<p><a class="btn btn-default" href="{% url 'note:template_list' %}">Template Listing</a></p> +<p><a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Buttons list" %}</a></p> <form method="post"> {% csrf_token %} {{form|crispy}} diff --git a/templates/note/transactiontemplate_list.html b/templates/note/transactiontemplate_list.html index 62e4d164794c9995153c7aeedb8def8a381b8e74..4960023694b30179bc3423c30c4a451fa3642a8b 100644 --- a/templates/note/transactiontemplate_list.html +++ b/templates/note/transactiontemplate_list.html @@ -15,7 +15,7 @@ <td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td> <td>{{ object.destination }}</td> <td>{{ object.amount | pretty_money }}</td> - <td>{{ object.template_type }}</td> + <td>{{ object.category }}</td> </tr> {% endfor %} </table> diff --git a/templates/registration/login.html b/templates/registration/login.html index 04ef8d7deb25d939ef1ca6f1eb6ab9995416ed69..5a4322d138ea110f8089d6c9a928d579740f476c 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -17,6 +17,10 @@ SPDX-License-Identifier: GPL-2.0-or-later </p> {% endif %} + <div class="alert alert-info"> + Vous pouvez aussi vous connecter via l'authentification centralisée <a href="{% url 'cas_login' %}">en suivant ce lien.</a> + </div> + <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} {{ form | crispy }} <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary"> diff --git a/tox.ini b/tox.ini index c4e88c786dc93b3d03e10e9b775644415267a2a1..2217b6bfbf407fc4056ac13140ae604e32bfd87f 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,10 @@ skipsdist = True setenv = PYTHONWARNINGS = all deps = - -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements/base.txt + -r{toxinidir}/requirements/api.txt + -r{toxinidir}/requirements/cas.txt + -r{toxinidir}/requirements/production.txt coverage commands = ./manage.py makemigrations @@ -18,7 +21,10 @@ commands = [testenv:linters] deps = - -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements/base.txt + -r{toxinidir}/requirements/api.txt + -r{toxinidir}/requirements/cas.txt + -r{toxinidir}/requirements/production.txt flake8 flake8-colors flake8-import-order @@ -26,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