mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	Merge branch 'master' into manage_button
This commit is contained in:
		
							
								
								
									
										13
									
								
								.env_example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.env_example
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					[submodule "apps/scripts"]
 | 
				
			||||||
 | 
						path = apps/scripts
 | 
				
			||||||
 | 
						url = git@gitlab.crans.org:bde/nk20-scripts.git
 | 
				
			||||||
@@ -9,13 +9,13 @@ RUN apt update && \
 | 
				
			|||||||
    apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
 | 
					    apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
 | 
				
			||||||
    rm -rf /var/lib/apt/lists/*
 | 
					    rm -rf /var/lib/apt/lists/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY requirements.txt /code/
 | 
					COPY . /code/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Comment what is not needed
 | 
				
			||||||
RUN pip install -r requirements/base.txt
 | 
					RUN pip install -r requirements/base.txt
 | 
				
			||||||
RUN pip install -r requirements/api.txt
 | 
					RUN pip install -r requirements/api.txt
 | 
				
			||||||
RUN pip install -r requirements/cas.txt
 | 
					RUN pip install -r requirements/cas.txt
 | 
				
			||||||
RUN pip install -r requirements/production.txt
 | 
					RUN pip install -r requirements/production.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY . /code/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ENTRYPOINT ["/code/entrypoint.sh"]
 | 
					ENTRYPOINT ["/code/entrypoint.sh"]
 | 
				
			||||||
EXPOSE 8000
 | 
					EXPOSE 8000
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										60
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								README.md
									
									
									
									
									
								
							@@ -32,6 +32,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
 | 
				
			|||||||
        $ python3 -m venv env
 | 
					        $ python3 -m venv env
 | 
				
			||||||
        $ source env/bin/activate
 | 
					        $ source env/bin/activate
 | 
				
			||||||
        (env)$ pip3 install -r requirements/base.txt
 | 
					        (env)$ pip3 install -r requirements/base.txt
 | 
				
			||||||
 | 
					        (env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite un base postgres
 | 
				
			||||||
        (env)$ deactivate
 | 
					        (env)$ deactivate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. uwsgi  et Nginx
 | 
					4. uwsgi  et Nginx
 | 
				
			||||||
@@ -40,14 +41,13 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $ cp nginx_note.conf_example nginx_note.conf
 | 
					        $ cp nginx_note.conf_example nginx_note.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
***Modifier le fichier pour être en accord avec le reste de votre config***
 | 
					    ***Modifier le fichier pour être en accord avec le reste de votre config***
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    On utilise uwsgi et Nginx pour gérer le coté serveur :
 | 
					    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/
 | 
					        $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,7 +85,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
 | 
				
			|||||||
        postgres=# CREATE DATABASE note_db OWNER note;
 | 
					        postgres=# CREATE DATABASE note_db OWNER note;
 | 
				
			||||||
        CREATE DATABASE
 | 
					        CREATE DATABASE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Si tout va bien:
 | 
					    Si tout va bien :
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        postgres=#\list
 | 
					        postgres=#\list
 | 
				
			||||||
        List of databases
 | 
					        List of databases
 | 
				
			||||||
@@ -96,22 +96,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
 | 
					         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
 | 
					         template1 | postgres | UTF8     | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres  +postgres=CTc/postgres
 | 
				
			||||||
        (4 rows)
 | 
					        (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
 | 
					6. Variable d'environnement et Migrations
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					    On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet 
 | 
				
			||||||
 | 
					    et on renseigne des secrets et des paramètres :
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        DJANGO_APP_STAGE="dev" # ou "prod" 
 | 
				
			||||||
 | 
					        DJANGO_DEV_STORE_METHOD="sqllite" # ou "postgres"
 | 
				
			||||||
 | 
					        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" # note.example.com
 | 
				
			||||||
 | 
					        CONTACT_EMAIL="tresorerie.bde@localhost"
 | 
				
			||||||
 | 
					        NOTE_URL="localhost" # serveur cas note.example.com si auto-hébergé.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
 | 
					    Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $ source /env/bin/activate
 | 
					        $ 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 makemigrations
 | 
				
			||||||
        (env)$ ./manage.py migrate
 | 
					        (env)$ ./manage.py migrate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,17 +133,21 @@ Il est possible de travailler sur une instance Docker.
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
        $ git clone git@gitlab.crans.org:bde/nk20.git
 | 
					        $ 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 :
 | 
					   ajouter les lignes suivantes, en les adaptant à la configuration voulue :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        nk20:
 | 
					        nk20:
 | 
				
			||||||
          build: /chemin/vers/nk20
 | 
					          build: /chemin/vers/nk20
 | 
				
			||||||
          volumes:
 | 
					          volumes:
 | 
				
			||||||
            - /chemin/vers/nk20:/code/
 | 
					            - /chemin/vers/nk20:/code/
 | 
				
			||||||
 | 
					          env_file: /chemin/vers/nk20/.env
 | 
				
			||||||
          restart: always
 | 
					          restart: always
 | 
				
			||||||
          labels:
 | 
					          labels:
 | 
				
			||||||
            - traefik.domain=ndd.exemple.com
 | 
					            - traefik.domain=ndd.example.com
 | 
				
			||||||
            - traefik.frontend.rule=Host:ndd.exemple.com
 | 
					            - traefik.frontend.rule=Host:ndd.example.com
 | 
				
			||||||
            - traefik.port=8000
 | 
					            - traefik.port=8000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. Enjoy :
 | 
					3. Enjoy :
 | 
				
			||||||
@@ -157,19 +168,22 @@ un serveur de développement par exemple sur son ordinateur.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $ python3 -m venv venv
 | 
					        $ python3 -m venv venv
 | 
				
			||||||
        $ source venv/bin/activate
 | 
					        $ source venv/bin/activate
 | 
				
			||||||
        (env)$ pip install -r requirements.txt
 | 
					        (env)$ pip install -r requirements/base.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 makemigrations
 | 
				
			||||||
        (env)$ ./manage.py migrate
 | 
					        (env)$ ./manage.py migrate
 | 
				
			||||||
        (env)$ ./manage.py loaddata initial
 | 
					        (env)$ ./manage.py loaddata initial
 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. Créer un super-utilisateur :
 | 
					5. Créer un super-utilisateur :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (env)$ ./manage.py createsuperuser
 | 
					        (env)$ ./manage.py createsuperuser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
5. Enjoy :
 | 
					6. Enjoy :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (env)$ ./manage.py runserver 0.0.0.0:8000
 | 
					        (env)$ ./manage.py runserver 0.0.0.0:8000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -184,4 +198,4 @@ Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
 | 
				
			|||||||
## Documentation
 | 
					## Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
La documentation est générée par django et son module admindocs.
 | 
					La documentation est générée par django et son module admindocs.
 | 
				
			||||||
**Commenter votre code !**
 | 
					**Commentez votre code !**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,15 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import viewsets
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
 | 
					from rest_framework.filters import SearchFilter
 | 
				
			||||||
 | 
					from api.viewsets import ReadProtectedModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
 | 
					from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
 | 
				
			||||||
from ..models import ActivityType, Activity, Guest
 | 
					from ..models import ActivityType, Activity, Guest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActivityTypeViewSet(viewsets.ModelViewSet):
 | 
					class ActivityTypeViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -15,9 +17,11 @@ class ActivityTypeViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = ActivityType.objects.all()
 | 
					    queryset = ActivityType.objects.all()
 | 
				
			||||||
    serializer_class = ActivityTypeSerializer
 | 
					    serializer_class = ActivityTypeSerializer
 | 
				
			||||||
 | 
					    filter_backends = [DjangoFilterBackend]
 | 
				
			||||||
 | 
					    filterset_fields = ['name', 'can_invite', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActivityViewSet(viewsets.ModelViewSet):
 | 
					class ActivityViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -25,9 +29,11 @@ class ActivityViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Activity.objects.all()
 | 
					    queryset = Activity.objects.all()
 | 
				
			||||||
    serializer_class = ActivitySerializer
 | 
					    serializer_class = ActivitySerializer
 | 
				
			||||||
 | 
					    filter_backends = [DjangoFilterBackend]
 | 
				
			||||||
 | 
					    filterset_fields = ['name', 'description', 'activity_type', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GuestViewSet(viewsets.ModelViewSet):
 | 
					class GuestViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -35,3 +41,5 @@ class GuestViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Guest.objects.all()
 | 
					    queryset = Guest.objects.all()
 | 
				
			||||||
    serializer_class = GuestSerializer
 | 
					    serializer_class = GuestSerializer
 | 
				
			||||||
 | 
					    filter_backends = [SearchFilter]
 | 
				
			||||||
 | 
					    search_fields = ['$name', ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,10 +3,17 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.conf.urls import url, include
 | 
					from django.conf.urls import url, include
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
from rest_framework import routers, serializers, viewsets
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
 | 
					from rest_framework import routers, serializers
 | 
				
			||||||
 | 
					from rest_framework.filters import SearchFilter
 | 
				
			||||||
 | 
					from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
				
			||||||
from activity.api.urls import register_activity_urls
 | 
					from activity.api.urls import register_activity_urls
 | 
				
			||||||
 | 
					from api.viewsets import ReadProtectedModelViewSet
 | 
				
			||||||
from member.api.urls import register_members_urls
 | 
					from member.api.urls import register_members_urls
 | 
				
			||||||
from note.api.urls import register_note_urls
 | 
					from note.api.urls import register_note_urls
 | 
				
			||||||
 | 
					from logs.api.urls import register_logs_urls
 | 
				
			||||||
 | 
					from permission.api.urls import register_permission_urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSerializer(serializers.ModelSerializer):
 | 
					class UserSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -24,7 +31,18 @@ class UserSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserViewSet(viewsets.ModelViewSet):
 | 
					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(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -32,15 +50,32 @@ class UserViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = User.objects.all()
 | 
					    queryset = User.objects.all()
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    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', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This ViewSet is the only one that is accessible from all authenticated users!
 | 
				
			||||||
 | 
					class ContentTypeViewSet(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.
 | 
					# Routers provide an easy way of automatically determining the URL conf.
 | 
				
			||||||
# Register each app API router and user viewset
 | 
					# Register each app API router and user viewset
 | 
				
			||||||
router = routers.DefaultRouter()
 | 
					router = routers.DefaultRouter()
 | 
				
			||||||
 | 
					router.register('models', ContentTypeViewSet)
 | 
				
			||||||
router.register('user', UserViewSet)
 | 
					router.register('user', UserViewSet)
 | 
				
			||||||
register_members_urls(router, 'members')
 | 
					register_members_urls(router, 'members')
 | 
				
			||||||
register_activity_urls(router, 'activity')
 | 
					register_activity_urls(router, 'activity')
 | 
				
			||||||
register_note_urls(router, 'note')
 | 
					register_note_urls(router, 'note')
 | 
				
			||||||
 | 
					register_permission_urls(router, 'permission')
 | 
				
			||||||
 | 
					register_logs_urls(router, 'logs')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app_name = 'api'
 | 
					app_name = 'api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					# 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 permission.backends import PermissionBackend
 | 
				
			||||||
 | 
					from rest_framework import viewsets
 | 
				
			||||||
 | 
					from note_kfet.middlewares import get_current_authenticated_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReadProtectedModelViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Protect a ModelViewSet by filtering the objects that the user cannot see.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
 | 
				
			||||||
 | 
					        user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					        self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
 | 
				
			||||||
 | 
					        user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					        self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
 | 
				
			||||||
							
								
								
									
										0
									
								
								apps/logs/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/logs/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								apps/logs/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/logs/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										11
									
								
								apps/logs/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/logs/api/urls.py
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
							
								
								
									
										23
									
								
								apps/logs/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/logs/api/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -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.filters import OrderingFilter
 | 
				
			||||||
 | 
					from api.viewsets import ReadOnlyProtectedModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .serializers import ChangelogSerializer
 | 
				
			||||||
 | 
					from ..models import Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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', ]
 | 
				
			||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import AppConfig
 | 
					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 _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,4 +12,7 @@ class LogsConfig(AppConfig):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def ready(self):
 | 
					    def ready(self):
 | 
				
			||||||
        # noinspection PyUnresolvedReferences
 | 
					        # 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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,6 +56,12 @@ class Changelog(models.Model):
 | 
				
			|||||||
        max_length=16,
 | 
					        max_length=16,
 | 
				
			||||||
        null=False,
 | 
					        null=False,
 | 
				
			||||||
        blank=False,
 | 
					        blank=False,
 | 
				
			||||||
 | 
					        choices=[
 | 
				
			||||||
 | 
					            ('create', _('create')),
 | 
				
			||||||
 | 
					            ('edit', _('edit')),
 | 
				
			||||||
 | 
					            ('delete', _('delete')),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        default='edit',
 | 
				
			||||||
        verbose_name=_('action'),
 | 
					        verbose_name=_('action'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,67 +1,39 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import inspect
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
from django.core import serializers
 | 
					from rest_framework.renderers import JSONRenderer
 | 
				
			||||||
from django.db.models.signals import pre_save, post_save, post_delete
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from note.models import NoteUser, Alias
 | 
				
			||||||
 | 
					from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Changelog
 | 
					from .models import Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import getpass
 | 
				
			||||||
def get_request_in_signal(sender):
 | 
					 | 
				
			||||||
    req = None
 | 
					 | 
				
			||||||
    for entry in reversed(inspect.stack()):
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            req = entry[0].f_locals['request']
 | 
					 | 
				
			||||||
            # Check if there is a user
 | 
					 | 
				
			||||||
            # noinspection PyStatementEffect
 | 
					 | 
				
			||||||
            req.user
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not req:
 | 
					 | 
				
			||||||
        print("WARNING: Attempt to save " + str(sender) + " with no user")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return req
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_user_and_ip(sender):
 | 
					 | 
				
			||||||
    req = get_request_in_signal(sender)
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        user = req.user
 | 
					 | 
				
			||||||
        if 'HTTP_X_FORWARDED_FOR' in req.META:
 | 
					 | 
				
			||||||
            ip = req.META.get('HTTP_X_FORWARDED_FOR')
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            ip = req.META.get('REMOTE_ADDR')
 | 
					 | 
				
			||||||
    except:
 | 
					 | 
				
			||||||
        user = None
 | 
					 | 
				
			||||||
        ip = None
 | 
					 | 
				
			||||||
    return user, ip
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ces modèles ne nécessitent pas de logs
 | 
				
			||||||
EXCLUDED = [
 | 
					EXCLUDED = [
 | 
				
			||||||
    'admin.logentry',
 | 
					    'admin.logentry',
 | 
				
			||||||
    'authtoken.token',
 | 
					    'authtoken.token',
 | 
				
			||||||
 | 
					    'cas_server.proxygrantingticket',
 | 
				
			||||||
 | 
					    'cas_server.proxyticket',
 | 
				
			||||||
 | 
					    'cas_server.serviceticket',
 | 
				
			||||||
    'cas_server.user',
 | 
					    'cas_server.user',
 | 
				
			||||||
    'cas_server.userattributes',
 | 
					    'cas_server.userattributes',
 | 
				
			||||||
    'contenttypes.contenttype',
 | 
					    'contenttypes.contenttype',
 | 
				
			||||||
    'logs.changelog',
 | 
					    'logs.changelog',  # Never remove this line
 | 
				
			||||||
    'migrations.migration',
 | 
					    'migrations.migration',
 | 
				
			||||||
    'note.noteuser',
 | 
					    'note.note'  # We only store the subclasses
 | 
				
			||||||
    'note.noteclub',
 | 
					    'note.transaction',
 | 
				
			||||||
    'note.notespecial',
 | 
					 | 
				
			||||||
    'sessions.session',
 | 
					    'sessions.session',
 | 
				
			||||||
    'reversion.revision',
 | 
					 | 
				
			||||||
    'reversion.version',
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(pre_save)
 | 
					 | 
				
			||||||
def pre_save_object(sender, instance, **kwargs):
 | 
					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()
 | 
					    qs = sender.objects.filter(pk=instance.pk).all()
 | 
				
			||||||
    if qs.exists():
 | 
					    if qs.exists():
 | 
				
			||||||
        instance._previous = qs.get()
 | 
					        instance._previous = qs.get()
 | 
				
			||||||
@@ -69,30 +41,51 @@ def pre_save_object(sender, instance, **kwargs):
 | 
				
			|||||||
        instance._previous = None
 | 
					        instance._previous = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save)
 | 
					 | 
				
			||||||
def save_object(sender, instance, **kwargs):
 | 
					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
 | 
					    # noinspection PyProtectedMember
 | 
				
			||||||
    if instance._meta.label_lower in EXCLUDED:
 | 
					    if instance._meta.label_lower in EXCLUDED:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # noinspection PyProtectedMember
 | 
				
			||||||
    previous = instance._previous
 | 
					    previous = instance._previous
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user, ip = get_user_and_ip(sender)
 | 
					    # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
 | 
				
			||||||
 | 
					    user, ip = get_current_authenticated_user(), get_current_ip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from django.contrib.auth.models import AnonymousUser
 | 
					    if user is None:
 | 
				
			||||||
    if isinstance(user, AnonymousUser):
 | 
					        # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
 | 
				
			||||||
        user = None
 | 
					        # 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:
 | 
					    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:
 | 
					        if instance.last_login != previous.last_login:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None
 | 
					    # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
 | 
				
			||||||
    instance_json = serializers.serialize('json', [instance, ])[1:-1]
 | 
					    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:
 | 
					    if previous_json == instance_json:
 | 
				
			||||||
        # No modification
 | 
					        # Pas de log s'il n'y a pas de modification
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Changelog.objects.create(user=user,
 | 
					    Changelog.objects.create(user=user,
 | 
				
			||||||
@@ -105,15 +98,38 @@ def save_object(sender, instance, **kwargs):
 | 
				
			|||||||
                             ).save()
 | 
					                             ).save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_delete)
 | 
					 | 
				
			||||||
def delete_object(sender, instance, **kwargs):
 | 
					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
 | 
					    # noinspection PyProtectedMember
 | 
				
			||||||
    if instance._meta.label_lower in EXCLUDED:
 | 
					    if instance._meta.label_lower in EXCLUDED:
 | 
				
			||||||
        return
 | 
					        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,
 | 
					    Changelog.objects.create(user=user,
 | 
				
			||||||
                             ip=ip,
 | 
					                             ip=ip,
 | 
				
			||||||
                             model=ContentType.objects.get_for_model(instance),
 | 
					                             model=ContentType.objects.get_for_model(instance),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ class ProfileSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Profile
 | 
					        model = Profile
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					        read_only_fields = ('user', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubSerializer(serializers.ModelSerializer):
 | 
					class ClubSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,14 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import viewsets
 | 
					from rest_framework.filters import SearchFilter
 | 
				
			||||||
 | 
					from api.viewsets import ReadProtectedModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
 | 
					from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
 | 
				
			||||||
from ..models import Profile, Club, Role, Membership
 | 
					from ..models import Profile, Club, Role, Membership
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProfileViewSet(viewsets.ModelViewSet):
 | 
					class ProfileViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -17,7 +18,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    serializer_class = ProfileSerializer
 | 
					    serializer_class = ProfileSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubViewSet(viewsets.ModelViewSet):
 | 
					class ClubViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -25,9 +26,11 @@ class ClubViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Club.objects.all()
 | 
					    queryset = Club.objects.all()
 | 
				
			||||||
    serializer_class = ClubSerializer
 | 
					    serializer_class = ClubSerializer
 | 
				
			||||||
 | 
					    filter_backends = [SearchFilter]
 | 
				
			||||||
 | 
					    search_fields = ['$name', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RoleViewSet(viewsets.ModelViewSet):
 | 
					class RoleViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -35,9 +38,11 @@ class RoleViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Role.objects.all()
 | 
					    queryset = Role.objects.all()
 | 
				
			||||||
    serializer_class = RoleSerializer
 | 
					    serializer_class = RoleSerializer
 | 
				
			||||||
 | 
					    filter_backends = [SearchFilter]
 | 
				
			||||||
 | 
					    search_fields = ['$name', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MembershipViewSet(viewsets.ModelViewSet):
 | 
					class MembershipViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,12 +6,21 @@ from crispy_forms.helper import FormHelper
 | 
				
			|||||||
from crispy_forms.layout import Layout
 | 
					from crispy_forms.layout import Layout
 | 
				
			||||||
from dal import autocomplete
 | 
					from dal import autocomplete
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
from django.contrib.auth.forms import UserCreationForm
 | 
					from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
 | 
					from permission.models import PermissionMask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Profile, Club, Membership
 | 
					from .models import Profile, Club, Membership
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CustomAuthenticationForm(AuthenticationForm):
 | 
				
			||||||
 | 
					    permission_mask = forms.ModelChoiceField(
 | 
				
			||||||
 | 
					        label="Masque de permissions",
 | 
				
			||||||
 | 
					        queryset=PermissionMask.objects.order_by("rank"),
 | 
				
			||||||
 | 
					        empty_label=None,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SignUpForm(UserCreationForm):
 | 
					class SignUpForm(UserCreationForm):
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.urls import reverse, reverse_lazy
 | 
					from django.urls import reverse, reverse_lazy
 | 
				
			||||||
@@ -46,6 +48,7 @@ class Profile(models.Model):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _('user profile')
 | 
					        verbose_name = _('user profile')
 | 
				
			||||||
        verbose_name_plural = _('user profile')
 | 
					        verbose_name_plural = _('user profile')
 | 
				
			||||||
 | 
					        indexes = [models.Index(fields=['user'])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        return reverse('user_detail', args=(self.pk,))
 | 
					        return reverse('user_detail', args=(self.pk,))
 | 
				
			||||||
@@ -149,15 +152,13 @@ class Membership(models.Model):
 | 
				
			|||||||
        verbose_name=_('fee'),
 | 
					        verbose_name=_('fee'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def valid(self):
 | 
				
			||||||
 | 
					        if self.date_end is not None:
 | 
				
			||||||
 | 
					            return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _('membership')
 | 
					        verbose_name = _('membership')
 | 
				
			||||||
        verbose_name_plural = _('memberships')
 | 
					        verbose_name_plural = _('memberships')
 | 
				
			||||||
 | 
					        indexes = [models.Index(fields=['user'])]
 | 
				
			||||||
# @receiver(post_save, sender=settings.AUTH_USER_MODEL)
 | 
					 | 
				
			||||||
# def save_user_profile(instance, created, **_kwargs):
 | 
					 | 
				
			||||||
#     """
 | 
					 | 
				
			||||||
#     Hook to save an user profile when an user is updated
 | 
					 | 
				
			||||||
#     """
 | 
					 | 
				
			||||||
#     if created:
 | 
					 | 
				
			||||||
#         Profile.objects.create(user=instance)
 | 
					 | 
				
			||||||
#     instance.profile.save()
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ from django.conf import settings
 | 
				
			|||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
 | 
					from django.contrib.auth.views import LoginView
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.http import HttpResponseRedirect
 | 
					from django.http import HttpResponseRedirect
 | 
				
			||||||
@@ -23,13 +24,23 @@ from note.forms import AliasForm, ImageForm
 | 
				
			|||||||
from note.models import Alias, NoteUser
 | 
					from note.models import Alias, NoteUser
 | 
				
			||||||
from note.models.transactions import Transaction
 | 
					from note.models.transactions import Transaction
 | 
				
			||||||
from note.tables import HistoryTable, AliasTable
 | 
					from note.tables import HistoryTable, AliasTable
 | 
				
			||||||
 | 
					from permission.backends import PermissionBackend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .filters import UserFilter, UserFilterFormHelper
 | 
					from .filters import UserFilter, UserFilterFormHelper
 | 
				
			||||||
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
 | 
					from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
 | 
				
			||||||
 | 
					    CustomAuthenticationForm
 | 
				
			||||||
from .models import Club, Membership
 | 
					from .models import Club, Membership
 | 
				
			||||||
from .tables import ClubTable, UserTable
 | 
					from .tables import ClubTable, UserTable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CustomLoginView(LoginView):
 | 
				
			||||||
 | 
					    form_class = CustomAuthenticationForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def form_valid(self, form):
 | 
				
			||||||
 | 
					        self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
 | 
				
			||||||
 | 
					        return super().form_valid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserCreateView(CreateView):
 | 
					class UserCreateView(CreateView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Une vue pour inscrire un utilisateur et lui créer un profile
 | 
					    Une vue pour inscrire un utilisateur et lui créer un profile
 | 
				
			||||||
@@ -120,11 +131,14 @@ class UserDetailView(LoginRequiredMixin, DetailView):
 | 
				
			|||||||
    context_object_name = "user_object"
 | 
					    context_object_name = "user_object"
 | 
				
			||||||
    template_name = "member/profile_detail.html"
 | 
					    template_name = "member/profile_detail.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
 | 
					        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        user = context['user_object']
 | 
					        user = context['user_object']
 | 
				
			||||||
        history_list = \
 | 
					        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)
 | 
					        context['history_list'] = HistoryTable(history_list)
 | 
				
			||||||
        club_list = \
 | 
					        club_list = \
 | 
				
			||||||
            Membership.objects.all().filter(user=user).only("club")
 | 
					            Membership.objects.all().filter(user=user).only("club")
 | 
				
			||||||
@@ -147,7 +161,7 @@ class UserListView(LoginRequiredMixin, SingleTableView):
 | 
				
			|||||||
    formhelper_class = UserFilterFormHelper
 | 
					    formhelper_class = UserFilterFormHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self, **kwargs):
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
        qs = super().get_queryset()
 | 
					        qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
 | 
				
			||||||
        self.filter = self.filter_class(self.request.GET, queryset=qs)
 | 
					        self.filter = self.filter_class(self.request.GET, queryset=qs)
 | 
				
			||||||
        self.filter.form.helper = self.formhelper_class()
 | 
					        self.filter.form.helper = self.formhelper_class()
 | 
				
			||||||
        return self.filter.qs
 | 
					        return self.filter.qs
 | 
				
			||||||
@@ -203,7 +217,6 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView):
 | 
				
			|||||||
        return HttpResponseRedirect(self.get_success_url())
 | 
					        return HttpResponseRedirect(self.get_success_url())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_success_url(self):
 | 
					    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):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
@@ -297,10 +310,10 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
 | 
				
			|||||||
        if not self.request.user.is_authenticated:
 | 
					        if not self.request.user.is_authenticated:
 | 
				
			||||||
            return User.objects.none()
 | 
					            return User.objects.none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        qs = User.objects.all()
 | 
					        qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.q:
 | 
					        if self.q:
 | 
				
			||||||
            qs = qs.filter(username__regex=self.q)
 | 
					            qs = qs.filter(username__regex="^" + self.q)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return qs
 | 
					        return qs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -328,11 +341,17 @@ class ClubListView(LoginRequiredMixin, SingleTableView):
 | 
				
			|||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    table_class = ClubTable
 | 
					    table_class = ClubTable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
 | 
					        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClubDetailView(LoginRequiredMixin, DetailView):
 | 
					class ClubDetailView(LoginRequiredMixin, DetailView):
 | 
				
			||||||
    model = Club
 | 
					    model = Club
 | 
				
			||||||
    context_object_name = "club"
 | 
					    context_object_name = "club"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
 | 
					        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        club = context["club"]
 | 
					        club = context["club"]
 | 
				
			||||||
@@ -351,6 +370,11 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView):
 | 
				
			|||||||
    form_class = MembershipForm
 | 
					    form_class = MembershipForm
 | 
				
			||||||
    template_name = 'member/add_members.html'
 | 
					    template_name = 'member/add_members.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self, **kwargs):
 | 
				
			||||||
 | 
					        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")
 | 
				
			||||||
 | 
					                                             | PermissionBackend.filter_queryset(self.request.user, Membership,
 | 
				
			||||||
 | 
					                                                                                 "change"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        context['formset'] = MemberFormSet()
 | 
					        context['formset'] = MemberFormSet()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
					from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
				
			||||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
 | 
					from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
 | 
				
			||||||
    TemplateTransaction, MembershipTransaction
 | 
					    RecurrentTransaction, MembershipTransaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AliasInlines(admin.TabularInline):
 | 
					class AliasInlines(admin.TabularInline):
 | 
				
			||||||
@@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    Admin customisation for Transaction
 | 
					    Admin customisation for Transaction
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    child_models = (TemplateTransaction, MembershipTransaction)
 | 
					    child_models = (RecurrentTransaction, MembershipTransaction)
 | 
				
			||||||
    list_display = ('created_at', 'poly_source', 'poly_destination',
 | 
					    list_display = ('created_at', 'poly_source', 'poly_destination',
 | 
				
			||||||
                    'quantity', 'amount', 'valid')
 | 
					                    'quantity', 'amount', 'valid')
 | 
				
			||||||
    list_filter = ('valid',)
 | 
					    list_filter = ('valid',)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,8 @@ from rest_framework import serializers
 | 
				
			|||||||
from rest_polymorphic.serializers import PolymorphicSerializer
 | 
					from rest_polymorphic.serializers import PolymorphicSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
					from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
				
			||||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
 | 
					from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
 | 
				
			||||||
 | 
					    RecurrentTransaction, SpecialTransaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteSerializer(serializers.ModelSerializer):
 | 
					class NoteSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -17,12 +18,7 @@ class NoteSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Note
 | 
					        model = Note
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
        extra_kwargs = {
 | 
					        read_only_fields = [f.name for f in model._meta.get_fields()]  # Notes are read-only protected
 | 
				
			||||||
            'url': {
 | 
					 | 
				
			||||||
                'view_name': 'project-detail',
 | 
					 | 
				
			||||||
                'lookup_field': 'pk'
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteClubSerializer(serializers.ModelSerializer):
 | 
					class NoteClubSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -30,10 +26,15 @@ class NoteClubSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    REST API Serializer for Club's notes.
 | 
					    REST API Serializer for Club's notes.
 | 
				
			||||||
    The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
 | 
					    The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    name = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = NoteClub
 | 
					        model = NoteClub
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					        read_only_fields = ('note', 'club', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_name(self, obj):
 | 
				
			||||||
 | 
					        return str(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteSpecialSerializer(serializers.ModelSerializer):
 | 
					class NoteSpecialSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -41,10 +42,15 @@ class NoteSpecialSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    REST API Serializer for special notes.
 | 
					    REST API Serializer for special notes.
 | 
				
			||||||
    The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
 | 
					    The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    name = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = NoteSpecial
 | 
					        model = NoteSpecial
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					        read_only_fields = ('note', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_name(self, obj):
 | 
				
			||||||
 | 
					        return str(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteUserSerializer(serializers.ModelSerializer):
 | 
					class NoteUserSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -52,10 +58,15 @@ class NoteUserSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    REST API Serializer for User's notes.
 | 
					    REST API Serializer for User's notes.
 | 
				
			||||||
    The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
 | 
					    The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    name = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = NoteUser
 | 
					        model = NoteUser
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					        read_only_fields = ('note', 'user', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_name(self, obj):
 | 
				
			||||||
 | 
					        return str(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AliasSerializer(serializers.ModelSerializer):
 | 
					class AliasSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -67,6 +78,7 @@ class AliasSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Alias
 | 
					        model = Alias
 | 
				
			||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					        read_only_fields = ('note', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
					class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
				
			||||||
@@ -77,6 +89,20 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
				
			|||||||
        NoteSpecial: NoteSpecialSerializer
 | 
					        NoteSpecial: NoteSpecialSerializer
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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):
 | 
					class TransactionTemplateSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -100,6 +126,17 @@ class TransactionSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
        fields = '__all__'
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurrentTransactionSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    REST API Serializer for Transactions.
 | 
				
			||||||
 | 
					    The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = RecurrentTransaction
 | 
				
			||||||
 | 
					        fields = '__all__'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MembershipTransactionSerializer(serializers.ModelSerializer):
 | 
					class MembershipTransactionSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API Serializer for Membership transactions.
 | 
					    REST API Serializer for Membership transactions.
 | 
				
			||||||
@@ -109,3 +146,26 @@ class MembershipTransactionSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = MembershipTransaction
 | 
					        model = MembershipTransaction
 | 
				
			||||||
        fields = '__all__'
 | 
					        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,
 | 
				
			||||||
 | 
					        RecurrentTransaction: RecurrentTransactionSerializer,
 | 
				
			||||||
 | 
					        MembershipTransaction: MembershipTransactionSerializer,
 | 
				
			||||||
 | 
					        SpecialTransaction: SpecialTransactionSerializer,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Transaction
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .views import NotePolymorphicViewSet, AliasViewSet, \
 | 
					from .views import NotePolymorphicViewSet, AliasViewSet, \
 | 
				
			||||||
    TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
 | 
					    TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def register_note_urls(router, path):
 | 
					def register_note_urls(router, path):
 | 
				
			||||||
@@ -12,6 +12,6 @@ def register_note_urls(router, path):
 | 
				
			|||||||
    router.register(path + '/note', NotePolymorphicViewSet)
 | 
					    router.register(path + '/note', NotePolymorphicViewSet)
 | 
				
			||||||
    router.register(path + '/alias', AliasViewSet)
 | 
					    router.register(path + '/alias', AliasViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.register(path + '/transaction/category', TemplateCategoryViewSet)
 | 
				
			||||||
    router.register(path + '/transaction/transaction', TransactionViewSet)
 | 
					    router.register(path + '/transaction/transaction', TransactionViewSet)
 | 
				
			||||||
    router.register(path + '/transaction/template', TransactionTemplateViewSet)
 | 
					    router.register(path + '/transaction/template', TransactionTemplateViewSet)
 | 
				
			||||||
    router.register(path + '/transaction/membership', MembershipTransactionViewSet)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,56 +2,17 @@
 | 
				
			|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
from rest_framework import viewsets
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
 | 
					from rest_framework.filters import OrderingFilter, SearchFilter
 | 
				
			||||||
 | 
					from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
 | 
					from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
 | 
				
			||||||
    NoteUserSerializer, AliasSerializer, \
 | 
					    TransactionTemplateSerializer, TransactionPolymorphicSerializer
 | 
				
			||||||
    TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer
 | 
					from ..models.notes import Note, Alias
 | 
				
			||||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
					from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
 | 
				
			||||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoteViewSet(viewsets.ModelViewSet):
 | 
					class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    REST API View set.
 | 
					 | 
				
			||||||
    The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
 | 
					 | 
				
			||||||
    then render it on /api/note/note/
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    queryset = Note.objects.all()
 | 
					 | 
				
			||||||
    serializer_class = NoteSerializer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NoteClubViewSet(viewsets.ModelViewSet):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    REST API View set.
 | 
					 | 
				
			||||||
    The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
 | 
					 | 
				
			||||||
    then render it on /api/note/club/
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    queryset = NoteClub.objects.all()
 | 
					 | 
				
			||||||
    serializer_class = NoteClubSerializer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    REST API View set.
 | 
					 | 
				
			||||||
    The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
 | 
					 | 
				
			||||||
    then render it on /api/note/special/
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    queryset = NoteSpecial.objects.all()
 | 
					 | 
				
			||||||
    serializer_class = NoteSpecialSerializer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NoteUserViewSet(viewsets.ModelViewSet):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    REST API View set.
 | 
					 | 
				
			||||||
    The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
 | 
					 | 
				
			||||||
    then render it on /api/note/user/
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    queryset = NoteUser.objects.all()
 | 
					 | 
				
			||||||
    serializer_class = NoteUserSerializer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -59,36 +20,27 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Note.objects.all()
 | 
					    queryset = Note.objects.all()
 | 
				
			||||||
    serializer_class = NotePolymorphicSerializer
 | 
					    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):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Parse query and apply filters.
 | 
					        Parse query and apply filters.
 | 
				
			||||||
        :return: The filtered set of requested notes
 | 
					        :return: The filtered set of requested notes
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        queryset = Note.objects.all()
 | 
					        queryset = super().get_queryset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        alias = self.request.query_params.get("alias", ".*")
 | 
					        alias = self.request.query_params.get("alias", ".*")
 | 
				
			||||||
        queryset = queryset.filter(
 | 
					        queryset = queryset.filter(
 | 
				
			||||||
            Q(alias__name__regex=alias)
 | 
					            Q(alias__name__regex="^" + alias)
 | 
				
			||||||
            | Q(alias__normalized_name__regex=alias.lower()))
 | 
					            | Q(alias__normalized_name__regex="^" + Alias.normalize(alias))
 | 
				
			||||||
 | 
					            | Q(alias__normalized_name__regex="^" + alias.lower()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        note_type = self.request.query_params.get("type", None)
 | 
					        return queryset.distinct()
 | 
				
			||||||
        if note_type:
 | 
					 | 
				
			||||||
            types = str(note_type).lower()
 | 
					 | 
				
			||||||
            if "user" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(polymorphic_ctype__model="noteuser")
 | 
					 | 
				
			||||||
            elif "club" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(polymorphic_ctype__model="noteclub")
 | 
					 | 
				
			||||||
            elif "special" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(
 | 
					 | 
				
			||||||
                    polymorphic_ctype__model="notespecial")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                queryset = queryset.none()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return queryset
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AliasViewSet(viewsets.ModelViewSet):
 | 
					class AliasViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -96,6 +48,9 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Alias.objects.all()
 | 
					    queryset = Alias.objects.all()
 | 
				
			||||||
    serializer_class = AliasSerializer
 | 
					    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):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -103,35 +58,30 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
        :return: The filtered set of requested aliases
 | 
					        :return: The filtered set of requested aliases
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        queryset = Alias.objects.all()
 | 
					        queryset = super().get_queryset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        alias = self.request.query_params.get("alias", ".*")
 | 
					        alias = self.request.query_params.get("alias", ".*")
 | 
				
			||||||
        queryset = queryset.filter(
 | 
					        queryset = queryset.filter(
 | 
				
			||||||
            Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
 | 
					            Q(name__regex="^" + alias)
 | 
				
			||||||
 | 
					            | Q(normalized_name__regex="^" + Alias.normalize(alias))
 | 
				
			||||||
        note_id = self.request.query_params.get("note", None)
 | 
					            | Q(normalized_name__regex="^" + alias.lower()))
 | 
				
			||||||
        if note_id:
 | 
					 | 
				
			||||||
            queryset = queryset.filter(id=note_id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        note_type = self.request.query_params.get("type", None)
 | 
					 | 
				
			||||||
        if note_type:
 | 
					 | 
				
			||||||
            types = str(note_type).lower()
 | 
					 | 
				
			||||||
            if "user" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(
 | 
					 | 
				
			||||||
                    note__polymorphic_ctype__model="noteuser")
 | 
					 | 
				
			||||||
            elif "club" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(
 | 
					 | 
				
			||||||
                    note__polymorphic_ctype__model="noteclub")
 | 
					 | 
				
			||||||
            elif "special" in types:
 | 
					 | 
				
			||||||
                queryset = queryset.filter(
 | 
					 | 
				
			||||||
                    note__polymorphic_ctype__model="notespecial")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                queryset = queryset.none()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return queryset
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
					class TemplateCategoryViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
@@ -139,23 +89,17 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = TransactionTemplate.objects.all()
 | 
					    queryset = TransactionTemplate.objects.all()
 | 
				
			||||||
    serializer_class = TransactionTemplateSerializer
 | 
					    serializer_class = TransactionTemplateSerializer
 | 
				
			||||||
 | 
					    filter_backends = [DjangoFilterBackend]
 | 
				
			||||||
 | 
					    filterset_fields = ['name', 'amount', 'display', 'category', ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransactionViewSet(viewsets.ModelViewSet):
 | 
					class TransactionViewSet(ReadProtectedModelViewSet):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    REST API View set.
 | 
					    REST API View set.
 | 
				
			||||||
    The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
 | 
					    The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
 | 
				
			||||||
    then render it on /api/note/transaction/transaction/
 | 
					    then render it on /api/note/transaction/transaction/
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    queryset = Transaction.objects.all()
 | 
					    queryset = Transaction.objects.all()
 | 
				
			||||||
    serializer_class = TransactionSerializer
 | 
					    serializer_class = TransactionPolymorphicSerializer
 | 
				
			||||||
 | 
					    filter_backends = [SearchFilter]
 | 
				
			||||||
 | 
					    search_fields = ['$reason', ]
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,220 +1,259 @@
 | 
				
			|||||||
[
 | 
					[
 | 
				
			||||||
    {
 | 
					  {
 | 
				
			||||||
        "model": "note.note",
 | 
					    "model": "note.note",
 | 
				
			||||||
        "pk": 1,
 | 
					    "pk": 1,
 | 
				
			||||||
        "fields": {
 | 
					    "fields": {
 | 
				
			||||||
            "polymorphic_ctype": 39,
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
            "balance": 0,
 | 
					        "note",
 | 
				
			||||||
            "is_active": true,
 | 
					        "notespecial"
 | 
				
			||||||
            "display_image": "",
 | 
					      ],
 | 
				
			||||||
            "created_at": "2020-02-20T20:02:48.778Z"
 | 
					      "balance": 0,
 | 
				
			||||||
        }
 | 
					      "last_negative": null,
 | 
				
			||||||
    },
 | 
					      "is_active": true,
 | 
				
			||||||
    {
 | 
					      "display_image": "",
 | 
				
			||||||
        "model": "note.note",
 | 
					      "created_at": "2020-02-20T20:02:48.778Z"
 | 
				
			||||||
        "pk": 2,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "polymorphic_ctype": 39,
 | 
					 | 
				
			||||||
            "balance": 0,
 | 
					 | 
				
			||||||
            "is_active": true,
 | 
					 | 
				
			||||||
            "display_image": "",
 | 
					 | 
				
			||||||
            "created_at": "2020-02-20T20:06:39.546Z"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.note",
 | 
					 | 
				
			||||||
        "pk": 3,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "polymorphic_ctype": 39,
 | 
					 | 
				
			||||||
            "balance": 0,
 | 
					 | 
				
			||||||
            "is_active": true,
 | 
					 | 
				
			||||||
            "display_image": "",
 | 
					 | 
				
			||||||
            "created_at": "2020-02-20T20:06:43.049Z"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.note",
 | 
					 | 
				
			||||||
        "pk": 4,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "polymorphic_ctype": 39,
 | 
					 | 
				
			||||||
            "balance": 0,
 | 
					 | 
				
			||||||
            "is_active": true,
 | 
					 | 
				
			||||||
            "display_image": "",
 | 
					 | 
				
			||||||
            "created_at": "2020-02-20T20:06:50.996Z"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.note",
 | 
					 | 
				
			||||||
        "pk": 5,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "polymorphic_ctype": 38,
 | 
					 | 
				
			||||||
            "balance": 0,
 | 
					 | 
				
			||||||
            "is_active": true,
 | 
					 | 
				
			||||||
            "display_image": "",
 | 
					 | 
				
			||||||
            "created_at": "2020-02-20T20:09:38.615Z"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.note",
 | 
					 | 
				
			||||||
        "pk": 6,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "polymorphic_ctype": 38,
 | 
					 | 
				
			||||||
            "balance": 0,
 | 
					 | 
				
			||||||
            "is_active": true,
 | 
					 | 
				
			||||||
            "display_image": "",
 | 
					 | 
				
			||||||
            "created_at": "2020-02-20T20:16:14.753Z"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.notespecial",
 | 
					 | 
				
			||||||
        "pk": 1,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "special_type": "Esp\u00e8ces"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.notespecial",
 | 
					 | 
				
			||||||
        "pk": 2,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "special_type": "Carte bancaire"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.notespecial",
 | 
					 | 
				
			||||||
        "pk": 3,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "special_type": "Ch\u00e8que"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.notespecial",
 | 
					 | 
				
			||||||
        "pk": 4,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "special_type": "Virement bancaire"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.noteclub",
 | 
					 | 
				
			||||||
        "pk": 5,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "club": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.noteclub",
 | 
					 | 
				
			||||||
        "pk": 6,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "club": 2
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 1,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Esp\u00e8ces",
 | 
					 | 
				
			||||||
            "normalized_name": "especes",
 | 
					 | 
				
			||||||
            "note": 1
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 2,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Carte bancaire",
 | 
					 | 
				
			||||||
            "normalized_name": "cartebancaire",
 | 
					 | 
				
			||||||
            "note": 2
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 3,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Ch\u00e8que",
 | 
					 | 
				
			||||||
            "normalized_name": "cheque",
 | 
					 | 
				
			||||||
            "note": 3
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 4,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Virement bancaire",
 | 
					 | 
				
			||||||
            "normalized_name": "virementbancaire",
 | 
					 | 
				
			||||||
            "note": 4
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 5,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "BDE",
 | 
					 | 
				
			||||||
            "normalized_name": "bde",
 | 
					 | 
				
			||||||
            "note": 5
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.alias",
 | 
					 | 
				
			||||||
        "pk": 6,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Kfet",
 | 
					 | 
				
			||||||
            "normalized_name": "kfet",
 | 
					 | 
				
			||||||
            "note": 6
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 1,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Soft"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 2,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Pulls"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 3,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Gala"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 4,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Clubs"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 5,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Bouffe"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 6,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "BDA"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 7,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Autre"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        "model": "note.templatecategory",
 | 
					 | 
				
			||||||
        "pk": 8,
 | 
					 | 
				
			||||||
        "fields": {
 | 
					 | 
				
			||||||
            "name": "Alcool"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "notespecial"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "",
 | 
				
			||||||
 | 
					      "created_at": "2020-02-20T20:06:39.546Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "notespecial"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "",
 | 
				
			||||||
 | 
					      "created_at": "2020-02-20T20:06:43.049Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "notespecial"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "",
 | 
				
			||||||
 | 
					      "created_at": "2020-02-20T20:06:50.996Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "noteclub"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "",
 | 
				
			||||||
 | 
					      "created_at": "2020-02-20T20:09:38.615Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "noteclub"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "",
 | 
				
			||||||
 | 
					      "created_at": "2020-02-20T20:16:14.753Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.note",
 | 
				
			||||||
 | 
					    "pk": 7,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "polymorphic_ctype": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "noteuser"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "balance": 0,
 | 
				
			||||||
 | 
					      "last_negative": null,
 | 
				
			||||||
 | 
					      "is_active": true,
 | 
				
			||||||
 | 
					      "display_image": "pic/default.png",
 | 
				
			||||||
 | 
					      "created_at": "2020-03-22T13:01:35.680Z"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.noteclub",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "club": 1
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.noteclub",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "club": 2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.notespecial",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "special_type": "Esp\u00e8ces"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.notespecial",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "special_type": "Carte bancaire"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.notespecial",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "special_type": "Ch\u00e8que"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.notespecial",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "special_type": "Virement bancaire"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Esp\u00e8ces",
 | 
				
			||||||
 | 
					      "normalized_name": "especes",
 | 
				
			||||||
 | 
					      "note": 1
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Carte bancaire",
 | 
				
			||||||
 | 
					      "normalized_name": "cartebancaire",
 | 
				
			||||||
 | 
					      "note": 2
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Ch\u00e8que",
 | 
				
			||||||
 | 
					      "normalized_name": "cheque",
 | 
				
			||||||
 | 
					      "note": 3
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Virement bancaire",
 | 
				
			||||||
 | 
					      "normalized_name": "virementbancaire",
 | 
				
			||||||
 | 
					      "note": 4
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "BDE",
 | 
				
			||||||
 | 
					      "normalized_name": "bde",
 | 
				
			||||||
 | 
					      "note": 5
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.alias",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Kfet",
 | 
				
			||||||
 | 
					      "normalized_name": "kfet",
 | 
				
			||||||
 | 
					      "note": 6
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Soft"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Pulls"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Gala"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Clubs"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Bouffe"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "BDA"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 7,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Autre"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "note.templatecategory",
 | 
				
			||||||
 | 
					    "pk": 8,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Alcool"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ from django import forms
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Alias
 | 
					from .models import Alias
 | 
				
			||||||
from .models import Transaction, TransactionTemplate, TemplateTransaction
 | 
					from .models import TransactionTemplate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AliasForm(forms.ModelForm):
 | 
					class AliasForm(forms.ModelForm):
 | 
				
			||||||
@@ -50,82 +50,3 @@ class TransactionTemplateForm(forms.ModelForm):
 | 
				
			|||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 "source" not 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.template = button
 | 
					 | 
				
			||||||
        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,
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
					from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
				
			||||||
from .transactions import MembershipTransaction, Transaction, \
 | 
					from .transactions import MembershipTransaction, Transaction, \
 | 
				
			||||||
    TemplateCategory, TransactionTemplate, TemplateTransaction
 | 
					    TemplateCategory, TransactionTemplate, RecurrentTransaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = [
 | 
					__all__ = [
 | 
				
			||||||
    # Notes
 | 
					    # Notes
 | 
				
			||||||
    'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
 | 
					    'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
 | 
				
			||||||
    # Transactions
 | 
					    # Transactions
 | 
				
			||||||
    'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
 | 
					    'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
 | 
				
			||||||
    'TemplateTransaction',
 | 
					    'RecurrentTransaction',
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -209,6 +209,10 @@ class Alias(models.Model):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("alias")
 | 
					        verbose_name = _("alias")
 | 
				
			||||||
        verbose_name_plural = _("aliases")
 | 
					        verbose_name_plural = _("aliases")
 | 
				
			||||||
 | 
					        indexes = [
 | 
				
			||||||
 | 
					            models.Index(fields=['name']),
 | 
				
			||||||
 | 
					            models.Index(fields=['normalized_name']),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
@@ -231,7 +235,7 @@ class Alias(models.Model):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            sim_alias = Alias.objects.get(normalized_name=normalized_name)
 | 
					            sim_alias = Alias.objects.get(normalized_name=normalized_name)
 | 
				
			||||||
            if self != sim_alias:
 | 
					            if self != sim_alias:
 | 
				
			||||||
                raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)),
 | 
					                raise ValidationError(_('An alias with a similar name already exists: {} ').format(sim_alias),
 | 
				
			||||||
                                      code="same_alias"
 | 
					                                      code="same_alias"
 | 
				
			||||||
                                      )
 | 
					                                      )
 | 
				
			||||||
        except Alias.DoesNotExist:
 | 
					        except Alias.DoesNotExist:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ from django.utils import timezone
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from polymorphic.models import PolymorphicModel
 | 
					from polymorphic.models import PolymorphicModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .notes import Note, NoteClub
 | 
					from .notes import Note, NoteClub, NoteSpecial
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Defines transactions
 | 
					Defines transactions
 | 
				
			||||||
@@ -68,6 +68,7 @@ class TransactionTemplate(models.Model):
 | 
				
			|||||||
    description = models.CharField(
 | 
					    description = models.CharField(
 | 
				
			||||||
        verbose_name=_('description'),
 | 
					        verbose_name=_('description'),
 | 
				
			||||||
        max_length=255,
 | 
					        max_length=255,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
@@ -106,7 +107,10 @@ class Transaction(PolymorphicModel):
 | 
				
			|||||||
        verbose_name=_('quantity'),
 | 
					        verbose_name=_('quantity'),
 | 
				
			||||||
        default=1,
 | 
					        default=1,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    amount = models.PositiveIntegerField(verbose_name=_('amount'), )
 | 
					    amount = models.PositiveIntegerField(
 | 
				
			||||||
 | 
					        verbose_name=_('amount'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    reason = models.CharField(
 | 
					    reason = models.CharField(
 | 
				
			||||||
        verbose_name=_('reason'),
 | 
					        verbose_name=_('reason'),
 | 
				
			||||||
        max_length=255,
 | 
					        max_length=255,
 | 
				
			||||||
@@ -119,6 +123,11 @@ class Transaction(PolymorphicModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("transaction")
 | 
					        verbose_name = _("transaction")
 | 
				
			||||||
        verbose_name_plural = _("transactions")
 | 
					        verbose_name_plural = _("transactions")
 | 
				
			||||||
 | 
					        indexes = [
 | 
				
			||||||
 | 
					            models.Index(fields=['created_at']),
 | 
				
			||||||
 | 
					            models.Index(fields=['source']),
 | 
				
			||||||
 | 
					            models.Index(fields=['destination']),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -127,6 +136,7 @@ class Transaction(PolymorphicModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if self.source.pk == self.destination.pk:
 | 
					        if self.source.pk == self.destination.pk:
 | 
				
			||||||
            # When source == destination, no money is transfered
 | 
					            # When source == destination, no money is transfered
 | 
				
			||||||
 | 
					            super().save(*args, **kwargs)
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        created = self.pk is None
 | 
					        created = self.pk is None
 | 
				
			||||||
@@ -142,20 +152,25 @@ class Transaction(PolymorphicModel):
 | 
				
			|||||||
            self.source.balance -= to_transfer
 | 
					            self.source.balance -= to_transfer
 | 
				
			||||||
            self.destination.balance += to_transfer
 | 
					            self.destination.balance += to_transfer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We save first the transaction, in case of the user has no right to transfer money
 | 
				
			||||||
 | 
					        super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Save notes
 | 
					        # Save notes
 | 
				
			||||||
        self.source.save()
 | 
					        self.source.save()
 | 
				
			||||||
        self.destination.save()
 | 
					        self.destination.save()
 | 
				
			||||||
        super().save(*args, **kwargs)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def total(self):
 | 
					    def total(self):
 | 
				
			||||||
        return self.amount * self.quantity
 | 
					        return self.amount * self.quantity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def type(self):
 | 
				
			||||||
 | 
					        return _('Transfer')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TemplateTransaction(Transaction):
 | 
					
 | 
				
			||||||
 | 
					class RecurrentTransaction(Transaction):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
 | 
					    Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
 | 
				
			||||||
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template = models.ForeignKey(
 | 
					    template = models.ForeignKey(
 | 
				
			||||||
@@ -168,6 +183,36 @@ class TemplateTransaction(Transaction):
 | 
				
			|||||||
        on_delete=models.PROTECT,
 | 
					        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):
 | 
					class MembershipTransaction(Transaction):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -184,3 +229,7 @@ class MembershipTransaction(Transaction):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("membership transaction")
 | 
					        verbose_name = _("membership transaction")
 | 
				
			||||||
        verbose_name_plural = _("membership transactions")
 | 
					        verbose_name_plural = _("membership transactions")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def type(self):
 | 
				
			||||||
 | 
					        return _('membership transaction')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,15 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django_tables2 as tables
 | 
					import django_tables2 as tables
 | 
				
			||||||
from django.db.models import F
 | 
					from django.db.models import F
 | 
				
			||||||
from django_tables2.utils import A
 | 
					from django_tables2.utils import A
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models.notes import Alias
 | 
					from .models.notes import Alias
 | 
				
			||||||
from .models.transactions import Transaction, TransactionTemplate
 | 
					from .models.transactions import Transaction
 | 
				
			||||||
from .templatetags.pretty_money import pretty_money
 | 
					from .templatetags.pretty_money import pretty_money
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,17 +20,25 @@ class HistoryTable(tables.Table):
 | 
				
			|||||||
                'table table-condensed table-striped table-hover'
 | 
					                'table table-condensed table-striped table-hover'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        model = Transaction
 | 
					        model = Transaction
 | 
				
			||||||
        exclude = ("polymorphic_ctype", )
 | 
					        exclude = ("id", "polymorphic_ctype", )
 | 
				
			||||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
					        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() !!
 | 
					    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):
 | 
					    def order_total(self, queryset, is_descending):
 | 
				
			||||||
        # needed for rendering
 | 
					        # needed for rendering
 | 
				
			||||||
        queryset = queryset.annotate(total=F('amount') * F('quantity')) \
 | 
					        queryset = queryset.annotate(total=F('amount') * F('quantity')) \
 | 
				
			||||||
            .order_by(('-' if is_descending else '') + 'total')
 | 
					            .order_by(('-' if is_descending else '') + 'total')
 | 
				
			||||||
        return (queryset, True)
 | 
					        return queryset, True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def render_amount(self, value):
 | 
					    def render_amount(self, value):
 | 
				
			||||||
        return pretty_money(value)
 | 
					        return pretty_money(value)
 | 
				
			||||||
@@ -35,6 +46,16 @@ class HistoryTable(tables.Table):
 | 
				
			|||||||
    def render_total(self, value):
 | 
					    def render_total(self, value):
 | 
				
			||||||
        return pretty_money(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 AliasTable(tables.Table):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								apps/note/templatetags/getenv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/note/templatetags/getenv.py
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
@@ -11,7 +11,7 @@ def pretty_money(value):
 | 
				
			|||||||
            abs(value) // 100,
 | 
					            abs(value) // 100,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        return "{:s}{:d} € {:02d}".format(
 | 
					        return "{:s}{:d}.{:02d} €".format(
 | 
				
			||||||
            "- " if value < 0 else "",
 | 
					            "- " if value < 0 else "",
 | 
				
			||||||
            abs(value) // 100,
 | 
					            abs(value) // 100,
 | 
				
			||||||
            abs(value) % 100,
 | 
					            abs(value) % 100,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,16 +3,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from dal import autocomplete
 | 
					from dal import autocomplete
 | 
				
			||||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.urls import reverse
 | 
					 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from django.views.generic import CreateView, ListView, UpdateView
 | 
					from django.views.generic import CreateView, ListView, UpdateView
 | 
				
			||||||
 | 
					 | 
				
			||||||
from django_tables2 import SingleTableView
 | 
					from django_tables2 import SingleTableView
 | 
				
			||||||
 | 
					from permission.backends import PermissionBackend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .forms import TransactionTemplateForm
 | 
				
			||||||
 | 
					from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
 | 
				
			||||||
 | 
					from .models.transactions import SpecialTransaction
 | 
				
			||||||
 | 
					from .tables import HistoryTable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
 | 
					 | 
				
			||||||
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
 | 
					 | 
				
			||||||
from .tables import ButtonTable
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransactionCreateView(LoginRequiredMixin, SingleTableView):
 | 
					class TransactionCreateView(LoginRequiredMixin, SingleTableView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -23,34 +25,27 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    model = Transaction
 | 
					    model = Transaction
 | 
				
			||||||
    form_class = TransactionForm
 | 
					    form_class = TransactionForm
 | 
				
			||||||
 | 
					    # Transaction history table
 | 
				
			||||||
 | 
					    table_class = HistoryTable
 | 
				
			||||||
 | 
					    table_pagination = {"per_page": 50}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
					        return Transaction.objects.filter(PermissionBackend.filter_queryset(
 | 
				
			||||||
 | 
					            self.request.user, Transaction, "view")
 | 
				
			||||||
 | 
					        ).order_by("-id").all()[:50]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Add some context variables in template such as page title
 | 
					        Add some context variables in template such as page title
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        context['title'] = _('Transfer money from your account '
 | 
					        context['title'] = _('Transfer money')
 | 
				
			||||||
                             'to one or others')
 | 
					        context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
 | 
				
			||||||
 | 
					        context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
 | 
				
			||||||
        context['no_cache'] = True
 | 
					        context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context
 | 
					        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):
 | 
					class NoteAutocomplete(autocomplete.Select2QuerySetView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -71,7 +66,7 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # self.q est le paramètre de la recherche
 | 
					        # self.q est le paramètre de la recherche
 | 
				
			||||||
        if self.q:
 | 
					        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()
 | 
					                .order_by('normalized_name').distinct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Filtrage par type de note (user, club, special)
 | 
					        # Filtrage par type de note (user, club, special)
 | 
				
			||||||
@@ -131,31 +126,37 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
 | 
				
			|||||||
    form_class = TransactionTemplateForm
 | 
					    form_class = TransactionTemplateForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConsoView(LoginRequiredMixin, CreateView):
 | 
					class ConsoView(LoginRequiredMixin, SingleTableView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    The Magic View that make people pay their beer and burgers.
 | 
					    The Magic View that make people pay their beer and burgers.
 | 
				
			||||||
    (Most of the magic happens in the dark world of Javascript see consos.js)
 | 
					    (Most of the magic happens in the dark world of Javascript see consos.js)
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    model = TemplateTransaction
 | 
					 | 
				
			||||||
    template_name = "note/conso_form.html"
 | 
					    template_name = "note/conso_form.html"
 | 
				
			||||||
    form_class = ConsoForm
 | 
					
 | 
				
			||||||
 | 
					    # Transaction history table
 | 
				
			||||||
 | 
					    table_class = HistoryTable
 | 
				
			||||||
 | 
					    table_pagination = {"per_page": 50}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
					        return Transaction.objects.filter(
 | 
				
			||||||
 | 
					            PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
 | 
				
			||||||
 | 
					        ).order_by("-id").all()[:50]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Add some context variables in template such as page title
 | 
					        Add some context variables in template such as page title
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        context = super().get_context_data(**kwargs)
 | 
					        context = super().get_context_data(**kwargs)
 | 
				
			||||||
        context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
 | 
					        from django.db.models import Count
 | 
				
			||||||
            .order_by('category')
 | 
					        buttons = TransactionTemplate.objects.filter(
 | 
				
			||||||
        context['title'] = _("Consommations")
 | 
					            PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
 | 
				
			||||||
 | 
					        ).filter(display=True).annotate(clicks=Count('recurrenttransaction')).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(RecurrentTransaction).pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # select2 compatibility
 | 
					        # select2 compatibility
 | 
				
			||||||
        context['no_cache'] = True
 | 
					        context['no_cache'] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_success_url(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        When clicking a button, reload the same page
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return reverse('note:consos')
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,4 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app_name = 'logs'
 | 
					default_app_config = 'permission.apps.PermissionConfig'
 | 
				
			||||||
 | 
					 | 
				
			||||||
# TODO User interface
 | 
					 | 
				
			||||||
urlpatterns = [
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
							
								
								
									
										31
									
								
								apps/permission/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/permission/admin.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-lateré
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Permission, PermissionMask, RolePermissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.register(PermissionMask)
 | 
				
			||||||
 | 
					class PermissionMaskAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Admin customisation for PermissionMask
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    list_display = ('description', 'rank', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.register(Permission)
 | 
				
			||||||
 | 
					class PermissionAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Admin customisation for Permission
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    list_display = ('type', 'model', 'field', 'mask', 'description', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.register(RolePermissions)
 | 
				
			||||||
 | 
					class RolePermissionsAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Admin customisation for RolePermissions
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    list_display = ('role', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..models import Permission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermissionSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    REST API Serializer for Permission types.
 | 
				
			||||||
 | 
					    The djangorestframework plugin will analyse the model `Permission` and parse all fields in the API.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Permission
 | 
				
			||||||
 | 
					        fields = '__all__'
 | 
				
			||||||
							
								
								
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .views import PermissionViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def register_permission_urls(router, path):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Configure router for permission REST API.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    router.register(path, PermissionViewSet)
 | 
				
			||||||
							
								
								
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# 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 api.viewsets import ReadOnlyProtectedModelViewSet
 | 
				
			||||||
 | 
					from .serializers import PermissionSerializer
 | 
				
			||||||
 | 
					from ..models import Permission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermissionViewSet(ReadOnlyProtectedModelViewSet):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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 = Permission.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = PermissionSerializer
 | 
				
			||||||
 | 
					    filter_backends = [DjangoFilterBackend]
 | 
				
			||||||
 | 
					    filterset_fields = ['model', 'type', ]
 | 
				
			||||||
							
								
								
									
										14
									
								
								apps/permission/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/permission/apps.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					from django.db.models.signals import pre_save, pre_delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermissionConfig(AppConfig):
 | 
				
			||||||
 | 
					    name = 'permission'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					        from . import signals
 | 
				
			||||||
 | 
					        pre_save.connect(signals.pre_save_object)
 | 
				
			||||||
 | 
					        pre_delete.connect(signals.pre_delete_object)
 | 
				
			||||||
							
								
								
									
										116
									
								
								apps/permission/backends.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								apps/permission/backends.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.contrib.auth.backends import ModelBackend
 | 
				
			||||||
 | 
					from django.contrib.auth.models import User, AnonymousUser
 | 
				
			||||||
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
 | 
					from django.db.models import Q, F
 | 
				
			||||||
 | 
					from note.models import Note, NoteUser, NoteClub, NoteSpecial
 | 
				
			||||||
 | 
					from note_kfet.middlewares import get_current_session
 | 
				
			||||||
 | 
					from member.models import Membership, Club
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Permission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermissionBackend(ModelBackend):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Manage permissions of users
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    supports_object_permissions = True
 | 
				
			||||||
 | 
					    supports_anonymous_user = False
 | 
				
			||||||
 | 
					    supports_inactive_user = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def permissions(user, model, type):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        List all permissions of the given user that applies to a given model and a give type
 | 
				
			||||||
 | 
					        :param user: The owner of the permissions
 | 
				
			||||||
 | 
					        :param model: The model that the permissions shoud apply
 | 
				
			||||||
 | 
					        :param type: The type of the permissions: view, change, add or delete
 | 
				
			||||||
 | 
					        :return: A generator of the requested permissions
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
 | 
				
			||||||
 | 
					                .filter(
 | 
				
			||||||
 | 
					            rolepermissions__role__membership__user=user,
 | 
				
			||||||
 | 
					            model__app_label=model.app_label,  # For polymorphic models, we don't filter on model type
 | 
				
			||||||
 | 
					            type=type,
 | 
				
			||||||
 | 
					        ).all():
 | 
				
			||||||
 | 
					            if not isinstance(model, permission.model.__class__):
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            club = Club.objects.get(pk=permission.club)
 | 
				
			||||||
 | 
					            permission = permission.about(
 | 
				
			||||||
 | 
					                user=user,
 | 
				
			||||||
 | 
					                club=club,
 | 
				
			||||||
 | 
					                User=User,
 | 
				
			||||||
 | 
					                Club=Club,
 | 
				
			||||||
 | 
					                Membership=Membership,
 | 
				
			||||||
 | 
					                Note=Note,
 | 
				
			||||||
 | 
					                NoteUser=NoteUser,
 | 
				
			||||||
 | 
					                NoteClub=NoteClub,
 | 
				
			||||||
 | 
					                NoteSpecial=NoteSpecial,
 | 
				
			||||||
 | 
					                F=F,
 | 
				
			||||||
 | 
					                Q=Q
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if permission.mask.rank <= get_current_session().get("permission_mask", 0):
 | 
				
			||||||
 | 
					                yield permission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def filter_queryset(user, model, t, field=None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Filter a queryset by considering the permissions of a given user.
 | 
				
			||||||
 | 
					        :param user: The owner of the permissions that are fetched
 | 
				
			||||||
 | 
					        :param model: The concerned model of the queryset
 | 
				
			||||||
 | 
					        :param t: The type of modification (view, add, change, delete)
 | 
				
			||||||
 | 
					        :param field: The field of the model to test, if concerned
 | 
				
			||||||
 | 
					        :return: A query that corresponds to the filter to give to a queryset
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if user is None or isinstance(user, AnonymousUser):
 | 
				
			||||||
 | 
					            # Anonymous users can't do anything
 | 
				
			||||||
 | 
					            return Q(pk=-1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if user.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
 | 
				
			||||||
 | 
					            # Superusers have all rights
 | 
				
			||||||
 | 
					            return Q()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(model, ContentType):
 | 
				
			||||||
 | 
					            model = ContentType.objects.get_for_model(model)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Never satisfied
 | 
				
			||||||
 | 
					        query = Q(pk=-1)
 | 
				
			||||||
 | 
					        perms = PermissionBackend.permissions(user, model, t)
 | 
				
			||||||
 | 
					        for perm in perms:
 | 
				
			||||||
 | 
					            if perm.field and field != perm.field:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            if perm.type != t or perm.model != model:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            perm.update_query()
 | 
				
			||||||
 | 
					            query = query | perm.query
 | 
				
			||||||
 | 
					        return query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def has_perm(self, user_obj, perm, obj=None):
 | 
				
			||||||
 | 
					        if user_obj is None or isinstance(user_obj, AnonymousUser):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if obj is None:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        perm = perm.split('.')[-1].split('_', 2)
 | 
				
			||||||
 | 
					        perm_type = perm[0]
 | 
				
			||||||
 | 
					        perm_field = perm[2] if len(perm) == 3 else None
 | 
				
			||||||
 | 
					        ct = ContentType.objects.get_for_model(obj)
 | 
				
			||||||
 | 
					        if any(permission.applies(obj, perm_type, perm_field)
 | 
				
			||||||
 | 
					               for permission in self.permissions(user_obj, ct, perm_type)):
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def has_module_perms(self, user_obj, app_label):
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_all_permissions(self, user_obj, obj=None):
 | 
				
			||||||
 | 
					        ct = ContentType.objects.get_for_model(obj)
 | 
				
			||||||
 | 
					        return list(self.permissions(user_obj, ct, "view"))
 | 
				
			||||||
							
								
								
									
										653
									
								
								apps/permission/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										653
									
								
								apps/permission/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,653 @@
 | 
				
			|||||||
 | 
					[
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Adh\u00e9rent BDE"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Adh\u00e9rent Kfet"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Pr\u00e9sident\u00b7e BDE"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Tr\u00e9sorier\u00b7\u00e8re BDE"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Respo info"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "GC Kfet"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 7,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Pr\u00e9sident\u00b7e de club"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "member.role",
 | 
				
			||||||
 | 
					    "pk": 8,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "name": "Tr\u00e9sorier\u00b7\u00e8re de club"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permissionmask",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "rank": 0,
 | 
				
			||||||
 | 
					      "description": "Droits basiques"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permissionmask",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "rank": 1,
 | 
				
			||||||
 | 
					      "description": "Droits note seulement"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permissionmask",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "rank": 42,
 | 
				
			||||||
 | 
					      "description": "Tous mes droits"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View our User object"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "member",
 | 
				
			||||||
 | 
					        "profile"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"user\": [\"user\"]}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View our profile"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "noteuser"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View our own note"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "authtoken",
 | 
				
			||||||
 | 
					        "token"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"user\": [\"user\"]}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View our API token"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 5,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View our own transactions"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 6,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "alias"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View aliases of clubs and members of Kfet club"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 7,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "last_login",
 | 
				
			||||||
 | 
					      "description": "Change myself's last login"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 8,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "username",
 | 
				
			||||||
 | 
					      "description": "Change myself's username"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 9,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "first_name",
 | 
				
			||||||
 | 
					      "description": "Change myself's first name"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 10,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "last_name",
 | 
				
			||||||
 | 
					      "description": "Change myself's last name"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 11,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "auth",
 | 
				
			||||||
 | 
					        "user"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "email",
 | 
				
			||||||
 | 
					      "description": "Change myself's email"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 12,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "authtoken",
 | 
				
			||||||
 | 
					        "token"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"user\": [\"user\"]}",
 | 
				
			||||||
 | 
					      "type": "delete",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Delete API Token"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 13,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "authtoken",
 | 
				
			||||||
 | 
					        "token"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"user\": [\"user\"]}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Create API Token"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 14,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "alias"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"note\": [\"user\", \"note\"]}",
 | 
				
			||||||
 | 
					      "type": "delete",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Remove alias"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 15,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "alias"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"note\": [\"user\", \"note\"]}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Add alias"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 16,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "noteuser"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "display_image",
 | 
				
			||||||
 | 
					      "description": "Change myself's display image"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 17,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, {\"amount__lte\": [\"user\", \"note\", \"balance\"]}]",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Transfer from myself's note"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 18,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "note"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "balance",
 | 
				
			||||||
 | 
					      "description": "Update a note balance with a transaction"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 19,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "note"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View notes of club members"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 20,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Create transactions with a club"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 21,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "recurrenttransaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Create transactions from buttons with a club"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 22,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "member",
 | 
				
			||||||
 | 
					        "club"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{\"pk\": [\"club\", \"pk\"]}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View club infos"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 23,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 1,
 | 
				
			||||||
 | 
					      "field": "valid",
 | 
				
			||||||
 | 
					      "description": "Update validation status of a transaction"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 24,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View all transactions"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 25,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "notespecial"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Display credit/debit interface"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 26,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "specialtransaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Create credit/debit transaction"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 27,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "templatecategory"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View button categories"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 28,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "templatecategory"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 3,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Change button category"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 29,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "templatecategory"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 3,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Add button category"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 30,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transactiontemplate"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "view",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "View buttons"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 31,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transactiontemplate"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 3,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Add buttons"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 32,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transactiontemplate"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "change",
 | 
				
			||||||
 | 
					      "mask": 3,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Update buttons"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.permission",
 | 
				
			||||||
 | 
					    "pk": 33,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "model": [
 | 
				
			||||||
 | 
					        "note",
 | 
				
			||||||
 | 
					        "transaction"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "query": "{}",
 | 
				
			||||||
 | 
					      "type": "add",
 | 
				
			||||||
 | 
					      "mask": 2,
 | 
				
			||||||
 | 
					      "field": "",
 | 
				
			||||||
 | 
					      "description": "Create any transaction"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.rolepermissions",
 | 
				
			||||||
 | 
					    "pk": 1,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "role": 1,
 | 
				
			||||||
 | 
					      "permissions": [
 | 
				
			||||||
 | 
					        1,
 | 
				
			||||||
 | 
					        2,
 | 
				
			||||||
 | 
					        7,
 | 
				
			||||||
 | 
					        8,
 | 
				
			||||||
 | 
					        9,
 | 
				
			||||||
 | 
					        10,
 | 
				
			||||||
 | 
					        11
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.rolepermissions",
 | 
				
			||||||
 | 
					    "pk": 2,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "role": 2,
 | 
				
			||||||
 | 
					      "permissions": [
 | 
				
			||||||
 | 
					        1,
 | 
				
			||||||
 | 
					        2,
 | 
				
			||||||
 | 
					        3,
 | 
				
			||||||
 | 
					        4,
 | 
				
			||||||
 | 
					        5,
 | 
				
			||||||
 | 
					        6,
 | 
				
			||||||
 | 
					        7,
 | 
				
			||||||
 | 
					        8,
 | 
				
			||||||
 | 
					        9,
 | 
				
			||||||
 | 
					        10,
 | 
				
			||||||
 | 
					        11,
 | 
				
			||||||
 | 
					        12,
 | 
				
			||||||
 | 
					        13,
 | 
				
			||||||
 | 
					        14,
 | 
				
			||||||
 | 
					        15,
 | 
				
			||||||
 | 
					        16,
 | 
				
			||||||
 | 
					        17,
 | 
				
			||||||
 | 
					        18
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.rolepermissions",
 | 
				
			||||||
 | 
					    "pk": 3,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "role": 8,
 | 
				
			||||||
 | 
					      "permissions": [
 | 
				
			||||||
 | 
					        19,
 | 
				
			||||||
 | 
					        20,
 | 
				
			||||||
 | 
					        21,
 | 
				
			||||||
 | 
					        22
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "model": "permission.rolepermissions",
 | 
				
			||||||
 | 
					    "pk": 4,
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "role": 4,
 | 
				
			||||||
 | 
					      "permissions": [
 | 
				
			||||||
 | 
					        23,
 | 
				
			||||||
 | 
					        24,
 | 
				
			||||||
 | 
					        25,
 | 
				
			||||||
 | 
					        26,
 | 
				
			||||||
 | 
					        27,
 | 
				
			||||||
 | 
					        28,
 | 
				
			||||||
 | 
					        29,
 | 
				
			||||||
 | 
					        30,
 | 
				
			||||||
 | 
					        31,
 | 
				
			||||||
 | 
					        32,
 | 
				
			||||||
 | 
					        33
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										0
									
								
								apps/permission/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										284
									
								
								apps/permission/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								apps/permission/models.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import functools
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import operator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.db.models import F, Q, Model
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from member.models import Role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InstancedPermission:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, model, query, type, field, mask, **kwargs):
 | 
				
			||||||
 | 
					        self.model = model
 | 
				
			||||||
 | 
					        self.raw_query = query
 | 
				
			||||||
 | 
					        self.query = None
 | 
				
			||||||
 | 
					        self.type = type
 | 
				
			||||||
 | 
					        self.field = field
 | 
				
			||||||
 | 
					        self.mask = mask
 | 
				
			||||||
 | 
					        self.kwargs = kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def applies(self, obj, permission_type, field_name=None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns True if the permission applies to
 | 
				
			||||||
 | 
					        the field `field_name` object `obj`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(obj, self.model.model_class()):
 | 
				
			||||||
 | 
					            # The permission does not apply to the model
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.type == 'add':
 | 
				
			||||||
 | 
					            if permission_type == self.type:
 | 
				
			||||||
 | 
					                self.update_query()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Don't increase indexes
 | 
				
			||||||
 | 
					                obj.pk = 0
 | 
				
			||||||
 | 
					                # Force insertion, no data verification, no trigger
 | 
				
			||||||
 | 
					                Model.save(obj, force_insert=True)
 | 
				
			||||||
 | 
					                ret = obj in self.model.model_class().objects.filter(self.query).all()
 | 
				
			||||||
 | 
					                # Delete testing object
 | 
				
			||||||
 | 
					                Model.delete(obj)
 | 
				
			||||||
 | 
					                return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if permission_type == self.type:
 | 
				
			||||||
 | 
					            if self.field and field_name != self.field:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            self.update_query()
 | 
				
			||||||
 | 
					            return obj in self.model.model_class().objects.filter(self.query).all()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update_query(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        The query is not analysed in a first time. It is analysed at most once if needed.
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.query:
 | 
				
			||||||
 | 
					            # noinspection PyProtectedMember
 | 
				
			||||||
 | 
					            self.query = Permission._about(self.raw_query, **self.kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
					        if self.field:
 | 
				
			||||||
 | 
					            return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.__repr__()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermissionMask(models.Model):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Permissions that are hidden behind a mask
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rank = models.PositiveSmallIntegerField(
 | 
				
			||||||
 | 
					        unique=True,
 | 
				
			||||||
 | 
					        verbose_name=_('rank'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    description = models.CharField(
 | 
				
			||||||
 | 
					        max_length=255,
 | 
				
			||||||
 | 
					        unique=True,
 | 
				
			||||||
 | 
					        verbose_name=_('description'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.description
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Permission(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PERMISSION_TYPES = [
 | 
				
			||||||
 | 
					        ('add', 'add'),
 | 
				
			||||||
 | 
					        ('view', 'view'),
 | 
				
			||||||
 | 
					        ('change', 'change'),
 | 
				
			||||||
 | 
					        ('delete', 'delete')
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # A json encoded Q object with the following grammar
 | 
				
			||||||
 | 
					    #  query -> [] | {}  (the empty query representing all objects)
 | 
				
			||||||
 | 
					    #  query -> ["AND", query, …]            AND multiple queries
 | 
				
			||||||
 | 
					    #         | ["OR", query, …]             OR multiple queries
 | 
				
			||||||
 | 
					    #         | ["NOT", query]               Opposite of query
 | 
				
			||||||
 | 
					    #  query -> {key: value, …}              A list of fields and values of a Q object
 | 
				
			||||||
 | 
					    #  key   -> string                       A field name
 | 
				
			||||||
 | 
					    #  value -> int | string | bool | null   Literal values
 | 
				
			||||||
 | 
					    #         | [parameter, …]               A parameter. See compute_param for more details.
 | 
				
			||||||
 | 
					    #         | {"F": oper}                  An F object
 | 
				
			||||||
 | 
					    #  oper  -> [string, …]                  A parameter. See compute_param for more details.
 | 
				
			||||||
 | 
					    #         | ["ADD", oper, …]             Sum multiple F objects or literal
 | 
				
			||||||
 | 
					    #         | ["SUB", oper, oper]          Substract two F objects or literal
 | 
				
			||||||
 | 
					    #         | ["MUL", oper, …]             Multiply F objects or literals
 | 
				
			||||||
 | 
					    #         | int | string | bool | null   Literal values
 | 
				
			||||||
 | 
					    #         | ["F", string]                A field
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # Examples:
 | 
				
			||||||
 | 
					    #  Q(is_superuser=True)  := {"is_superuser": true}
 | 
				
			||||||
 | 
					    #  ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
 | 
				
			||||||
 | 
					    query = models.TextField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mask = models.ForeignKey(
 | 
				
			||||||
 | 
					        PermissionMask,
 | 
				
			||||||
 | 
					        on_delete=models.PROTECT,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    field = models.CharField(max_length=255, blank=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    description = models.CharField(max_length=255, blank=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        unique_together = ('model', 'query', 'type', 'field')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean(self):
 | 
				
			||||||
 | 
					        self.query = json.dumps(json.loads(self.query))
 | 
				
			||||||
 | 
					        if self.field and self.type not in {'view', 'change'}:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Specifying field applies only to view and change permission types."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self, **kwargs):
 | 
				
			||||||
 | 
					        self.full_clean()
 | 
				
			||||||
 | 
					        super().save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def compute_f(oper, **kwargs):
 | 
				
			||||||
 | 
					        if isinstance(oper, list):
 | 
				
			||||||
 | 
					            if oper[0] == 'ADD':
 | 
				
			||||||
 | 
					                return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
				
			||||||
 | 
					            elif oper[0] == 'SUB':
 | 
				
			||||||
 | 
					                return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
 | 
				
			||||||
 | 
					            elif oper[0] == 'MUL':
 | 
				
			||||||
 | 
					                return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
 | 
				
			||||||
 | 
					            elif oper[0] == 'F':
 | 
				
			||||||
 | 
					                return F(oper[1])
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                field = kwargs[oper[0]]
 | 
				
			||||||
 | 
					                for i in range(1, len(oper)):
 | 
				
			||||||
 | 
					                    field = getattr(field, oper[i])
 | 
				
			||||||
 | 
					                return field
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return oper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def compute_param(value, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        A parameter is given by a list. The first argument is the name of the parameter.
 | 
				
			||||||
 | 
					        The parameters are the user, the club, and some classes (Note, ...)
 | 
				
			||||||
 | 
					        If there are more arguments in the list, then attributes are queried.
 | 
				
			||||||
 | 
					        For example, ["user", "note", "balance"] will return the balance of the note of the user.
 | 
				
			||||||
 | 
					        If an argument is a list, then this is interpreted with a function call:
 | 
				
			||||||
 | 
					            First argument is the name of the function, next arguments are parameters, and if there is a dict,
 | 
				
			||||||
 | 
					            then the dict is given as kwargs.
 | 
				
			||||||
 | 
					            For example: NoteUser.objects.filter(user__memberships__club__name="Kfet").all() is translated by:
 | 
				
			||||||
 | 
					            ["NoteUser", "objects", ["filter", {"user__memberships__club__name": "Kfet"}], ["all"]]
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(value, list):
 | 
				
			||||||
 | 
					            return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        field = kwargs[value[0]]
 | 
				
			||||||
 | 
					        for i in range(1, len(value)):
 | 
				
			||||||
 | 
					            if isinstance(value[i], list):
 | 
				
			||||||
 | 
					                if value[i][0] in kwargs:
 | 
				
			||||||
 | 
					                    field = Permission.compute_param(value[i], **kwargs)
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                field = getattr(field, value[i][0])
 | 
				
			||||||
 | 
					                params = []
 | 
				
			||||||
 | 
					                call_kwargs = {}
 | 
				
			||||||
 | 
					                for j in range(1, len(value[i])):
 | 
				
			||||||
 | 
					                    param = Permission.compute_param(value[i][j], **kwargs)
 | 
				
			||||||
 | 
					                    if isinstance(param, dict):
 | 
				
			||||||
 | 
					                        for key in param:
 | 
				
			||||||
 | 
					                            val = Permission.compute_param(param[key], **kwargs)
 | 
				
			||||||
 | 
					                            call_kwargs[key] = val
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        params.append(param)
 | 
				
			||||||
 | 
					                field = field(*params, **call_kwargs)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                field = getattr(field, value[i])
 | 
				
			||||||
 | 
					        return field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _about(query, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Translate JSON query into a Q query.
 | 
				
			||||||
 | 
					        :param query: The JSON query
 | 
				
			||||||
 | 
					        :param kwargs: Additional params
 | 
				
			||||||
 | 
					        :return: A Q object
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if len(query) == 0:
 | 
				
			||||||
 | 
					            # The query is either [] or {} and
 | 
				
			||||||
 | 
					            # applies to all objects of the model
 | 
				
			||||||
 | 
					            # to represent this we return a trivial request
 | 
				
			||||||
 | 
					            return Q(pk=F("pk"))
 | 
				
			||||||
 | 
					        if isinstance(query, list):
 | 
				
			||||||
 | 
					            if query[0] == 'AND':
 | 
				
			||||||
 | 
					                return functools.reduce(operator.and_, [Permission._about(query, **kwargs) for query in query[1:]])
 | 
				
			||||||
 | 
					            elif query[0] == 'OR':
 | 
				
			||||||
 | 
					                return functools.reduce(operator.or_, [Permission._about(query, **kwargs) for query in query[1:]])
 | 
				
			||||||
 | 
					            elif query[0] == 'NOT':
 | 
				
			||||||
 | 
					                return ~Permission._about(query[1], **kwargs)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return Q(pk=F("pk"))
 | 
				
			||||||
 | 
					        elif isinstance(query, dict):
 | 
				
			||||||
 | 
					            q_kwargs = {}
 | 
				
			||||||
 | 
					            for key in query:
 | 
				
			||||||
 | 
					                value = query[key]
 | 
				
			||||||
 | 
					                if isinstance(value, list):
 | 
				
			||||||
 | 
					                    # It is a parameter we query its return value
 | 
				
			||||||
 | 
					                    q_kwargs[key] = Permission.compute_param(value, **kwargs)
 | 
				
			||||||
 | 
					                elif isinstance(value, dict):
 | 
				
			||||||
 | 
					                    # It is an F object
 | 
				
			||||||
 | 
					                    q_kwargs[key] = Permission.compute_f(value['F'], **kwargs)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    q_kwargs[key] = value
 | 
				
			||||||
 | 
					            return Q(**q_kwargs)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # TODO: find a better way to crash here
 | 
				
			||||||
 | 
					            raise Exception("query {} is wrong".format(query))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def about(self, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return an InstancedPermission with the parameters
 | 
				
			||||||
 | 
					        replaced by their values and the query interpreted
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        query = json.loads(self.query)
 | 
				
			||||||
 | 
					        # query = self._about(query, **kwargs)
 | 
				
			||||||
 | 
					        return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        if self.field:
 | 
				
			||||||
 | 
					            return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RolePermissions(models.Model):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Permissions associated with a Role
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    role = models.ForeignKey(
 | 
				
			||||||
 | 
					        Role,
 | 
				
			||||||
 | 
					        on_delete=models.PROTECT,
 | 
				
			||||||
 | 
					        related_name='+',
 | 
				
			||||||
 | 
					        verbose_name=_('role'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    permissions = models.ManyToManyField(
 | 
				
			||||||
 | 
					        Permission,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return str(self.role)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										63
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rest_framework.permissions import DjangoObjectPermissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SAFE_METHODS = ('HEAD', 'OPTIONS', )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StrongDjangoObjectPermissions(DjangoObjectPermissions):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Default DjangoObjectPermissions grant view permission to all.
 | 
				
			||||||
 | 
					    This is a simple patch of this class that controls view access.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    perms_map = {
 | 
				
			||||||
 | 
					        'GET': ['%(app_label)s.view_%(model_name)s'],
 | 
				
			||||||
 | 
					        'OPTIONS': [],
 | 
				
			||||||
 | 
					        'HEAD': [],
 | 
				
			||||||
 | 
					        'POST': ['%(app_label)s.add_%(model_name)s'],
 | 
				
			||||||
 | 
					        'PUT': ['%(app_label)s.change_%(model_name)s'],
 | 
				
			||||||
 | 
					        'PATCH': ['%(app_label)s.change_%(model_name)s'],
 | 
				
			||||||
 | 
					        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_required_object_permissions(self, method, model_cls):
 | 
				
			||||||
 | 
					        kwargs = {
 | 
				
			||||||
 | 
					            'app_label': model_cls._meta.app_label,
 | 
				
			||||||
 | 
					            'model_name': model_cls._meta.model_name
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if method not in self.perms_map:
 | 
				
			||||||
 | 
					            from rest_framework import exceptions
 | 
				
			||||||
 | 
					            raise exceptions.MethodNotAllowed(method)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [perm % kwargs for perm in self.perms_map[method]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def has_object_permission(self, request, view, obj):
 | 
				
			||||||
 | 
					        # authentication checks have already executed via has_permission
 | 
				
			||||||
 | 
					        queryset = self._queryset(view)
 | 
				
			||||||
 | 
					        model_cls = queryset.model
 | 
				
			||||||
 | 
					        user = request.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        perms = self.get_required_object_permissions(request.method, model_cls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not user.has_perms(perms, obj):
 | 
				
			||||||
 | 
					            # If the user does not have permissions we need to determine if
 | 
				
			||||||
 | 
					            # they have read permissions to see 403, or not, and simply see
 | 
				
			||||||
 | 
					            # a 404 response.
 | 
				
			||||||
 | 
					            from django.http import Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if request.method in SAFE_METHODS:
 | 
				
			||||||
 | 
					                # Read permissions already checked and failed, no need
 | 
				
			||||||
 | 
					                # to make another lookup.
 | 
				
			||||||
 | 
					                raise Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            read_perms = self.get_required_object_permissions('GET', model_cls)
 | 
				
			||||||
 | 
					            if not user.has_perms(read_perms, obj):
 | 
				
			||||||
 | 
					                raise Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Has read permissions.
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
							
								
								
									
										106
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.exceptions import PermissionDenied
 | 
				
			||||||
 | 
					from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from logs import signals as logs_signals
 | 
				
			||||||
 | 
					from permission.backends import PermissionBackend
 | 
				
			||||||
 | 
					from note_kfet.middlewares import get_current_authenticated_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EXCLUDED = [
 | 
				
			||||||
 | 
					    'cas_server.proxygrantingticket',
 | 
				
			||||||
 | 
					    'cas_server.proxyticket',
 | 
				
			||||||
 | 
					    'cas_server.serviceticket',
 | 
				
			||||||
 | 
					    'cas_server.user',
 | 
				
			||||||
 | 
					    'cas_server.userattributes',
 | 
				
			||||||
 | 
					    'contenttypes.contenttype',
 | 
				
			||||||
 | 
					    'logs.changelog',
 | 
				
			||||||
 | 
					    'migrations.migration',
 | 
				
			||||||
 | 
					    'sessions.session',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pre_save_object(sender, instance, **kwargs):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Before a model get saved, we check the permissions
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # noinspection PyProtectedMember
 | 
				
			||||||
 | 
					    if instance._meta.label_lower in EXCLUDED:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        # Action performed on shell is always granted
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    qs = sender.objects.filter(pk=instance.pk).all()
 | 
				
			||||||
 | 
					    model_name_full = instance._meta.label_lower.split(".")
 | 
				
			||||||
 | 
					    app_label = model_name_full[0]
 | 
				
			||||||
 | 
					    model_name = model_name_full[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if qs.exists():
 | 
				
			||||||
 | 
					        # We check if the user can change the model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If the user has all right on a model, then OK
 | 
				
			||||||
 | 
					        if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # In the other case, we check if he/she has the right to change one field
 | 
				
			||||||
 | 
					        previous = qs.get()
 | 
				
			||||||
 | 
					        for field in instance._meta.fields:
 | 
				
			||||||
 | 
					            field_name = field.name
 | 
				
			||||||
 | 
					            old_value = getattr(previous, field.name)
 | 
				
			||||||
 | 
					            new_value = getattr(instance, field.name)
 | 
				
			||||||
 | 
					            # If the field wasn't modified, no need to check the permissions
 | 
				
			||||||
 | 
					            if old_value == new_value:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
 | 
				
			||||||
 | 
					                raise PermissionDenied
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # We check if the user can add the model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # While checking permissions, the object will be inserted in the DB, then removed.
 | 
				
			||||||
 | 
					        # We disable temporary the connectors
 | 
				
			||||||
 | 
					        pre_save.disconnect(pre_save_object)
 | 
				
			||||||
 | 
					        pre_delete.disconnect(pre_delete_object)
 | 
				
			||||||
 | 
					        # We disable also logs connectors
 | 
				
			||||||
 | 
					        pre_save.disconnect(logs_signals.pre_save_object)
 | 
				
			||||||
 | 
					        post_save.disconnect(logs_signals.save_object)
 | 
				
			||||||
 | 
					        post_delete.disconnect(logs_signals.delete_object)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # We check if the user has right to add the object
 | 
				
			||||||
 | 
					        has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Then we reconnect all
 | 
				
			||||||
 | 
					        pre_save.connect(pre_save_object)
 | 
				
			||||||
 | 
					        pre_delete.connect(pre_delete_object)
 | 
				
			||||||
 | 
					        pre_save.connect(logs_signals.pre_save_object)
 | 
				
			||||||
 | 
					        post_save.connect(logs_signals.save_object)
 | 
				
			||||||
 | 
					        post_delete.connect(logs_signals.delete_object)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not has_perm:
 | 
				
			||||||
 | 
					            raise PermissionDenied
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pre_delete_object(sender, instance, **kwargs):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Before a model get deleted, we check the permissions
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # noinspection PyProtectedMember
 | 
				
			||||||
 | 
					    if instance._meta.label_lower in EXCLUDED:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        # Action performed on shell is always granted
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model_name_full = instance._meta.label_lower.split(".")
 | 
				
			||||||
 | 
					    app_label = model_name_full[0]
 | 
				
			||||||
 | 
					    model_name = model_name_full[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # We check if the user has rights to delete the object
 | 
				
			||||||
 | 
					    if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance):
 | 
				
			||||||
 | 
					        raise PermissionDenied
 | 
				
			||||||
							
								
								
									
										0
									
								
								apps/permission/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								apps/permission/templatetags/perms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								apps/permission/templatetags/perms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					# 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.template.defaultfilters import stringfilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from note_kfet.middlewares import get_current_authenticated_user, get_current_session
 | 
				
			||||||
 | 
					from django import template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from permission.backends import PermissionBackend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stringfilter
 | 
				
			||||||
 | 
					def not_empty_model_list(model_name):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Return True if and only if the current user has right to see any object of the given model.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					    session = get_current_session()
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    elif user.is_superuser and session.get("permission_mask", 0) >= 42:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    if session.get("not_empty_model_list_" + model_name, None):
 | 
				
			||||||
 | 
					        return session.get("not_empty_model_list_" + model_name, None) == 1
 | 
				
			||||||
 | 
					    spl = model_name.split(".")
 | 
				
			||||||
 | 
					    ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
 | 
				
			||||||
 | 
					    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all()
 | 
				
			||||||
 | 
					    session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2
 | 
				
			||||||
 | 
					    return session.get("not_empty_model_list_" + model_name) == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@stringfilter
 | 
				
			||||||
 | 
					def not_empty_model_change_list(model_name):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Return True if and only if the current user has right to change any object of the given model.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    user = get_current_authenticated_user()
 | 
				
			||||||
 | 
					    session = get_current_session()
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    elif user.is_superuser and session.get("permission_mask", 0) >= 42:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    if session.get("not_empty_model_change_list_" + model_name, None):
 | 
				
			||||||
 | 
					        return session.get("not_empty_model_change_list_" + model_name, None) == 1
 | 
				
			||||||
 | 
					    spl = model_name.split(".")
 | 
				
			||||||
 | 
					    ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
 | 
				
			||||||
 | 
					    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
 | 
				
			||||||
 | 
					    session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2
 | 
				
			||||||
 | 
					    return session.get("not_empty_model_change_list_" + model_name) == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					register = template.Library()
 | 
				
			||||||
 | 
					register.filter('not_empty_model_list', not_empty_model_list)
 | 
				
			||||||
 | 
					register.filter('not_empty_model_change_list', not_empty_model_change_list)
 | 
				
			||||||
@@ -2,12 +2,17 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# 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 compilemessages
 | 
				
			||||||
python manage.py makemigrations
 | 
					python manage.py makemigrations
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Wait for database
 | 
					 | 
				
			||||||
sleep 5
 | 
					 | 
				
			||||||
python manage.py migrate
 | 
					python manage.py migrate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: use uwsgi in production
 | 
					 | 
				
			||||||
python manage.py runserver 0.0.0.0:8000
 | 
					python manage.py runserver 0.0.0.0:8000
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2020-03-07 18:01+0100\n"
 | 
					"POT-Creation-Date: 2020-03-16 11:53+0100\n"
 | 
				
			||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
				
			||||||
@@ -23,9 +23,10 @@ msgid "activity"
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/activity/models.py:19 apps/activity/models.py:44
 | 
					#: apps/activity/models.py:19 apps/activity/models.py:44
 | 
				
			||||||
#: apps/member/models.py:60 apps/member/models.py:111
 | 
					#: apps/member/models.py:61 apps/member/models.py:112
 | 
				
			||||||
#: apps/note/models/notes.py:187 apps/note/models/transactions.py:24
 | 
					#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
 | 
				
			||||||
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15
 | 
					#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
 | 
				
			||||||
 | 
					#: templates/member/profile_detail.html:15
 | 
				
			||||||
msgid "name"
 | 
					msgid "name"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,8 +50,8 @@ msgstr ""
 | 
				
			|||||||
msgid "description"
 | 
					msgid "description"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/activity/models.py:54 apps/note/models/notes.py:163
 | 
					#: apps/activity/models.py:54 apps/note/models/notes.py:164
 | 
				
			||||||
#: apps/note/models/transactions.py:62
 | 
					#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
 | 
				
			||||||
msgid "type"
 | 
					msgid "type"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -86,11 +87,11 @@ msgstr ""
 | 
				
			|||||||
msgid "API"
 | 
					msgid "API"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/apps.py:10
 | 
					#: apps/logs/apps.py:11
 | 
				
			||||||
msgid "Logs"
 | 
					msgid "Logs"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:21 apps/note/models/notes.py:116
 | 
					#: apps/logs/models.py:21 apps/note/models/notes.py:117
 | 
				
			||||||
msgid "user"
 | 
					msgid "user"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -114,15 +115,27 @@ msgstr ""
 | 
				
			|||||||
msgid "new data"
 | 
					msgid "new data"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:59
 | 
					#: apps/logs/models.py:60
 | 
				
			||||||
 | 
					msgid "create"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/logs/models.py:61
 | 
				
			||||||
 | 
					msgid "edit"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/logs/models.py:62
 | 
				
			||||||
 | 
					msgid "delete"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/logs/models.py:65
 | 
				
			||||||
msgid "action"
 | 
					msgid "action"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:67
 | 
					#: apps/logs/models.py:73
 | 
				
			||||||
msgid "timestamp"
 | 
					msgid "timestamp"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:71
 | 
					#: apps/logs/models.py:77
 | 
				
			||||||
msgid "Logs cannot be destroyed."
 | 
					msgid "Logs cannot be destroyed."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,73 +167,73 @@ msgstr ""
 | 
				
			|||||||
msgid "user profile"
 | 
					msgid "user profile"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:65
 | 
					#: apps/member/models.py:66
 | 
				
			||||||
msgid "email"
 | 
					msgid "email"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:70
 | 
					#: apps/member/models.py:71
 | 
				
			||||||
msgid "membership fee"
 | 
					msgid "membership fee"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:74
 | 
					#: apps/member/models.py:75
 | 
				
			||||||
msgid "membership duration"
 | 
					msgid "membership duration"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:75
 | 
					#: apps/member/models.py:76
 | 
				
			||||||
msgid "The longest time a membership can last (NULL = infinite)."
 | 
					msgid "The longest time a membership can last (NULL = infinite)."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:80
 | 
					#: apps/member/models.py:81
 | 
				
			||||||
msgid "membership start"
 | 
					msgid "membership start"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:81
 | 
					#: apps/member/models.py:82
 | 
				
			||||||
msgid "How long after January 1st the members can renew their membership."
 | 
					msgid "How long after January 1st the members can renew their membership."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:86
 | 
					#: apps/member/models.py:87
 | 
				
			||||||
msgid "membership end"
 | 
					msgid "membership end"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:87
 | 
					#: apps/member/models.py:88
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"How long the membership can last after January 1st of the next year after "
 | 
					"How long the membership can last after January 1st of the next year after "
 | 
				
			||||||
"members can renew their membership."
 | 
					"members can renew their membership."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:93 apps/note/models/notes.py:138
 | 
					#: apps/member/models.py:94 apps/note/models/notes.py:139
 | 
				
			||||||
msgid "club"
 | 
					msgid "club"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:94
 | 
					#: apps/member/models.py:95
 | 
				
			||||||
msgid "clubs"
 | 
					msgid "clubs"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:117
 | 
					#: apps/member/models.py:118
 | 
				
			||||||
msgid "role"
 | 
					msgid "role"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:118
 | 
					#: apps/member/models.py:119
 | 
				
			||||||
msgid "roles"
 | 
					msgid "roles"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:142
 | 
					#: apps/member/models.py:143
 | 
				
			||||||
msgid "membership starts on"
 | 
					msgid "membership starts on"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:145
 | 
					#: apps/member/models.py:146
 | 
				
			||||||
msgid "membership ends on"
 | 
					msgid "membership ends on"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:149
 | 
					#: apps/member/models.py:150
 | 
				
			||||||
msgid "fee"
 | 
					msgid "fee"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:153
 | 
					#: apps/member/models.py:154
 | 
				
			||||||
msgid "membership"
 | 
					msgid "membership"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:154
 | 
					#: apps/member/models.py:155
 | 
				
			||||||
msgid "memberships"
 | 
					msgid "memberships"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -237,140 +250,136 @@ msgstr ""
 | 
				
			|||||||
msgid "Account #%(id)s: %(username)s"
 | 
					msgid "Account #%(id)s: %(username)s"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/views.py:200
 | 
					#: apps/member/views.py:202
 | 
				
			||||||
msgid "Alias successfully deleted"
 | 
					msgid "Alias successfully deleted"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
 | 
					#: apps/note/admin.py:120 apps/note/models/transactions.py:94
 | 
				
			||||||
msgid "source"
 | 
					msgid "source"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/admin.py:128 apps/note/admin.py:156
 | 
					#: 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"
 | 
					msgid "destination"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/apps.py:14 apps/note/models/notes.py:57
 | 
					#: apps/note/apps.py:14 apps/note/models/notes.py:58
 | 
				
			||||||
msgid "note"
 | 
					msgid "note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:26
 | 
					#: apps/note/forms.py:20
 | 
				
			||||||
msgid "New Alias"
 | 
					msgid "New Alias"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:31
 | 
					#: apps/note/forms.py:25
 | 
				
			||||||
msgid "select an image"
 | 
					msgid "select an image"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:32
 | 
					#: apps/note/forms.py:26
 | 
				
			||||||
msgid "Maximal size: 2MB"
 | 
					msgid "Maximal size: 2MB"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:77
 | 
					#: apps/note/models/notes.py:27
 | 
				
			||||||
msgid "Source and destination must be different."
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: apps/note/models/notes.py:26
 | 
					 | 
				
			||||||
msgid "account balance"
 | 
					msgid "account balance"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:27
 | 
					#: apps/note/models/notes.py:28
 | 
				
			||||||
msgid "in centimes, money credited for this instance"
 | 
					msgid "in centimes, money credited for this instance"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:31
 | 
					#: apps/note/models/notes.py:32
 | 
				
			||||||
msgid "last negative date"
 | 
					msgid "last negative date"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:32
 | 
					#: apps/note/models/notes.py:33
 | 
				
			||||||
msgid "last time the balance was negative"
 | 
					msgid "last time the balance was negative"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:37
 | 
					#: apps/note/models/notes.py:38
 | 
				
			||||||
msgid "active"
 | 
					msgid "active"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:40
 | 
					#: apps/note/models/notes.py:41
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"Designates whether this note should be treated as active. Unselect this "
 | 
					"Designates whether this note should be treated as active. Unselect this "
 | 
				
			||||||
"instead of deleting notes."
 | 
					"instead of deleting notes."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:44
 | 
					#: apps/note/models/notes.py:45
 | 
				
			||||||
msgid "display image"
 | 
					msgid "display image"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:52 apps/note/models/transactions.py:102
 | 
					#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
 | 
				
			||||||
msgid "created at"
 | 
					msgid "created at"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:58
 | 
					#: apps/note/models/notes.py:59
 | 
				
			||||||
msgid "notes"
 | 
					msgid "notes"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:66
 | 
					#: apps/note/models/notes.py:67
 | 
				
			||||||
msgid "Note"
 | 
					msgid "Note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:76 apps/note/models/notes.py:100
 | 
					#: apps/note/models/notes.py:77 apps/note/models/notes.py:101
 | 
				
			||||||
msgid "This alias is already taken."
 | 
					msgid "This alias is already taken."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:120
 | 
					#: apps/note/models/notes.py:121
 | 
				
			||||||
msgid "one's note"
 | 
					msgid "one's note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:121
 | 
					#: apps/note/models/notes.py:122
 | 
				
			||||||
msgid "users note"
 | 
					msgid "users note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:127
 | 
					#: apps/note/models/notes.py:128
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "%(user)s's note"
 | 
					msgid "%(user)s's note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:142
 | 
					#: apps/note/models/notes.py:143
 | 
				
			||||||
msgid "club note"
 | 
					msgid "club note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:143
 | 
					#: apps/note/models/notes.py:144
 | 
				
			||||||
msgid "clubs notes"
 | 
					msgid "clubs notes"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:149
 | 
					#: apps/note/models/notes.py:150
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "Note of %(club)s club"
 | 
					msgid "Note of %(club)s club"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:169
 | 
					#: apps/note/models/notes.py:170
 | 
				
			||||||
msgid "special note"
 | 
					msgid "special note"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:170
 | 
					#: apps/note/models/notes.py:171
 | 
				
			||||||
msgid "special notes"
 | 
					msgid "special notes"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:193
 | 
					#: apps/note/models/notes.py:194
 | 
				
			||||||
msgid "Invalid alias"
 | 
					msgid "Invalid alias"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:209
 | 
					#: apps/note/models/notes.py:210
 | 
				
			||||||
msgid "alias"
 | 
					msgid "alias"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:210 templates/member/profile_detail.html:37
 | 
					#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
 | 
				
			||||||
msgid "aliases"
 | 
					msgid "aliases"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:228
 | 
					#: apps/note/models/notes.py:233
 | 
				
			||||||
msgid "Alias is too long."
 | 
					msgid "Alias is too long."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:233
 | 
					#: 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 ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:242
 | 
					#: apps/note/models/notes.py:247
 | 
				
			||||||
msgid "You can't delete your main alias."
 | 
					msgid "You can't delete your main alias."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -386,7 +395,7 @@ msgstr ""
 | 
				
			|||||||
msgid "A template with this name already exist"
 | 
					msgid "A template with this name already exist"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "amount"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -394,74 +403,116 @@ msgstr ""
 | 
				
			|||||||
msgid "in centimes"
 | 
					msgid "in centimes"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:74
 | 
					#: apps/note/models/transactions.py:75
 | 
				
			||||||
msgid "transaction template"
 | 
					msgid "transaction template"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:75
 | 
					#: apps/note/models/transactions.py:76
 | 
				
			||||||
msgid "transaction templates"
 | 
					msgid "transaction templates"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:106
 | 
					#: apps/note/models/transactions.py:107
 | 
				
			||||||
msgid "quantity"
 | 
					msgid "quantity"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:111
 | 
					#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
 | 
				
			||||||
msgid "reason"
 | 
					msgid "Gift"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:115
 | 
					#: apps/note/models/transactions.py:118 templates/base.html:90
 | 
				
			||||||
msgid "valid"
 | 
					#: templates/note/transaction_form.html:19
 | 
				
			||||||
 | 
					#: templates/note/transaction_form.html:126
 | 
				
			||||||
 | 
					msgid "Transfer"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:120
 | 
					#: apps/note/models/transactions.py:119
 | 
				
			||||||
msgid "transaction"
 | 
					msgid "Template"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:121
 | 
					#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
 | 
				
			||||||
msgid "transactions"
 | 
					msgid "Credit"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:184
 | 
					#: 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"
 | 
					msgid "membership transaction"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:185
 | 
					#: apps/note/models/transactions.py:129
 | 
				
			||||||
 | 
					msgid "reason"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:133
 | 
				
			||||||
 | 
					msgid "valid"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:138
 | 
				
			||||||
 | 
					msgid "transaction"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:139
 | 
				
			||||||
 | 
					msgid "transactions"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:207
 | 
				
			||||||
 | 
					msgid "first_name"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:212
 | 
				
			||||||
 | 
					msgid "bank"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:231
 | 
				
			||||||
msgid "membership transactions"
 | 
					msgid "membership transactions"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/views.py:29
 | 
					#: apps/note/views.py:31
 | 
				
			||||||
msgid "Transfer money from your account to one or others"
 | 
					msgid "Transfer money"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/views.py:138
 | 
					#: apps/note/views.py:132 templates/base.html:78
 | 
				
			||||||
msgid "Consommations"
 | 
					msgid "Consumptions"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: note_kfet/settings/base.py:162
 | 
					#: note_kfet/settings/__init__.py:61
 | 
				
			||||||
msgid "German"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:163
 | 
					 | 
				
			||||||
msgid "English"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:164
 | 
					 | 
				
			||||||
msgid "French"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:215
 | 
					 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"The Central Authentication Service grants you access to most of our websites "
 | 
					"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 "
 | 
					"by authenticating only once, so you don't need to type your credentials "
 | 
				
			||||||
"again unless your session expires or you logout."
 | 
					"again unless your session expires or you logout."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:156
 | 
				
			||||||
 | 
					msgid "German"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:157
 | 
				
			||||||
 | 
					msgid "English"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:158
 | 
				
			||||||
 | 
					msgid "French"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/base.html:13
 | 
					#: templates/base.html:13
 | 
				
			||||||
msgid "The ENS Paris-Saclay BDE note."
 | 
					msgid "The ENS Paris-Saclay BDE note."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/cas_server/base.html:7 templates/cas_server/base.html:26
 | 
					#: 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"
 | 
					msgid "Central Authentication Service"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -511,6 +562,16 @@ msgstr ""
 | 
				
			|||||||
msgid "Connect to the service"
 | 
					msgid "Connect to the service"
 | 
				
			||||||
msgstr ""
 | 
					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
 | 
					#: templates/member/club_detail.html:10
 | 
				
			||||||
msgid "Membership starts on"
 | 
					msgid "Membership starts on"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
@@ -531,6 +592,14 @@ msgstr ""
 | 
				
			|||||||
msgid "Transaction history"
 | 
					msgid "Transaction history"
 | 
				
			||||||
msgstr ""
 | 
					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
 | 
					#: templates/member/manage_auth_tokens.html:16
 | 
				
			||||||
msgid "Token"
 | 
					msgid "Token"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
@@ -579,12 +648,87 @@ msgstr ""
 | 
				
			|||||||
msgid "Save Changes"
 | 
					msgid "Save Changes"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: templates/member/signup.html:5 templates/member/signup.html:8
 | 
				
			||||||
#: templates/member/signup.html:14
 | 
					#: templates/member/signup.html:14
 | 
				
			||||||
msgid "Sign Up"
 | 
					msgid "Sign up"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/note/transaction_form.html:35
 | 
					#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
 | 
				
			||||||
msgid "Transfer"
 | 
					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 ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/registration/logged_out.html:8
 | 
					#: templates/registration/logged_out.html:8
 | 
				
			||||||
@@ -596,7 +740,7 @@ msgid "Log in again"
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/registration/login.html:7 templates/registration/login.html:8
 | 
					#: 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
 | 
					#: templates/registration/password_reset_complete.html:10
 | 
				
			||||||
msgid "Log in"
 | 
					msgid "Log in"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
@@ -608,7 +752,7 @@ msgid ""
 | 
				
			|||||||
"page. Would you like to login to a different account?"
 | 
					"page. Would you like to login to a different account?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/registration/login.html:23
 | 
					#: templates/registration/login.html:27
 | 
				
			||||||
msgid "Forgotten your password or username?"
 | 
					msgid "Forgotten your password or username?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2020-03-07 18:01+0100\n"
 | 
					"POT-Creation-Date: 2020-03-16 11:53+0100\n"
 | 
				
			||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
				
			||||||
@@ -18,9 +18,10 @@ msgid "activity"
 | 
				
			|||||||
msgstr "activité"
 | 
					msgstr "activité"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/activity/models.py:19 apps/activity/models.py:44
 | 
					#: apps/activity/models.py:19 apps/activity/models.py:44
 | 
				
			||||||
#: apps/member/models.py:60 apps/member/models.py:111
 | 
					#: apps/member/models.py:61 apps/member/models.py:112
 | 
				
			||||||
#: apps/note/models/notes.py:187 apps/note/models/transactions.py:24
 | 
					#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
 | 
				
			||||||
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15
 | 
					#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
 | 
				
			||||||
 | 
					#: templates/member/profile_detail.html:15
 | 
				
			||||||
msgid "name"
 | 
					msgid "name"
 | 
				
			||||||
msgstr "nom"
 | 
					msgstr "nom"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,8 +45,8 @@ msgstr "types d'activité"
 | 
				
			|||||||
msgid "description"
 | 
					msgid "description"
 | 
				
			||||||
msgstr "description"
 | 
					msgstr "description"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/activity/models.py:54 apps/note/models/notes.py:163
 | 
					#: apps/activity/models.py:54 apps/note/models/notes.py:164
 | 
				
			||||||
#: apps/note/models/transactions.py:62
 | 
					#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
 | 
				
			||||||
msgid "type"
 | 
					msgid "type"
 | 
				
			||||||
msgstr "type"
 | 
					msgstr "type"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,19 +82,17 @@ msgstr "invités"
 | 
				
			|||||||
msgid "API"
 | 
					msgid "API"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/apps.py:10
 | 
					#: apps/logs/apps.py:11
 | 
				
			||||||
msgid "Logs"
 | 
					msgid "Logs"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:21 apps/note/models/notes.py:116
 | 
					#: apps/logs/models.py:21 apps/note/models/notes.py:117
 | 
				
			||||||
msgid "user"
 | 
					msgid "user"
 | 
				
			||||||
msgstr "utilisateur"
 | 
					msgstr "utilisateur"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:27
 | 
					#: apps/logs/models.py:27
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "address"
 | 
					 | 
				
			||||||
msgid "IP Address"
 | 
					msgid "IP Address"
 | 
				
			||||||
msgstr "adresse"
 | 
					msgstr "Adresse IP"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:35
 | 
					#: apps/logs/models.py:35
 | 
				
			||||||
msgid "model"
 | 
					msgid "model"
 | 
				
			||||||
@@ -108,22 +107,30 @@ msgid "previous data"
 | 
				
			|||||||
msgstr "Données précédentes"
 | 
					msgstr "Données précédentes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:52
 | 
					#: apps/logs/models.py:52
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "end date"
 | 
					 | 
				
			||||||
msgid "new data"
 | 
					msgid "new data"
 | 
				
			||||||
msgstr "Nouvelles données"
 | 
					msgstr "Nouvelles données"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:59
 | 
					#: apps/logs/models.py:60
 | 
				
			||||||
#, fuzzy
 | 
					msgid "create"
 | 
				
			||||||
#| msgid "section"
 | 
					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"
 | 
					msgid "action"
 | 
				
			||||||
msgstr "Action"
 | 
					msgstr "Action"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:67
 | 
					#: apps/logs/models.py:73
 | 
				
			||||||
msgid "timestamp"
 | 
					msgid "timestamp"
 | 
				
			||||||
msgstr "Date"
 | 
					msgstr "Date"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/logs/models.py:71
 | 
					#: apps/logs/models.py:77
 | 
				
			||||||
msgid "Logs cannot be destroyed."
 | 
					msgid "Logs cannot be destroyed."
 | 
				
			||||||
msgstr "Les logs ne peuvent pas être détruits."
 | 
					msgstr "Les logs ne peuvent pas être détruits."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -155,37 +162,37 @@ msgstr "payé"
 | 
				
			|||||||
msgid "user profile"
 | 
					msgid "user profile"
 | 
				
			||||||
msgstr "profil utilisateur"
 | 
					msgstr "profil utilisateur"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:65
 | 
					#: apps/member/models.py:66
 | 
				
			||||||
msgid "email"
 | 
					msgid "email"
 | 
				
			||||||
msgstr "courriel"
 | 
					msgstr "courriel"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:70
 | 
					#: apps/member/models.py:71
 | 
				
			||||||
msgid "membership fee"
 | 
					msgid "membership fee"
 | 
				
			||||||
msgstr "cotisation pour adhérer"
 | 
					msgstr "cotisation pour adhérer"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:74
 | 
					#: apps/member/models.py:75
 | 
				
			||||||
msgid "membership duration"
 | 
					msgid "membership duration"
 | 
				
			||||||
msgstr "durée de l'adhésion"
 | 
					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)."
 | 
					msgid "The longest time a membership can last (NULL = infinite)."
 | 
				
			||||||
msgstr "La durée maximale d'une adhésion (NULL = infinie)."
 | 
					msgstr "La durée maximale d'une adhésion (NULL = infinie)."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:80
 | 
					#: apps/member/models.py:81
 | 
				
			||||||
msgid "membership start"
 | 
					msgid "membership start"
 | 
				
			||||||
msgstr "début de l'adhésion"
 | 
					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."
 | 
					msgid "How long after January 1st the members can renew their membership."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
 | 
					"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
 | 
				
			||||||
"adhésion."
 | 
					"adhésion."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:86
 | 
					#: apps/member/models.py:87
 | 
				
			||||||
msgid "membership end"
 | 
					msgid "membership end"
 | 
				
			||||||
msgstr "fin de l'adhésion"
 | 
					msgstr "fin de l'adhésion"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:87
 | 
					#: apps/member/models.py:88
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"How long the membership can last after January 1st of the next year after "
 | 
					"How long the membership can last after January 1st of the next year after "
 | 
				
			||||||
"members can renew their membership."
 | 
					"members can renew their membership."
 | 
				
			||||||
@@ -193,39 +200,39 @@ msgstr ""
 | 
				
			|||||||
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
 | 
					"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."
 | 
					"suivante avant que les adhérents peuvent renouveler leur adhésion."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:93 apps/note/models/notes.py:138
 | 
					#: apps/member/models.py:94 apps/note/models/notes.py:139
 | 
				
			||||||
msgid "club"
 | 
					msgid "club"
 | 
				
			||||||
msgstr "club"
 | 
					msgstr "club"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:94
 | 
					#: apps/member/models.py:95
 | 
				
			||||||
msgid "clubs"
 | 
					msgid "clubs"
 | 
				
			||||||
msgstr "clubs"
 | 
					msgstr "clubs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:117
 | 
					#: apps/member/models.py:118
 | 
				
			||||||
msgid "role"
 | 
					msgid "role"
 | 
				
			||||||
msgstr "rôle"
 | 
					msgstr "rôle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:118
 | 
					#: apps/member/models.py:119
 | 
				
			||||||
msgid "roles"
 | 
					msgid "roles"
 | 
				
			||||||
msgstr "rôles"
 | 
					msgstr "rôles"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:142
 | 
					#: apps/member/models.py:143
 | 
				
			||||||
msgid "membership starts on"
 | 
					msgid "membership starts on"
 | 
				
			||||||
msgstr "l'adhésion commence le"
 | 
					msgstr "l'adhésion commence le"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:145
 | 
					#: apps/member/models.py:146
 | 
				
			||||||
msgid "membership ends on"
 | 
					msgid "membership ends on"
 | 
				
			||||||
msgstr "l'adhésion finie le"
 | 
					msgstr "l'adhésion finie le"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:149
 | 
					#: apps/member/models.py:150
 | 
				
			||||||
msgid "fee"
 | 
					msgid "fee"
 | 
				
			||||||
msgstr "cotisation"
 | 
					msgstr "cotisation"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:153
 | 
					#: apps/member/models.py:154
 | 
				
			||||||
msgid "membership"
 | 
					msgid "membership"
 | 
				
			||||||
msgstr "adhésion"
 | 
					msgstr "adhésion"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/models.py:154
 | 
					#: apps/member/models.py:155
 | 
				
			||||||
msgid "memberships"
 | 
					msgid "memberships"
 | 
				
			||||||
msgstr "adhésions"
 | 
					msgstr "adhésions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -242,145 +249,137 @@ msgstr "Un alias avec un nom similaire existe déjà."
 | 
				
			|||||||
msgid "Account #%(id)s: %(username)s"
 | 
					msgid "Account #%(id)s: %(username)s"
 | 
				
			||||||
msgstr "Compte n°%(id)s : %(username)s"
 | 
					msgstr "Compte n°%(id)s : %(username)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/member/views.py:200
 | 
					#: apps/member/views.py:202
 | 
				
			||||||
msgid "Alias successfully deleted"
 | 
					msgid "Alias successfully deleted"
 | 
				
			||||||
msgstr ""
 | 
					msgstr "L'alias a bien été supprimé"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
 | 
					#: apps/note/admin.py:120 apps/note/models/transactions.py:94
 | 
				
			||||||
msgid "source"
 | 
					msgid "source"
 | 
				
			||||||
msgstr "source"
 | 
					msgstr "source"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/admin.py:128 apps/note/admin.py:156
 | 
					#: 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"
 | 
					msgid "destination"
 | 
				
			||||||
msgstr "destination"
 | 
					msgstr "destination"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/apps.py:14 apps/note/models/notes.py:57
 | 
					#: apps/note/apps.py:14 apps/note/models/notes.py:58
 | 
				
			||||||
msgid "note"
 | 
					msgid "note"
 | 
				
			||||||
msgstr "note"
 | 
					msgstr "note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:26
 | 
					#: apps/note/forms.py:20
 | 
				
			||||||
msgid "New Alias"
 | 
					msgid "New Alias"
 | 
				
			||||||
msgstr ""
 | 
					msgstr "Nouvel alias"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:31
 | 
					#: apps/note/forms.py:25
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "display image"
 | 
					 | 
				
			||||||
msgid "select an image"
 | 
					msgid "select an image"
 | 
				
			||||||
msgstr "image affichée"
 | 
					msgstr "Choisissez une image"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:32
 | 
					#: apps/note/forms.py:26
 | 
				
			||||||
msgid "Maximal size: 2MB"
 | 
					msgid "Maximal size: 2MB"
 | 
				
			||||||
msgstr ""
 | 
					msgstr "Taille maximale : 2 Mo"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/forms.py:77
 | 
					#: apps/note/models/notes.py:27
 | 
				
			||||||
msgid "Source and destination must be different."
 | 
					 | 
				
			||||||
msgstr "La source et la destination doivent être différentes."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: apps/note/models/notes.py:26
 | 
					 | 
				
			||||||
msgid "account balance"
 | 
					msgid "account balance"
 | 
				
			||||||
msgstr "solde du compte"
 | 
					msgstr "solde du compte"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:27
 | 
					#: apps/note/models/notes.py:28
 | 
				
			||||||
msgid "in centimes, money credited for this instance"
 | 
					msgid "in centimes, money credited for this instance"
 | 
				
			||||||
msgstr "en centimes, argent crédité pour cette 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"
 | 
					msgid "last negative date"
 | 
				
			||||||
msgstr "dernier date de négatif"
 | 
					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"
 | 
					msgid "last time the balance was negative"
 | 
				
			||||||
msgstr "dernier instant où la note était en négatif"
 | 
					msgstr "dernier instant où la note était en négatif"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:37
 | 
					#: apps/note/models/notes.py:38
 | 
				
			||||||
msgid "active"
 | 
					msgid "active"
 | 
				
			||||||
msgstr "actif"
 | 
					msgstr "actif"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:40
 | 
					#: apps/note/models/notes.py:41
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"Designates whether this note should be treated as active. Unselect this "
 | 
					"Designates whether this note should be treated as active. Unselect this "
 | 
				
			||||||
"instead of deleting notes."
 | 
					"instead of deleting notes."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Indique si la note est active. Désactiver cela plutôt que supprimer la note."
 | 
					"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"
 | 
					msgid "display image"
 | 
				
			||||||
msgstr "image affichée"
 | 
					msgstr "image affichée"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:52 apps/note/models/transactions.py:102
 | 
					#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
 | 
				
			||||||
msgid "created at"
 | 
					msgid "created at"
 | 
				
			||||||
msgstr "créée le"
 | 
					msgstr "créée le"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:58
 | 
					#: apps/note/models/notes.py:59
 | 
				
			||||||
msgid "notes"
 | 
					msgid "notes"
 | 
				
			||||||
msgstr "notes"
 | 
					msgstr "notes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:66
 | 
					#: apps/note/models/notes.py:67
 | 
				
			||||||
msgid "Note"
 | 
					msgid "Note"
 | 
				
			||||||
msgstr "Note"
 | 
					msgstr "Note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:76 apps/note/models/notes.py:100
 | 
					#: apps/note/models/notes.py:77 apps/note/models/notes.py:101
 | 
				
			||||||
msgid "This alias is already taken."
 | 
					msgid "This alias is already taken."
 | 
				
			||||||
msgstr "Cet alias est déjà pris."
 | 
					msgstr "Cet alias est déjà pris."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:120
 | 
					#: apps/note/models/notes.py:121
 | 
				
			||||||
msgid "one's note"
 | 
					msgid "one's note"
 | 
				
			||||||
msgstr "note d'un utilisateur"
 | 
					msgstr "note d'un utilisateur"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:121
 | 
					#: apps/note/models/notes.py:122
 | 
				
			||||||
msgid "users note"
 | 
					msgid "users note"
 | 
				
			||||||
msgstr "notes des utilisateurs"
 | 
					msgstr "notes des utilisateurs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:127
 | 
					#: apps/note/models/notes.py:128
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "%(user)s's note"
 | 
					msgid "%(user)s's note"
 | 
				
			||||||
msgstr "Note de %(user)s"
 | 
					msgstr "Note de %(user)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:142
 | 
					#: apps/note/models/notes.py:143
 | 
				
			||||||
msgid "club note"
 | 
					msgid "club note"
 | 
				
			||||||
msgstr "note d'un club"
 | 
					msgstr "note d'un club"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:143
 | 
					#: apps/note/models/notes.py:144
 | 
				
			||||||
msgid "clubs notes"
 | 
					msgid "clubs notes"
 | 
				
			||||||
msgstr "notes des clubs"
 | 
					msgstr "notes des clubs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:149
 | 
					#: apps/note/models/notes.py:150
 | 
				
			||||||
#, python-format
 | 
					#, python-format
 | 
				
			||||||
msgid "Note of %(club)s club"
 | 
					msgid "Note of %(club)s club"
 | 
				
			||||||
msgstr "Note du club %(club)s"
 | 
					msgstr "Note du club %(club)s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:169
 | 
					#: apps/note/models/notes.py:170
 | 
				
			||||||
msgid "special note"
 | 
					msgid "special note"
 | 
				
			||||||
msgstr "note spéciale"
 | 
					msgstr "note spéciale"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:170
 | 
					#: apps/note/models/notes.py:171
 | 
				
			||||||
msgid "special notes"
 | 
					msgid "special notes"
 | 
				
			||||||
msgstr "notes spéciales"
 | 
					msgstr "notes spéciales"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:193
 | 
					#: apps/note/models/notes.py:194
 | 
				
			||||||
msgid "Invalid alias"
 | 
					msgid "Invalid alias"
 | 
				
			||||||
msgstr "Alias invalide"
 | 
					msgstr "Alias invalide"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:209
 | 
					#: apps/note/models/notes.py:210
 | 
				
			||||||
msgid "alias"
 | 
					msgid "alias"
 | 
				
			||||||
msgstr "alias"
 | 
					msgstr "alias"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:210 templates/member/profile_detail.html:37
 | 
					#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
 | 
				
			||||||
msgid "aliases"
 | 
					msgid "aliases"
 | 
				
			||||||
msgstr "alias"
 | 
					msgstr "alias"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:228
 | 
					#: apps/note/models/notes.py:233
 | 
				
			||||||
msgid "Alias is too long."
 | 
					msgid "Alias is too long."
 | 
				
			||||||
msgstr "L'alias est trop long."
 | 
					msgstr "L'alias est trop long."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:233
 | 
					#: apps/note/models/notes.py:238
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "An alias with a similar name already exists:"
 | 
					 | 
				
			||||||
msgid "An alias with a similar name already exists: {} "
 | 
					msgid "An alias with a similar name already exists: {} "
 | 
				
			||||||
msgstr "Un alias avec un nom similaire existe déjà."
 | 
					msgstr "Un alias avec un nom similaire existe déjà : {}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/notes.py:242
 | 
					#: apps/note/models/notes.py:247
 | 
				
			||||||
msgid "You can't delete your main alias."
 | 
					msgid "You can't delete your main alias."
 | 
				
			||||||
msgstr "Vous ne pouvez pas supprimer votre alias principal."
 | 
					msgstr "Vous ne pouvez pas supprimer votre alias principal."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -393,11 +392,10 @@ msgid "transaction categories"
 | 
				
			|||||||
msgstr "catégories de transaction"
 | 
					msgstr "catégories de transaction"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:47
 | 
					#: apps/note/models/transactions.py:47
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
msgid "A template with this name already exist"
 | 
					msgid "A template with this name already exist"
 | 
				
			||||||
msgstr "Un modèle de transaction avec un nom similaire existe déjà."
 | 
					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"
 | 
					msgid "amount"
 | 
				
			||||||
msgstr "montant"
 | 
					msgstr "montant"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -405,74 +403,116 @@ msgstr "montant"
 | 
				
			|||||||
msgid "in centimes"
 | 
					msgid "in centimes"
 | 
				
			||||||
msgstr "en centimes"
 | 
					msgstr "en centimes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:74
 | 
					#: apps/note/models/transactions.py:75
 | 
				
			||||||
msgid "transaction template"
 | 
					msgid "transaction template"
 | 
				
			||||||
msgstr "modèle de transaction"
 | 
					msgstr "modèle de transaction"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:75
 | 
					#: apps/note/models/transactions.py:76
 | 
				
			||||||
msgid "transaction templates"
 | 
					msgid "transaction templates"
 | 
				
			||||||
msgstr "modèles de transaction"
 | 
					msgstr "modèles de transaction"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:106
 | 
					#: apps/note/models/transactions.py:107
 | 
				
			||||||
msgid "quantity"
 | 
					msgid "quantity"
 | 
				
			||||||
msgstr "quantité"
 | 
					msgstr "quantité"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:111
 | 
					#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
 | 
				
			||||||
msgid "reason"
 | 
					msgid "Gift"
 | 
				
			||||||
msgstr "raison"
 | 
					msgstr "Don"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:115
 | 
					#: apps/note/models/transactions.py:118 templates/base.html:90
 | 
				
			||||||
msgid "valid"
 | 
					#: templates/note/transaction_form.html:19
 | 
				
			||||||
msgstr "valide"
 | 
					#: templates/note/transaction_form.html:126
 | 
				
			||||||
 | 
					msgid "Transfer"
 | 
				
			||||||
 | 
					msgstr "Virement"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:120
 | 
					#: apps/note/models/transactions.py:119
 | 
				
			||||||
msgid "transaction"
 | 
					msgid "Template"
 | 
				
			||||||
msgstr "transaction"
 | 
					msgstr "Bouton"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:121
 | 
					#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
 | 
				
			||||||
msgid "transactions"
 | 
					msgid "Credit"
 | 
				
			||||||
msgstr "transactions"
 | 
					msgstr "Crédit"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:184
 | 
					#: 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"
 | 
					msgid "membership transaction"
 | 
				
			||||||
msgstr "transaction d'adhésion"
 | 
					msgstr "transaction d'adhésion"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/models/transactions.py:185
 | 
					#: apps/note/models/transactions.py:129
 | 
				
			||||||
 | 
					msgid "reason"
 | 
				
			||||||
 | 
					msgstr "raison"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:133
 | 
				
			||||||
 | 
					msgid "valid"
 | 
				
			||||||
 | 
					msgstr "valide"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:138
 | 
				
			||||||
 | 
					msgid "transaction"
 | 
				
			||||||
 | 
					msgstr "transaction"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:139
 | 
				
			||||||
 | 
					msgid "transactions"
 | 
				
			||||||
 | 
					msgstr "transactions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:207
 | 
				
			||||||
 | 
					msgid "first_name"
 | 
				
			||||||
 | 
					msgstr "Prénom"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:212
 | 
				
			||||||
 | 
					msgid "bank"
 | 
				
			||||||
 | 
					msgstr "Banque"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: apps/note/models/transactions.py:231
 | 
				
			||||||
msgid "membership transactions"
 | 
					msgid "membership transactions"
 | 
				
			||||||
msgstr "transactions d'adhésion"
 | 
					msgstr "transactions d'adhésion"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/views.py:29
 | 
					#: apps/note/views.py:31
 | 
				
			||||||
msgid "Transfer money from your account to one or others"
 | 
					msgid "Transfer money"
 | 
				
			||||||
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
 | 
					msgstr "Transferts d'argent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: apps/note/views.py:138
 | 
					#: apps/note/views.py:132 templates/base.html:78
 | 
				
			||||||
msgid "Consommations"
 | 
					msgid "Consumptions"
 | 
				
			||||||
msgstr "transactions"
 | 
					msgstr "Consommations"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: note_kfet/settings/base.py:162
 | 
					#: note_kfet/settings/__init__.py:61
 | 
				
			||||||
msgid "German"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:163
 | 
					 | 
				
			||||||
msgid "English"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:164
 | 
					 | 
				
			||||||
msgid "French"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: note_kfet/settings/base.py:215
 | 
					 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"The Central Authentication Service grants you access to most of our websites "
 | 
					"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 "
 | 
					"by authenticating only once, so you don't need to type your credentials "
 | 
				
			||||||
"again unless your session expires or you logout."
 | 
					"again unless your session expires or you logout."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:156
 | 
				
			||||||
 | 
					msgid "German"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:157
 | 
				
			||||||
 | 
					msgid "English"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: note_kfet/settings/base.py:158
 | 
				
			||||||
 | 
					msgid "French"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/base.html:13
 | 
					#: templates/base.html:13
 | 
				
			||||||
msgid "The ENS Paris-Saclay BDE note."
 | 
					msgid "The ENS Paris-Saclay BDE note."
 | 
				
			||||||
msgstr "La note du BDE de l'ENS Paris-Saclay."
 | 
					msgstr "La note du BDE de l'ENS Paris-Saclay."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/cas_server/base.html:7 templates/cas_server/base.html:26
 | 
					#: templates/base.html: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"
 | 
					msgid "Central Authentication Service"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -510,11 +550,11 @@ msgstr ""
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#: templates/cas_server/login.html:11
 | 
					#: templates/cas_server/login.html:11
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
"If you don't have any Note Kfet account, please follow <a href='/accounts"
 | 
					"If you don't have any Note Kfet account, please follow <a href='/accounts/"
 | 
				
			||||||
"/signup'>this link to sign up</a>."
 | 
					"signup'>this link to sign up</a>."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Si vous n'avez pas de compte Note Kfet, veuillez suivre <a href='/accounts"
 | 
					"Si vous n'avez pas de compte Note Kfet, veuillez suivre <a href='/accounts/"
 | 
				
			||||||
"/signup'>ce lien pour vous inscrire</a>."
 | 
					"signup'>ce lien pour vous inscrire</a>."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/cas_server/login.html:17
 | 
					#: templates/cas_server/login.html:17
 | 
				
			||||||
msgid "Login"
 | 
					msgid "Login"
 | 
				
			||||||
@@ -524,6 +564,16 @@ msgstr ""
 | 
				
			|||||||
msgid "Connect to the service"
 | 
					msgid "Connect to the service"
 | 
				
			||||||
msgstr ""
 | 
					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
 | 
					#: templates/member/club_detail.html:10
 | 
				
			||||||
msgid "Membership starts on"
 | 
					msgid "Membership starts on"
 | 
				
			||||||
msgstr "L'adhésion commence le"
 | 
					msgstr "L'adhésion commence le"
 | 
				
			||||||
@@ -544,6 +594,14 @@ msgstr "solde du compte"
 | 
				
			|||||||
msgid "Transaction history"
 | 
					msgid "Transaction history"
 | 
				
			||||||
msgstr "Historique des transactions"
 | 
					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
 | 
					#: templates/member/manage_auth_tokens.html:16
 | 
				
			||||||
msgid "Token"
 | 
					msgid "Token"
 | 
				
			||||||
msgstr "Jeton"
 | 
					msgstr "Jeton"
 | 
				
			||||||
@@ -557,10 +615,8 @@ msgid "Regenerate token"
 | 
				
			|||||||
msgstr "Regénérer le jeton"
 | 
					msgstr "Regénérer le jeton"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/member/profile_alias.html:10
 | 
					#: templates/member/profile_alias.html:10
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "alias"
 | 
					 | 
				
			||||||
msgid "Add alias"
 | 
					msgid "Add alias"
 | 
				
			||||||
msgstr "alias"
 | 
					msgstr "Ajouter un alias"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/member/profile_detail.html:15
 | 
					#: templates/member/profile_detail.html:15
 | 
				
			||||||
msgid "first name"
 | 
					msgid "first name"
 | 
				
			||||||
@@ -583,10 +639,8 @@ msgid "Manage auth token"
 | 
				
			|||||||
msgstr "Gérer les jetons d'authentification"
 | 
					msgstr "Gérer les jetons d'authentification"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/member/profile_detail.html:49
 | 
					#: templates/member/profile_detail.html:49
 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
#| msgid "Update Profile"
 | 
					 | 
				
			||||||
msgid "View Profile"
 | 
					msgid "View Profile"
 | 
				
			||||||
msgstr "Modifier le profil"
 | 
					msgstr "Voir le profil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/member/profile_detail.html:62
 | 
					#: templates/member/profile_detail.html:62
 | 
				
			||||||
msgid "View my memberships"
 | 
					msgid "View my memberships"
 | 
				
			||||||
@@ -596,13 +650,88 @@ msgstr "Voir mes adhésions"
 | 
				
			|||||||
msgid "Save Changes"
 | 
					msgid "Save Changes"
 | 
				
			||||||
msgstr "Sauvegarder les changements"
 | 
					msgstr "Sauvegarder les changements"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: templates/member/signup.html:5 templates/member/signup.html:8
 | 
				
			||||||
#: templates/member/signup.html:14
 | 
					#: templates/member/signup.html:14
 | 
				
			||||||
msgid "Sign Up"
 | 
					msgid "Sign up"
 | 
				
			||||||
msgstr ""
 | 
					msgstr "Inscription"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/note/transaction_form.html:35
 | 
					#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
 | 
				
			||||||
msgid "Transfer"
 | 
					msgid "Select emitters"
 | 
				
			||||||
msgstr "Virement"
 | 
					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
 | 
					#: templates/registration/logged_out.html:8
 | 
				
			||||||
msgid "Thanks for spending some quality time with the Web site today."
 | 
					msgid "Thanks for spending some quality time with the Web site today."
 | 
				
			||||||
@@ -613,7 +742,7 @@ msgid "Log in again"
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/registration/login.html:7 templates/registration/login.html:8
 | 
					#: 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
 | 
					#: templates/registration/password_reset_complete.html:10
 | 
				
			||||||
msgid "Log in"
 | 
					msgid "Log in"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
@@ -625,7 +754,7 @@ msgid ""
 | 
				
			|||||||
"page. Would you like to login to a different account?"
 | 
					"page. Would you like to login to a different account?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: templates/registration/login.html:23
 | 
					#: templates/registration/login.html:27
 | 
				
			||||||
msgid "Forgotten your password or username?"
 | 
					msgid "Forgotten your password or username?"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,66 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.contrib.auth.models import AnonymousUser, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from threading import local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.contrib.sessions.backends.db import SessionStore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
 | 
				
			||||||
 | 
					SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
 | 
				
			||||||
 | 
					IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_thread_locals = local()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _set_current_user_and_ip(user=None, session=None, ip=None):
 | 
				
			||||||
 | 
					    setattr(_thread_locals, USER_ATTR_NAME, user)
 | 
				
			||||||
 | 
					    setattr(_thread_locals, SESSION_ATTR_NAME, session)
 | 
				
			||||||
 | 
					    setattr(_thread_locals, IP_ATTR_NAME, ip)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_current_user() -> User:
 | 
				
			||||||
 | 
					    return getattr(_thread_locals, USER_ATTR_NAME, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_current_session() -> SessionStore:
 | 
				
			||||||
 | 
					    return getattr(_thread_locals, SESSION_ATTR_NAME, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_current_ip() -> str:
 | 
				
			||||||
 | 
					    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 SessionMiddleware(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, request.session, ip)
 | 
				
			||||||
 | 
					        response = self.get_response(request)
 | 
				
			||||||
 | 
					        _set_current_user_and_ip(None, None, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TurbolinksMiddleware(object):
 | 
					class TurbolinksMiddleware(object):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,9 @@
 | 
				
			|||||||
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .base import *
 | 
					from .base import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,28 +35,28 @@ read_env()
 | 
				
			|||||||
app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev')
 | 
					app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev')
 | 
				
			||||||
if app_stage == 'prod':
 | 
					if app_stage == 'prod':
 | 
				
			||||||
    from .production import *
 | 
					    from .production import *
 | 
				
			||||||
 | 
					 | 
				
			||||||
    DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS')
 | 
					 | 
				
			||||||
    SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
 | 
					 | 
				
			||||||
    ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', 'localhost')]
 | 
					 | 
				
			||||||
else:
 | 
					else:
 | 
				
			||||||
    from .development import *
 | 
					    from .development import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
    #in secrets.py defines everything you want
 | 
					    #in secrets.py defines everything you want
 | 
				
			||||||
    from .secrets import *
 | 
					    from .secrets import *
 | 
				
			||||||
 | 
					    INSTALLED_APPS += OPTIONAL_APPS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
except ImportError:
 | 
					except ImportError:
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if "cas" in INSTALLED_APPS:
 | 
					if "cas" in INSTALLED_APPS:
 | 
				
			||||||
    MIDDLEWARE += ['cas.middleware.CASMiddleware']
 | 
					    MIDDLEWARE += ['cas.middleware.CASMiddleware']
 | 
				
			||||||
    # CAS Settings
 | 
					    # CAS Settings
 | 
				
			||||||
 | 
					    CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
 | 
				
			||||||
    CAS_AUTO_CREATE_USER = False
 | 
					    CAS_AUTO_CREATE_USER = False
 | 
				
			||||||
    CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
 | 
					    CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
 | 
				
			||||||
    CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
 | 
					    CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
 | 
				
			||||||
    CAS_SHOW_SERVICE_MESSAGES = True
 | 
					    CAS_SHOW_SERVICE_MESSAGES = True
 | 
				
			||||||
    CAS_SHOW_POWERED = False
 | 
					    CAS_SHOW_POWERED = False
 | 
				
			||||||
    CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
 | 
					    CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
 | 
				
			||||||
 | 
					    CAS_PROVIDE_URL_TO_LOGOUT = True
 | 
				
			||||||
    CAS_INFO_MESSAGES = {
 | 
					    CAS_INFO_MESSAGES = {
 | 
				
			||||||
        "cas_explained": {
 | 
					        "cas_explained": {
 | 
				
			||||||
            "message": _(
 | 
					            "message": _(
 | 
				
			||||||
@@ -68,7 +73,11 @@ if "cas" in INSTALLED_APPS:
 | 
				
			|||||||
        'cas_explained',
 | 
					        'cas_explained',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',)
 | 
					    AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',)
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if "logs" in INSTALLED_APPS:
 | 
				
			||||||
 | 
					    MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if "debug_toolbar" in INSTALLED_APPS:
 | 
					if "debug_toolbar" in INSTALLED_APPS:
 | 
				
			||||||
    MIDDLEWARE.insert(1,"debug_toolbar.middleware.DebugToolbarMiddleware")
 | 
					    MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
 | 
				
			||||||
    INTERNAL_IPS = [ '127.0.0.1']
 | 
					    INTERNAL_IPS = ['127.0.0.1']
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,6 +59,7 @@ INSTALLED_APPS = [
 | 
				
			|||||||
    'activity',
 | 
					    'activity',
 | 
				
			||||||
    'member',
 | 
					    'member',
 | 
				
			||||||
    'note',
 | 
					    'note',
 | 
				
			||||||
 | 
					    'permission',
 | 
				
			||||||
    'api',
 | 
					    'api',
 | 
				
			||||||
    'logs',
 | 
					    'logs',
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@@ -124,22 +125,21 @@ PASSWORD_HASHERS = [
 | 
				
			|||||||
    'member.hashers.CustomNK15Hasher',
 | 
					    'member.hashers.CustomNK15Hasher',
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Django Guardian object permissions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
AUTHENTICATION_BACKENDS = (
 | 
					AUTHENTICATION_BACKENDS = (
 | 
				
			||||||
    'django.contrib.auth.backends.ModelBackend',  # this is default
 | 
					    'permission.backends.PermissionBackend',  # Custom role-based permission system
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
REST_FRAMEWORK = {
 | 
					REST_FRAMEWORK = {
 | 
				
			||||||
    # Use Django's standard `django.contrib.auth` permissions,
 | 
					 | 
				
			||||||
    # or allow read-only access for unauthenticated users.
 | 
					 | 
				
			||||||
    'DEFAULT_PERMISSION_CLASSES': [
 | 
					    'DEFAULT_PERMISSION_CLASSES': [
 | 
				
			||||||
        # TODO Maybe replace it with our custom permissions system
 | 
					        # Control API access with our role-based permission system
 | 
				
			||||||
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
 | 
					        'permission.permissions.StrongDjangoObjectPermissions',
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    'DEFAULT_AUTHENTICATION_CLASSES': [
 | 
					    'DEFAULT_AUTHENTICATION_CLASSES': [
 | 
				
			||||||
 | 
					        'rest_framework.authentication.SessionAuthentication',
 | 
				
			||||||
        'rest_framework.authentication.TokenAuthentication',
 | 
					        'rest_framework.authentication.TokenAuthentication',
 | 
				
			||||||
    ]
 | 
					    ],
 | 
				
			||||||
 | 
					    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
 | 
				
			||||||
 | 
					    'PAGE_SIZE': 20,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Internationalization
 | 
					# Internationalization
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,12 +17,24 @@ import os
 | 
				
			|||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
 | 
					# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
 | 
				
			||||||
from . import *
 | 
					from . import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DATABASES = {
 | 
					if os.getenv("DJANGO_DEV_STORE_METHOD", "sqllite") == "postgresql":
 | 
				
			||||||
    'default': {
 | 
					    DATABASES = {
 | 
				
			||||||
        'ENGINE': 'django.db.backends.sqlite3',
 | 
					        'default': {
 | 
				
			||||||
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
 | 
					            '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!
 | 
					# Break it, fix it!
 | 
				
			||||||
DEBUG = True
 | 
					DEBUG = True
 | 
				
			||||||
@@ -39,7 +51,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 | 
				
			|||||||
# EMAIL_HOST_USER = 'change_me'
 | 
					# EMAIL_HOST_USER = 'change_me'
 | 
				
			||||||
# EMAIL_HOST_PASSWORD = 'change_me'
 | 
					# EMAIL_HOST_PASSWORD = 'change_me'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SERVER_EMAIL = 'no-reply@example.org'
 | 
					SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Security settings
 | 
					# Security settings
 | 
				
			||||||
SECURE_CONTENT_TYPE_NOSNIFF = False
 | 
					SECURE_CONTENT_TYPE_NOSNIFF = False
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
					# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
				
			||||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
					# SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
########################
 | 
					########################
 | 
				
			||||||
# Production  Settings #
 | 
					# Production  Settings #
 | 
				
			||||||
########################
 | 
					########################
 | 
				
			||||||
@@ -14,11 +16,11 @@
 | 
				
			|||||||
DATABASES = {
 | 
					DATABASES = {
 | 
				
			||||||
    'default': {
 | 
					    'default': {
 | 
				
			||||||
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
 | 
					        'ENGINE': 'django.db.backends.postgresql_psycopg2',
 | 
				
			||||||
        'NAME': 'note_db',
 | 
					        'NAME': os.environ.get('DJANGO_DB_NAME', 'note_db'),
 | 
				
			||||||
        'USER': 'note',
 | 
					        'USER': os.environ.get('DJANGO_DB_USER', 'note'),
 | 
				
			||||||
        'PASSWORD': 'update_in_env_variable',
 | 
					        'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'),
 | 
				
			||||||
        'HOST': '127.0.0.1',
 | 
					        'HOST': os.environ.get('DJANGO_DB_HOST', 'localhost'),
 | 
				
			||||||
        'PORT': '',
 | 
					        'PORT': os.environ.get('DJANGO_DB_PORT', ''),  # Use default port
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,7 +28,9 @@ DATABASES = {
 | 
				
			|||||||
DEBUG = True
 | 
					DEBUG = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Mandatory !
 | 
					# Mandatory !
 | 
				
			||||||
ALLOWED_HOSTS = []
 | 
					ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Emails
 | 
					# Emails
 | 
				
			||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 | 
					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_USER = 'change_me'
 | 
				
			||||||
# EMAIL_HOST_PASSWORD = 'change_me'
 | 
					# EMAIL_HOST_PASSWORD = 'change_me'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SERVER_EMAIL = 'no-reply@example.org'
 | 
					SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Security settings
 | 
					# Security settings
 | 
				
			||||||
SECURE_CONTENT_TYPE_NOSNIFF = False
 | 
					SECURE_CONTENT_TYPE_NOSNIFF = False
 | 
				
			||||||
@@ -49,4 +53,4 @@ X_FRAME_OPTIONS = 'DENY'
 | 
				
			|||||||
SESSION_COOKIE_AGE = 60 * 60 * 3
 | 
					SESSION_COOKIE_AGE = 60 * 60 * 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# CAS Client settings
 | 
					# CAS Client settings
 | 
				
			||||||
CAS_SERVER_URL = "https://note.crans.org/cas/"
 | 
					CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ from django.contrib import admin
 | 
				
			|||||||
from django.urls import path, include
 | 
					from django.urls import path, include
 | 
				
			||||||
from django.views.generic import RedirectView
 | 
					from django.views.generic import RedirectView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from member.views import CustomLoginView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    # Dev so redirect to something random
 | 
					    # Dev so redirect to something random
 | 
				
			||||||
    path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
 | 
					    path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
 | 
				
			||||||
@@ -16,12 +18,12 @@ urlpatterns = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Include Django Contrib and Core routers
 | 
					    # Include Django Contrib and Core routers
 | 
				
			||||||
    path('i18n/', include('django.conf.urls.i18n')),
 | 
					    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/doc/', include('django.contrib.admindocs.urls')),
 | 
				
			||||||
    path('admin/', admin.site.urls),
 | 
					    path('admin/', admin.site.urls),
 | 
				
			||||||
    path('logs/', include('logs.urls')),
 | 
					    path('accounts/', include('member.urls')),
 | 
				
			||||||
    path('api/', include('api.urls')),  
 | 
					    path('accounts/login/', CustomLoginView.as_view()),
 | 
				
			||||||
 | 
					    path('accounts/', include('django.contrib.auth.urls')),
 | 
				
			||||||
 | 
					    path('api/', include('api.urls')),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 | 
					urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 | 
				
			||||||
@@ -37,8 +39,8 @@ if "cas" in settings.INSTALLED_APPS:
 | 
				
			|||||||
    from cas import views as cas_views
 | 
					    from cas import views as cas_views
 | 
				
			||||||
    urlpatterns += [
 | 
					    urlpatterns += [
 | 
				
			||||||
        # Include CAS Client routers
 | 
					        # Include CAS Client routers
 | 
				
			||||||
        path('accounts/login/', cas_views.login, name='login'),
 | 
					        path('accounts/login/cas/', cas_views.login, name='cas_login'),
 | 
				
			||||||
        path('accounts/logout/', cas_views.logout, name='logout'),
 | 
					        path('accounts/logout/cas/', cas_views.logout, name='cas_logout'),
 | 
				
			||||||
       
 | 
					       
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
if "debug_toolbar" in settings.INSTALLED_APPS:
 | 
					if "debug_toolbar" in settings.INSTALLED_APPS:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
djangorestframework==3.9.0
 | 
					 | 
				
			||||||
django-rest-polymorphic==0.1.8
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -19,4 +19,6 @@ requests==2.22.0
 | 
				
			|||||||
requests-oauthlib==1.2.0
 | 
					requests-oauthlib==1.2.0
 | 
				
			||||||
six==1.12.0
 | 
					six==1.12.0
 | 
				
			||||||
sqlparse==0.3.0
 | 
					sqlparse==0.3.0
 | 
				
			||||||
 | 
					djangorestframework==3.9.0
 | 
				
			||||||
 | 
					django-rest-polymorphic==0.1.8
 | 
				
			||||||
urllib3==1.25.3
 | 
					urllib3==1.25.3
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										297
									
								
								static/js/base.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								static/js/base.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
				
			|||||||
 | 
					// 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) {
 | 
				
			||||||
 | 
					    if (!note.display_image) {
 | 
				
			||||||
 | 
					        note.display_image = 'https://nk20.ynerant.fr/media/pic/default.png';
 | 
				
			||||||
 | 
					        $.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) {
 | 
				
			||||||
 | 
					            note.display_image = new_note.display_image.replace("http:", "https:");
 | 
				
			||||||
 | 
					            note.name = new_note.name;
 | 
				
			||||||
 | 
					            note.balance = new_note.balance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            displayNote(note, alias, user_note_field, profile_pic_field);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let img = note.display_image;
 | 
				
			||||||
 | 
					    if (alias !== note.name)
 | 
				
			||||||
 | 
					        alias += " (aka. " + note.name + ")";
 | 
				
			||||||
 | 
					    if (user_note_field !== null)
 | 
				
			||||||
 | 
					        $("#" + user_note_field).text(alias + (note.balance == null ? "" : (" : " + 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;
 | 
				
			||||||
 | 
					                note = {
 | 
				
			||||||
 | 
					                    id: note,
 | 
				
			||||||
 | 
					                    name: alias.name,
 | 
				
			||||||
 | 
					                    alias: alias,
 | 
				
			||||||
 | 
					                    balance: null
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name);
 | 
				
			||||||
 | 
					                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("");
 | 
				
			||||||
 | 
					                    old_pattern = "";
 | 
				
			||||||
 | 
					                    // 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": "RecurrentTransaction",
 | 
				
			||||||
 | 
					            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();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										206
									
								
								static/js/consos.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								static/js/consos.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,206 @@
 | 
				
			|||||||
 | 
					// 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 RecurrentTransaction)
 | 
				
			||||||
 | 
					 * @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("");
 | 
				
			||||||
 | 
					    $("#user_note").text("");
 | 
				
			||||||
 | 
					    $("#profile_pic").attr("src", "/media/pic/default.png");
 | 
				
			||||||
 | 
					    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 RecurrentTransaction)
 | 
				
			||||||
 | 
					 * @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": "RecurrentTransaction",
 | 
				
			||||||
 | 
					            "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");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										161
									
								
								static/js/transfer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								static/js/transfer.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					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("");
 | 
				
			||||||
 | 
					    $("#user_note").val("");
 | 
				
			||||||
 | 
					    $("#profile_pic").attr("src", "/media/pic/default.png");
 | 
				
			||||||
 | 
					    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() {
 | 
				
			||||||
 | 
					            if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) {
 | 
				
			||||||
 | 
					                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();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
{% load static i18n pretty_money static %}
 | 
					{% load static i18n pretty_money static getenv perms %}
 | 
				
			||||||
{% comment %}
 | 
					{% comment %}
 | 
				
			||||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
					SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
@@ -46,12 +46,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
            crossorigin="anonymous"></script>
 | 
					            crossorigin="anonymous"></script>
 | 
				
			||||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
 | 
					    <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
 | 
				
			||||||
            crossorigin="anonymous"></script>
 | 
					            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 #}
 | 
					    {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
 | 
				
			||||||
    {% if form.media %}
 | 
					    {% if form.media %}
 | 
				
			||||||
        {{ form.media }}
 | 
					        {{ form.media }}
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        .validate:hover {
 | 
				
			||||||
 | 
					            cursor: pointer;
 | 
				
			||||||
 | 
					            text-decoration: underline;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% block extracss %}{% endblock %}
 | 
					    {% block extracss %}{% endblock %}
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body class="d-flex w-100 h-100 flex-column">
 | 
					<body class="d-flex w-100 h-100 flex-column">
 | 
				
			||||||
@@ -66,27 +74,36 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
        <div class="collapse navbar-collapse" id="navbarNavDropdown">
 | 
					        <div class="collapse navbar-collapse" id="navbarNavDropdown">
 | 
				
			||||||
            <ul class="navbar-nav">
 | 
					            <ul class="navbar-nav">
 | 
				
			||||||
                <li class="nav-item active">
 | 
					                {% if "note.transactiontemplate"|not_empty_model_list %}
 | 
				
			||||||
                    <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
 | 
					                    <li class="nav-item active">
 | 
				
			||||||
                </li>
 | 
					                        <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
 | 
				
			||||||
                <li class="nav-item active">
 | 
					                    </li>
 | 
				
			||||||
                    <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
 | 
					                {% endif %}
 | 
				
			||||||
                </li>
 | 
					                {% if "member.club"|not_empty_model_list %}
 | 
				
			||||||
                <li class="nav-item active">
 | 
					                    <li class="nav-item active">
 | 
				
			||||||
                    <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
 | 
					                        <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
 | 
				
			||||||
                </li>
 | 
					                    </li>
 | 
				
			||||||
                <li class="nav-item active">
 | 
					                {% endif %}
 | 
				
			||||||
                    <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Button' %}</a>
 | 
					                {% if "activity.activity"|not_empty_model_list %}
 | 
				
			||||||
                </li>
 | 
					                    <li class="nav-item active">
 | 
				
			||||||
                <li class="nav-item active">
 | 
					                        <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
 | 
				
			||||||
                    <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
 | 
					                    </li>
 | 
				
			||||||
                </li>
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                {% if "note.transactiontemplate"|not_empty_model_change_list %}
 | 
				
			||||||
 | 
					                    <li class="nav-item active">
 | 
				
			||||||
 | 
					                        <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
 | 
				
			||||||
 | 
					                    </li>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                    <li class="nav-item active">
 | 
				
			||||||
 | 
					                        <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
 | 
				
			||||||
 | 
					                    </li>
 | 
				
			||||||
            </ul>
 | 
					            </ul>
 | 
				
			||||||
            <ul class="navbar-nav ml-auto">
 | 
					            <ul class="navbar-nav ml-auto">
 | 
				
			||||||
                {% if user.is_authenticated %}
 | 
					                {% if user.is_authenticated %}
 | 
				
			||||||
                    <li class="dropdown">
 | 
					                    <li class="dropdown">
 | 
				
			||||||
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
 | 
					                        <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>
 | 
					                        </a>
 | 
				
			||||||
                        <div class="dropdown-menu dropdown-menu-right"
 | 
					                        <div class="dropdown-menu dropdown-menu-right"
 | 
				
			||||||
                             aria-labelledby="navbarDropdownMenuLink">
 | 
					                             aria-labelledby="navbarDropdownMenuLink">
 | 
				
			||||||
@@ -115,6 +132,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    </nav>
 | 
					    </nav>
 | 
				
			||||||
    <div class="container-fluid my-3" style="max-width: 1600px;">
 | 
					    <div class="container-fluid my-3" style="max-width: 1600px;">
 | 
				
			||||||
        {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
 | 
					        {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
 | 
				
			||||||
 | 
					        <div id="messages"></div>
 | 
				
			||||||
        {% block content %}
 | 
					        {% block content %}
 | 
				
			||||||
            <p>Default content...</p>
 | 
					            <p>Default content...</p>
 | 
				
			||||||
        {% endblock content %}
 | 
					        {% endblock content %}
 | 
				
			||||||
@@ -128,7 +146,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
                      class="form-inline">
 | 
					                      class="form-inline">
 | 
				
			||||||
                    <span class="text-muted mr-1">
 | 
					                    <span class="text-muted mr-1">
 | 
				
			||||||
                        NoteKfet2020 —
 | 
					                        NoteKfet2020 —
 | 
				
			||||||
                        <a href="mailto:tresorie.bde@lists.crans.org"
 | 
					                        <a href="mailto:{{ "CONTACT_EMAIL" | getenv }}"
 | 
				
			||||||
                           class="text-muted">Nous contacter</a> —
 | 
					                           class="text-muted">Nous contacter</a> —
 | 
				
			||||||
                    </span>
 | 
					                    </span>
 | 
				
			||||||
                    {% csrf_token %}
 | 
					                    {% csrf_token %}
 | 
				
			||||||
@@ -158,6 +176,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</footer>
 | 
					</footer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    CSRF_TOKEN = "{{ csrf_token }}";
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block extrajavascript %}
 | 
					{% block extrajavascript %}
 | 
				
			||||||
{% endblock extrajavascript %}
 | 
					{% endblock extrajavascript %}
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								templates/django_filters/rest_framework/crispy_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								templates/django_filters/rest_framework/crispy_form.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{% load crispy_forms_tags %}
 | 
				
			||||||
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>{% trans "Field filters" %}</h2>
 | 
				
			||||||
 | 
					{% crispy filter.form %}
 | 
				
			||||||
							
								
								
									
										6
									
								
								templates/django_filters/rest_framework/form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								templates/django_filters/rest_framework/form.html
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
				
			||||||
							
								
								
									
										1
									
								
								templates/django_filters/widgets/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								templates/django_filters/widgets/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
 | 
				
			||||||
@@ -1,11 +1,12 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% load static %}
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load i18n %}
 | 
				
			||||||
{% load crispy_forms_tags %}
 | 
					{% load crispy_forms_tags %}
 | 
				
			||||||
{% block content %}
 | 
					{% 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">
 | 
					<form method="post">
 | 
				
			||||||
{% csrf_token %}
 | 
					{% csrf_token %}
 | 
				
			||||||
{{form|crispy}}
 | 
					{{form|crispy}}
 | 
				
			||||||
<button class="btn btn-primary" type="submit">Submit</button>
 | 
					<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% load render_table from django_tables2 %}
 | 
					{% load render_table from django_tables2 %}
 | 
				
			||||||
 | 
					{% load i18n %}
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% render_table  table %}
 | 
					{% 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 %}
 | 
					{% endblock %}
 | 
				
			||||||
{% block extrajavascript %}
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@
 | 
				
			|||||||
                    <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" >
 | 
					                    <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" >
 | 
				
			||||||
                </a>
 | 
					                </a>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="card-body">
 | 
					            <div class="card-body" id="profile_infos">
 | 
				
			||||||
                <dl class="row">
 | 
					                <dl class="row">
 | 
				
			||||||
                    <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
 | 
					                    <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
 | 
				
			||||||
                    <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
 | 
					                    <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
 | 
				
			||||||
@@ -76,7 +76,9 @@
 | 
				
			|||||||
                    </a>
 | 
					                    </a>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
 | 
					                <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>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -84,3 +86,12 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% 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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,16 +2,16 @@
 | 
				
			|||||||
{% extends 'base.html' %}
 | 
					{% extends 'base.html' %}
 | 
				
			||||||
{% load crispy_forms_tags %}
 | 
					{% load crispy_forms_tags %}
 | 
				
			||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
{% block title %}Sign Up{% endblock %}
 | 
					{% block title %}{% trans "Sign up" %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
  <h2>Sign up</h2>
 | 
					  <h2>{% trans "Sign up" %}</h2>
 | 
				
			||||||
  <form method="post">
 | 
					  <form method="post">
 | 
				
			||||||
      {% csrf_token %}
 | 
					      {% csrf_token %}
 | 
				
			||||||
      {{ form|crispy }}
 | 
					      {{ form|crispy }}
 | 
				
			||||||
      {{ profile_form|crispy }}
 | 
					      {{ profile_form|crispy }}
 | 
				
			||||||
      <button class="btn btn-success" type="submit">
 | 
					      <button class="btn btn-success" type="submit">
 | 
				
			||||||
          {% trans "Sign Up" %}
 | 
					          {% trans "Sign up" %}
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,97 +1,171 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% load i18n static pretty_money %}
 | 
					{% load i18n static pretty_money django_tables2 %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{# Remove page title #}
 | 
					{# Remove page title #}
 | 
				
			||||||
{% block contenttitle %}{% endblock %}
 | 
					{% block contenttitle %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
    {# Regroup buttons under categories #}
 | 
					    <div class="row mt-4">
 | 
				
			||||||
    {% regroup transaction_templates by category as categories %}
 | 
					        <div class="col-sm-5 col-md-4" id="infos_div">
 | 
				
			||||||
 | 
					            <div class="row">
 | 
				
			||||||
    <form method="post" onsubmit="window.onbeforeunload=null">
 | 
					                {# User details column #}
 | 
				
			||||||
        {% csrf_token %}
 | 
					                <div class="col-xl-5" id="note_infos_div">
 | 
				
			||||||
 | 
					                    <div class="card border-success shadow mb-4">
 | 
				
			||||||
        <div class="row">
 | 
					                        <img src="/media/pic/default.png"
 | 
				
			||||||
            <div class="col-sm-5 mb-4">
 | 
					                            id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
 | 
				
			||||||
                {% if form.non_field_errors %}
 | 
					                        <div class="card-body text-center">
 | 
				
			||||||
                    <p class="errornote">
 | 
					                            <span id="user_note"></span>
 | 
				
			||||||
                        {% 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>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                {% endfor %}
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="col-sm-7">
 | 
					                {# User selection column #}
 | 
				
			||||||
                <div class="card text-center shadow">
 | 
					                <div class="col-xl-7" id="user_select_div">
 | 
				
			||||||
                    {# Tabs for button categories #}
 | 
					                    <div class="card border-success shadow mb-4">
 | 
				
			||||||
                    <div class="card-header">
 | 
					                        <div class="card-header">
 | 
				
			||||||
                        <ul class="nav nav-tabs nav-fill card-header-tabs">
 | 
					                            <p class="card-text font-weight-bold">
 | 
				
			||||||
                            {% for category in categories %}
 | 
					                                {% trans "Select emitters" %}
 | 
				
			||||||
                                <li class="nav-item">
 | 
					                            </p>
 | 
				
			||||||
                                    <a class="nav-link" data-toggle="tab" href="#{{ category.grouper|slugify }}">
 | 
					                        </div>
 | 
				
			||||||
                                        {{ category.grouper }}
 | 
					                        <ul class="list-group list-group-flush" id="note_list">
 | 
				
			||||||
                                    </a>
 | 
					 | 
				
			||||||
                                </li>
 | 
					 | 
				
			||||||
                            {% endfor %}
 | 
					 | 
				
			||||||
                        </ul>
 | 
					                        </ul>
 | 
				
			||||||
                    </div>
 | 
					                        <div class="card-body">
 | 
				
			||||||
 | 
					                            <input class="form-control mx-auto d-block" type="text" id="note" />
 | 
				
			||||||
                    {# Tabs content #}
 | 
					                            <ul class="list-group list-group-flush" id="alias_matched">
 | 
				
			||||||
                    <div class="card-body">
 | 
					                            </ul>
 | 
				
			||||||
                        <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 %}
 | 
					 | 
				
			||||||
                                            <button class="btn btn-outline-dark rounded-0 flex-fill"
 | 
					 | 
				
			||||||
                                                    name="button" value="{{ button.name }}">
 | 
					 | 
				
			||||||
                                                {{ button.name }} ({{ button.amount | pretty_money }})
 | 
					 | 
				
			||||||
                                            </button>
 | 
					 | 
				
			||||||
                                        {% endfor %}
 | 
					 | 
				
			||||||
                                    </div>
 | 
					 | 
				
			||||||
                                </div>
 | 
					 | 
				
			||||||
                            {% endfor %}
 | 
					 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <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>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </form>
 | 
					
 | 
				
			||||||
 | 
					        {# 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 %}
 | 
				
			||||||
 | 
					                                        {% if button.display %}
 | 
				
			||||||
 | 
					                                            <button class="btn btn-outline-dark rounded-0 flex-fill"
 | 
				
			||||||
 | 
					                                                    id="button{{ button.id }}" name="button" value="{{ button.name }}">
 | 
				
			||||||
 | 
					                                                {{ button.name }} ({{ button.amount | pretty_money }})
 | 
				
			||||||
 | 
					                                            </button>
 | 
				
			||||||
 | 
					                                        {% endif %}
 | 
				
			||||||
 | 
					                                    {% 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>
 | 
				
			||||||
 | 
					    </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 %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block extrajavascript %}
 | 
					{% block extrajavascript %}
 | 
				
			||||||
 | 
					    <script type="text/javascript" src="/static/js/consos.js"></script>
 | 
				
			||||||
    <script type="text/javascript">
 | 
					    <script type="text/javascript">
 | 
				
			||||||
        $(document).ready(function() {
 | 
					        {% for button in most_used %}
 | 
				
			||||||
            // If hash of a category in the URL, then select this category
 | 
					            {% if button.display %}
 | 
				
			||||||
            // else select the first one
 | 
					                $("#most_used_button{{ button.id }}").click(function() {
 | 
				
			||||||
            if (location.hash) {
 | 
					                    addConso({{ button.destination.id }}, {{ button.amount }},
 | 
				
			||||||
                $("a[href='" + location.hash + "']").tab("show");
 | 
					                        {{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
 | 
				
			||||||
            } else {
 | 
					                        {{ button.id }}, "{{ button.name }}");
 | 
				
			||||||
                $("a[data-toggle='tab']").first().tab("show");
 | 
					                });
 | 
				
			||||||
            }
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // When selecting a category, change URL
 | 
					        {% for button in transaction_templates %}
 | 
				
			||||||
            $(document.body).on("click", "a[data-toggle='tab']", function(event) {
 | 
					            {% if button.display %}
 | 
				
			||||||
                location.hash = this.getAttribute("href");
 | 
					                $("#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>
 | 
					    </script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,35 +3,192 @@
 | 
				
			|||||||
SPDX-License-Identifier: GPL-2.0-or-later
 | 
					SPDX-License-Identifier: GPL-2.0-or-later
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% load i18n static %}
 | 
					{% load i18n static django_tables2 perms %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
    <form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %}
 | 
					
 | 
				
			||||||
        {% if form.non_field_errors %}
 | 
					    <div class="row">
 | 
				
			||||||
            <p class="errornote">
 | 
					        <div class="col-xl-12">
 | 
				
			||||||
                {% for error in form.non_field_errors %}
 | 
					            <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
 | 
				
			||||||
                    {{ error }}
 | 
					                <label for="type_gift" class="btn btn-sm btn-outline-primary active">
 | 
				
			||||||
                {% endfor %}
 | 
					                    <input type="radio" name="transaction_type" id="type_gift" checked>
 | 
				
			||||||
            </p>
 | 
					                    {% trans "Gift" %}
 | 
				
			||||||
        {% endif %}
 | 
					                </label>
 | 
				
			||||||
        <fieldset class="module aligned">
 | 
					                <label for="type_transfer" class="btn btn-sm btn-outline-primary">
 | 
				
			||||||
            {% for field in form %}
 | 
					                    <input type="radio" name="transaction_type" id="type_transfer">
 | 
				
			||||||
                <div class="form-row{% if field.errors %} errors{% endif %}">
 | 
					                    {% trans "Transfer" %}
 | 
				
			||||||
                    {{ field.errors }}
 | 
					                </label>
 | 
				
			||||||
                    <div>
 | 
					                {% if "note.notespecial"|not_empty_model_list %}
 | 
				
			||||||
                        {{ field.label_tag }}
 | 
					                    <label for="type_credit" class="btn btn-sm btn-outline-primary">
 | 
				
			||||||
                        {% if field.is_readonly %}
 | 
					                        <input type="radio" name="transaction_type" id="type_credit">
 | 
				
			||||||
                            <div class="readonly">{{ field.contents }}</div>
 | 
					                        {% trans "Credit" %}
 | 
				
			||||||
                        {% else %}
 | 
					                    </label>
 | 
				
			||||||
                            {{ field }}
 | 
					                    <label type="type_debit" class="btn btn-sm btn-outline-primary">
 | 
				
			||||||
                        {% endif %}
 | 
					                        <input type="radio" name="transaction_type" id="type_debit">
 | 
				
			||||||
                        {% if field.field.help_text %}
 | 
					                        {% trans "Debit" %}
 | 
				
			||||||
                            <div class="help">{{ field.field.help_text|safe }}</div>
 | 
					                    </label>
 | 
				
			||||||
                        {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					            </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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% if "note.notespecial"|not_empty_model_list %}
 | 
				
			||||||
 | 
					            <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>
 | 
				
			||||||
            {% endfor %}
 | 
					            </div>
 | 
				
			||||||
        </fieldset>
 | 
					        {% endif %}
 | 
				
			||||||
        <input type="submit" value="{% trans 'Transfer' %}">
 | 
					
 | 
				
			||||||
    </form>
 | 
					        <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>
 | 
				
			||||||
 | 
					                <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 %}
 | 
					{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% load static %}
 | 
					{% load static %}
 | 
				
			||||||
 | 
					{% load i18n %}
 | 
				
			||||||
{% load crispy_forms_tags %}
 | 
					{% load crispy_forms_tags %}
 | 
				
			||||||
{% block content %}
 | 
					{% 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">
 | 
					<form method="post">
 | 
				
			||||||
{% csrf_token %}
 | 
					{% csrf_token %}
 | 
				
			||||||
{{form|crispy}}
 | 
					{{form|crispy}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
 | 
				
			|||||||
            {% endblocktrans %}
 | 
					            {% endblocktrans %}
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					    {%url 'cas_login'  as cas_url %}
 | 
				
			||||||
 | 
					    {% if cas_url %}
 | 
				
			||||||
 | 
					    <div class="alert alert-info">
 | 
				
			||||||
 | 
					        {% trans "You can also register via the central authentification server " %}
 | 
				
			||||||
 | 
					         <a href="{{ cas_url }}"> {% trans "using this link "%}</a>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    {%endif%}
 | 
				
			||||||
    <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
 | 
					    <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
 | 
				
			||||||
        {{ form | crispy }}
 | 
					        {{ form | crispy }}
 | 
				
			||||||
        <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
 | 
					        <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							@@ -10,7 +10,6 @@ setenv =
 | 
				
			|||||||
	PYTHONWARNINGS = all
 | 
						PYTHONWARNINGS = all
 | 
				
			||||||
deps =
 | 
					deps =
 | 
				
			||||||
    -r{toxinidir}/requirements/base.txt
 | 
					    -r{toxinidir}/requirements/base.txt
 | 
				
			||||||
    -r{toxinidir}/requirements/api.txt
 | 
					 | 
				
			||||||
    -r{toxinidir}/requirements/cas.txt
 | 
					    -r{toxinidir}/requirements/cas.txt
 | 
				
			||||||
    -r{toxinidir}/requirements/production.txt
 | 
					    -r{toxinidir}/requirements/production.txt
 | 
				
			||||||
    coverage
 | 
					    coverage
 | 
				
			||||||
@@ -22,7 +21,6 @@ commands =
 | 
				
			|||||||
[testenv:linters]
 | 
					[testenv:linters]
 | 
				
			||||||
deps =
 | 
					deps =
 | 
				
			||||||
    -r{toxinidir}/requirements/base.txt
 | 
					    -r{toxinidir}/requirements/base.txt
 | 
				
			||||||
    -r{toxinidir}/requirements/api.txt
 | 
					 | 
				
			||||||
    -r{toxinidir}/requirements/cas.txt
 | 
					    -r{toxinidir}/requirements/cas.txt
 | 
				
			||||||
    -r{toxinidir}/requirements/production.txt
 | 
					    -r{toxinidir}/requirements/production.txt
 | 
				
			||||||
    flake8
 | 
					    flake8
 | 
				
			||||||
@@ -32,7 +30,7 @@ deps =
 | 
				
			|||||||
    pep8-naming
 | 
					    pep8-naming
 | 
				
			||||||
    pyflakes
 | 
					    pyflakes
 | 
				
			||||||
commands =
 | 
					commands =
 | 
				
			||||||
    flake8 apps/activity apps/api apps/member apps/note
 | 
					    flake8 apps/activity apps/api apps/logs apps/member apps/note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[flake8]
 | 
					[flake8]
 | 
				
			||||||
# Ignore too many errors, should be reduced in the future
 | 
					# Ignore too many errors, should be reduced in the future
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user