Merge branch 'consos' into rights

# Conflicts:
#	apps/logs/signals.py
#	note_kfet/settings/base.py
This commit is contained in:
Yohann D'ANELLO 2020-03-17 21:11:14 +01:00
commit 112d4b6c5a
81 changed files with 2850 additions and 818 deletions

13
.env_example Normal file
View 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
View File

@ -0,0 +1,3 @@
[submodule "apps/scripts"]
path = apps/scripts
url = git@gitlab.crans.org:bde/nk20-scripts.git

View File

@ -9,10 +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/
RUN pip install -r requirements.txt
COPY . /code/ COPY . /code/
# Comment what is not needed
RUN pip install -r requirements/base.txt
RUN pip install -r requirements/api.txt
RUN pip install -r requirements/cas.txt
RUN pip install -r requirements/production.txt
ENTRYPOINT ["/code/entrypoint.sh"] ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 8000 EXPOSE 8000

View File

@ -31,7 +31,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.txt (env)$ pip3 install -r requirements/base.txt
(env)$ deactivate (env)$ deactivate
4. uwsgi et Nginx 4. uwsgi et Nginx
@ -40,14 +40,13 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
$ cp nginx_note.conf_example nginx_note.conf $ cp nginx_note.conf_example nginx_note.conf
***Modifier le fichier pour être en accord avec le reste de votre config*** ***Modifier le fichier pour être en accord avec le reste de votre config***
On utilise uwsgi et Nginx pour gérer le coté serveu : On utilise uwsgi et Nginx pour gérer le coté serveur :
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/ $ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
Si l'on a un emperor (plusieurs instance uwsgi):
Si l'on a un emperor (plusieurs instance uwsgi):
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/ $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
@ -85,7 +84,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
postgres=# CREATE DATABASE note_db OWNER note; 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 +95,29 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres 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"
DJANGO_DEV_STORE_METHOD="sqllite"
DJANGO_DB_HOST="localhost"
DJANGO_DB_NAME="note_db"
DJANGO_DB_USER="note"
DJANGO_DB_PASSWORD="CHANGE_ME"
DJANGO_DB_PORT=""
DJANGO_SECRET_KEY="CHANGE_ME"
DJANGO_SETTINGS_MODULE="note_kfet.settings"
DOMAIN="localhost"
CONTACT_EMAIL="tresorerie.bde@localhost"
NOTE_URL="localhost"
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
$ source /env/bin/activate $ 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 +132,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 :
@ -159,17 +169,20 @@ un serveur de développement par exemple sur son ordinateur.
$ source venv/bin/activate $ source venv/bin/activate
(env)$ pip install -r requirements.txt (env)$ pip install -r requirements.txt
3. Migrations et chargement des données initiales : 3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
ce qu'il faut
4. Migrations et chargement des données initiales :
(env)$ ./manage.py makemigrations (env)$ ./manage.py 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 +197,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 !**

View File

@ -11,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin):
Admin customisation for Activity Admin customisation for Activity
""" """
list_display = ('name', 'activity_type', 'organizer') list_display = ('name', 'activity_type', 'organizer')
list_filter = ('activity_type', ) list_filter = ('activity_type',)
search_fields = ['name', 'organizer__name'] search_fields = ['name', 'organizer__name']
# Organize activities by start date # Organize activities by start date

View File

@ -11,6 +11,7 @@ class ActivityTypeSerializer(serializers.ModelSerializer):
REST API Serializer for Activity types. REST API Serializer for Activity types.
The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API. The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API.
""" """
class Meta: class Meta:
model = ActivityType model = ActivityType
fields = '__all__' fields = '__all__'
@ -21,6 +22,7 @@ class ActivitySerializer(serializers.ModelSerializer):
REST API Serializer for Activities. REST API Serializer for Activities.
The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API. The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Activity model = Activity
fields = '__all__' fields = '__all__'
@ -31,6 +33,7 @@ class GuestSerializer(serializers.ModelSerializer):
REST API Serializer for Guests. REST API Serializer for Guests.
The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API. The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Guest model = Guest
fields = '__all__' fields = '__all__'

View File

@ -1,10 +1,11 @@
# 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_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import SearchFilter
from ..models import ActivityType, Activity, Guest
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
from ..models import ActivityType, Activity, Guest
class ActivityTypeViewSet(viewsets.ModelViewSet): class ActivityTypeViewSet(viewsets.ModelViewSet):
@ -15,6 +16,8 @@ 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(viewsets.ModelViewSet):
@ -25,6 +28,8 @@ 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(viewsets.ModelViewSet):
@ -35,3 +40,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', ]

View File

@ -3,10 +3,14 @@
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 django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from rest_framework.filters import SearchFilter
from activity.api.urls import register_activity_urls from activity.api.urls import register_activity_urls
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
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -14,6 +18,7 @@ class UserSerializer(serializers.ModelSerializer):
REST API Serializer for Users. REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API. The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
""" """
class Meta: class Meta:
model = User model = User
exclude = ( exclude = (
@ -23,6 +28,17 @@ class UserSerializer(serializers.ModelSerializer):
) )
class ContentTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
class Meta:
model = ContentType
fields = '__all__'
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
""" """
REST API View set. REST API View set.
@ -31,15 +47,30 @@ 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', ]
class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/
"""
queryset = ContentType.objects.all()
serializer_class = ContentTypeSerializer
# Routers provide an easy way of automatically determining the URL conf. # 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_logs_urls(router, 'logs')
app_name = 'api' app_name = 'api'

View File

View 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
View 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
View 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 import viewsets
from rest_framework.filters import OrderingFilter
from .serializers import ChangelogSerializer
from ..models import Changelog
class ChangelogViewSet(viewsets.ReadOnlyModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
then render it on /api/logs/
"""
queryset = Changelog.objects.all()
serializer_class = ChangelogSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
ordering_fields = ['timestamp', ]
ordering = ['-timestamp', ]

View File

@ -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)

55
apps/logs/middlewares.py Normal file
View File

@ -0,0 +1,55 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from threading import local
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
_thread_locals = local()
def _set_current_user_and_ip(user=None, ip=None):
setattr(_thread_locals, USER_ATTR_NAME, user)
setattr(_thread_locals, IP_ATTR_NAME, ip)
def get_current_user():
return getattr(_thread_locals, USER_ATTR_NAME, None)
def get_current_ip():
return getattr(_thread_locals, IP_ATTR_NAME, None)
def get_current_authenticated_user():
current_user = get_current_user()
if isinstance(current_user, AnonymousUser):
return None
return current_user
class LogsMiddleware(object):
"""
This middleware get the current user with his or her IP address on each request.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
user = request.user
if 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR')
else:
ip = request.META.get('REMOTE_ADDR')
_set_current_user_and_ip(user, ip)
response = self.get_response(request)
_set_current_user_and_ip(None, None)
return response

View File

@ -1,16 +1,16 @@
# 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.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
class Changelog(models.Model): class Changelog(models.Model):
""" """
Store each modification on the database (except sessions and logging), Store each modification in the database (except sessions and logging),
including creating, editing and deleting models. including creating, editing and deleting models.
""" """
@ -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'),
) )

View File

@ -1,66 +1,40 @@
# 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
import getpass
from note.models import NoteUser, Alias
from .middlewares import get_current_authenticated_user, get_current_ip
from .models import Changelog from .models import Changelog
def get_request_in_signal(sender): # Ces modèles ne nécessitent pas de logs
req = None
for entry in reversed(inspect.stack()):
try:
req = entry[0].f_locals['request']
# Check if there is a user
# noinspection PyStatementEffect
req.user
break
except:
pass
if not req:
print("WARNING: Attempt to save " + str(sender) + " with no user")
return req
def get_user_and_ip(sender):
req = get_request_in_signal(sender)
try:
user = req.user
if 'HTTP_X_FORWARDED_FOR' in req.META:
ip = req.META.get('HTTP_X_FORWARDED_FOR')
else:
ip = req.META.get('REMOTE_ADDR')
except:
user = None
ip = None
return user, ip
EXCLUDED = [ EXCLUDED = [
'admin.logentry', 'admin.logentry',
'authtoken.token', 'authtoken.token',
'cas_server.user', 'cas_server.proxygrantingticket',
'cas_server.userattributes', 'cas_server.proxyticket',
'contenttypes.contenttype', 'cas_server.serviceticket',
'logs.changelog', 'cas_server.user',
'migrations.migration', 'cas_server.userattributes',
'note.noteuser', 'contenttypes.contenttype',
'note.noteclub', 'logs.changelog', # Never remove this line
'note.notespecial', 'migrations.migration',
'sessions.session', 'note.note' # We only store the subclasses
'reversion.revision', 'note.transaction',
'reversion.version', 'sessions.session',
] ]
@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()
@ -68,30 +42,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,
@ -104,15 +99,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),

View File

@ -18,9 +18,9 @@ class ProfileInline(admin.StackedInline):
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline, ) inlines = (ProfileInline,)
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_select_related = ('profile', ) list_select_related = ('profile',)
form = ProfileForm form = ProfileForm
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):

View File

@ -11,6 +11,7 @@ class ProfileSerializer(serializers.ModelSerializer):
REST API Serializer for Profiles. REST API Serializer for Profiles.
The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API. The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
@ -21,6 +22,7 @@ class ClubSerializer(serializers.ModelSerializer):
REST API Serializer for Clubs. REST API Serializer for Clubs.
The djangorestframework plugin will analyse the model `Club` and parse all fields in the API. The djangorestframework plugin will analyse the model `Club` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Club model = Club
fields = '__all__' fields = '__all__'
@ -31,6 +33,7 @@ class RoleSerializer(serializers.ModelSerializer):
REST API Serializer for Roles. REST API Serializer for Roles.
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Role model = Role
fields = '__all__' fields = '__all__'
@ -41,6 +44,7 @@ class MembershipSerializer(serializers.ModelSerializer):
REST API Serializer for Memberships. REST API Serializer for Memberships.
The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API. The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Membership model = Membership
fields = '__all__' fields = '__all__'

View File

@ -2,9 +2,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import SearchFilter
from ..models import Profile, Club, Role, Membership
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
from ..models import Profile, Club, Role, Membership
class ProfileViewSet(viewsets.ModelViewSet): class ProfileViewSet(viewsets.ModelViewSet):
@ -25,6 +26,8 @@ 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(viewsets.ModelViewSet):
@ -35,6 +38,8 @@ 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(viewsets.ModelViewSet):

View File

@ -1,11 +1,11 @@
# 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_filters import FilterSet, CharFilter
from django.contrib.auth.models import User
from django.db.models import CharField
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit from crispy_forms.layout import Layout, Submit
from django.contrib.auth.models import User
from django.db.models import CharField
from django_filters import FilterSet, CharFilter
class UserFilter(FilterSet): class UserFilter(FilterSet):

View File

@ -1,23 +1,22 @@
# 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 crispy_forms.bootstrap import Div
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout
from dal import autocomplete from dal import autocomplete
from django import forms
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django import forms
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm): class SignUpForm(UserCreationForm):
def __init__(self,*args,**kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs) super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.pop("autofocus", None) self.fields['username'].widget.attrs.pop("autofocus", None)
self.fields['first_name'].widget.attrs.update({"autofocus":"autofocus"}) self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
class Meta: class Meta:
model = User model = User
@ -28,6 +27,7 @@ class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. A form for the extras field provided by the :model:`member.Profile` model.
""" """
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
@ -42,7 +42,7 @@ class ClubForm(forms.ModelForm):
class AddMembersForm(forms.Form): class AddMembersForm(forms.Form):
class Meta: class Meta:
fields = ('', ) fields = ('',)
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
@ -54,13 +54,13 @@ class MembershipForm(forms.ModelForm):
# et récupère les noms d'utilisateur valides # et récupère les noms d'utilisateur valides
widgets = { widgets = {
'user': 'user':
autocomplete.ModelSelect2( autocomplete.ModelSelect2(
url='member:user_autocomplete', url='member:user_autocomplete',
attrs={ attrs={
'data-placeholder': 'Nom ...', 'data-placeholder': 'Nom ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
}, },
), ),
} }

View File

@ -5,8 +5,8 @@ import datetime
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
class Profile(models.Model): class Profile(models.Model):
@ -48,9 +48,10 @@ 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,))
@ -100,7 +101,7 @@ class Club(models.Model):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk, )) return reverse_lazy('member:club_detail', args=(self.pk,))
class Role(models.Model): class Role(models.Model):
@ -161,7 +162,7 @@ class Membership(models.Model):
class Meta: class Meta:
verbose_name = _('membership') verbose_name = _('membership')
verbose_name_plural = _('memberships') verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]
class RolePermissions(models.Model): class RolePermissions(models.Model):
""" """

View File

@ -1,6 +1,7 @@
# 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
def save_user_profile(instance, created, raw, **_kwargs): def save_user_profile(instance, created, raw, **_kwargs):
""" """
Hook to create and save a profile when an user is updated if it is not registered with the signup form Hook to create and save a profile when an user is updated if it is not registered with the signup form

View File

@ -1,33 +1,33 @@
# 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.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView,DeleteView
from django.views.generic.edit import FormMixin
from django.contrib.auth.models import User
from django.contrib import messages
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect
from django.db.models import Q
from django.core.exceptions import ValidationError
from django.conf import settings
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from dal import autocomplete
from PIL import Image
import io import io
from PIL import Image
from dal import autocomplete
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView, DeleteView
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
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 note.forms import AliasForm, ImageForm
from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .tables import ClubTable, UserTable
from .filters import UserFilter, UserFilterFormHelper from .filters import UserFilter, UserFilterFormHelper
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .models import Club, Membership
from .tables import ClubTable, UserTable
class UserCreateView(CreateView): class UserCreateView(CreateView):
@ -49,10 +49,10 @@ class UserCreateView(CreateView):
def form_valid(self, form): def form_valid(self, form):
profile_form = ProfileForm(self.request.POST) profile_form = ProfileForm(self.request.POST)
if form.is_valid() and profile_form.is_valid(): if form.is_valid() and profile_form.is_valid():
user = form.save() user = form.save(commit=False)
profile = profile_form.save(commit=False) user.profile = profile_form.save(commit=False)
profile.user = user user.save()
profile.save() user.profile.save()
return super().form_valid(form) return super().form_valid(form)
@ -109,7 +109,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
return reverse_lazy('member:user_detail', return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']}) kwargs={'pk': kwargs['id']})
else: else:
return reverse_lazy('member:user_detail', args=(self.object.id, )) return reverse_lazy('member:user_detail', args=(self.object.id,))
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
@ -124,7 +124,7 @@ class UserDetailView(LoginRequiredMixin, DetailView):
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")
@ -157,13 +157,14 @@ class UserListView(LoginRequiredMixin, SingleTableView):
context["filter"] = self.filter context["filter"] = self.filter
return context return context
class AliasView(LoginRequiredMixin,FormMixin,DetailView):
class AliasView(LoginRequiredMixin, FormMixin, DetailView):
model = User model = User
template_name = 'member/profile_alias.html' template_name = 'member/profile_alias.html'
context_object_name = 'user_object' context_object_name = 'user_object'
form_class = AliasForm form_class = AliasForm
def get_context_data(self,**kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['user_object'].note note = context['user_object'].note
context["aliases"] = AliasTable(note.alias_set.all()) context["aliases"] = AliasTable(note.alias_set.all())
@ -172,7 +173,7 @@ class AliasView(LoginRequiredMixin,FormMixin,DetailView):
def get_success_url(self): def get_success_url(self):
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id}) return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id})
def post(self,request,*args,**kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
@ -186,42 +187,45 @@ class AliasView(LoginRequiredMixin,FormMixin,DetailView):
alias.save() alias.save()
return super().form_valid(form) return super().form_valid(form)
class DeleteAliasView(LoginRequiredMixin, DeleteView): class DeleteAliasView(LoginRequiredMixin, DeleteView):
model = Alias model = Alias
def delete(self,request,*args,**kwargs): def delete(self, request, *args, **kwargs):
try: try:
self.object = self.get_object() self.object = self.get_object()
self.object.delete() self.object.delete()
except ValidationError as e: except ValidationError as e:
# TODO: pass message to redirected view. # TODO: pass message to redirected view.
messages.error(self.request,str(e)) messages.error(self.request, str(e))
else: else:
messages.success(self.request,_("Alias successfully deleted")) messages.success(self.request, _("Alias successfully deleted"))
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) 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):
return self.post(request, *args, **kwargs) return self.post(request, *args, **kwargs)
class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
model = User model = User
template_name = 'member/profile_picture_update.html' template_name = 'member/profile_picture_update.html'
context_object_name = 'user_object' context_object_name = 'user_object'
form_class = ImageForm form_class = ImageForm
def get_context_data(self,*args,**kwargs):
context = super().get_context_data(*args,**kwargs) def get_context_data(self, *args, **kwargs):
context['form'] = self.form_class(self.request.POST,self.request.FILES) context = super().get_context_data(*args, **kwargs)
context['form'] = self.form_class(self.request.POST, self.request.FILES)
return context return context
def get_success_url(self): def get_success_url(self):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id}) return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})
def post(self,request,*args,**kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
self.object = self.get_object() self.object = self.get_object()
if form.is_valid(): if form.is_valid():
return self.form_valid(form) return self.form_valid(form)
@ -230,7 +234,7 @@ class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
print(form) print(form)
return self.form_invalid(form) return self.form_invalid(form)
def form_valid(self,form): def form_valid(self, form):
image_field = form.cleaned_data['image'] image_field = form.cleaned_data['image']
x = form.cleaned_data['x'] x = form.cleaned_data['x']
y = form.cleaned_data['y'] y = form.cleaned_data['y']
@ -238,23 +242,24 @@ class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
h = form.cleaned_data['height'] h = form.cleaned_data['height']
# image crop and resize # image crop and resize
image_file = io.BytesIO(image_field.read()) image_file = io.BytesIO(image_field.read())
ext = image_field.name.split('.')[-1] # ext = image_field.name.split('.')[-1].lower()
# TODO: support GIF format
image = Image.open(image_file) image = Image.open(image_file)
image = image.crop((x, y, x+w, y+h)) image = image.crop((x, y, x + w, y + h))
image_clean = image.resize((settings.PIC_WIDTH, image_clean = image.resize((settings.PIC_WIDTH,
settings.PIC_RATIO*settings.PIC_WIDTH), settings.PIC_RATIO * settings.PIC_WIDTH),
Image.ANTIALIAS) Image.ANTIALIAS)
image_file = io.BytesIO() image_file = io.BytesIO()
image_clean.save(image_file,ext) image_clean.save(image_file, "PNG")
image_field.file = image_file image_field.file = image_file
# renaming # renaming
filename = "{}_pic.{}".format(self.object.note.pk, ext) filename = "{}_pic.png".format(self.object.note.pk)
image_field.name = filename image_field.name = filename
self.object.note.display_image = image_field self.object.note.display_image = image_field
self.object.note.save() self.object.note.save()
return super().form_valid(form) return super().form_valid(form)
class ManageAuthTokens(LoginRequiredMixin, TemplateView): class ManageAuthTokens(LoginRequiredMixin, TemplateView):
""" """
Affiche le jeton d'authentification, et permet de le regénérer Affiche le jeton d'authentification, et permet de le regénérer
@ -282,6 +287,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
""" """
Auto complete users by usernames Auto complete users by usernames
""" """
def get_queryset(self): def get_queryset(self):
""" """
Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion. Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion.
@ -294,7 +300,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
qs = User.objects.all() qs = User.objects.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
@ -330,7 +336,7 @@ class ClubDetailView(LoginRequiredMixin, DetailView):
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"]
club_transactions = \ club_transactions = \
Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))
context['history_list'] = HistoryTable(club_transactions) context['history_list'] = HistoryTable(club_transactions)
club_member = \ club_member = \

View File

@ -47,11 +47,11 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
""" """
Child for a club note, see NoteAdmin Child for a club note, see NoteAdmin
""" """
inlines = (AliasInlines, ) inlines = (AliasInlines,)
# We can't change club after creation or the balance # We can't change club after creation or the balance
readonly_fields = ('club', 'balance') readonly_fields = ('club', 'balance')
search_fields = ('club', ) search_fields = ('club',)
def has_add_permission(self, request): def has_add_permission(self, request):
""" """
@ -71,7 +71,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin):
""" """
Child for a special note, see NoteAdmin Child for a special note, see NoteAdmin
""" """
readonly_fields = ('balance', ) readonly_fields = ('balance',)
@admin.register(NoteUser) @admin.register(NoteUser)
@ -79,7 +79,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
""" """
Child for an user note, see NoteAdmin Child for an user note, see NoteAdmin
""" """
inlines = (AliasInlines, ) inlines = (AliasInlines,)
# We can't change user after creation or the balance # We can't change user after creation or the balance
readonly_fields = ('user', 'balance') readonly_fields = ('user', 'balance')
@ -133,7 +133,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
Else the amount of money would not be transferred Else the amount of money would not be transferred
""" """
if obj: # user is editing an existing object if obj: # user is editing an existing object
return 'created_at', 'source', 'destination', 'quantity',\ return 'created_at', 'source', 'destination', 'quantity', \
'amount' 'amount'
return [] return []
@ -143,9 +143,9 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
""" """
Admin customisation for TransactionTemplate Admin customisation for TransactionTemplate
""" """
list_display = ('name', 'poly_destination', 'amount', 'category', 'display', ) list_display = ('name', 'poly_destination', 'amount', 'category', 'display',)
list_filter = ('category', 'display') list_filter = ('category', 'display')
autocomplete_fields = ('destination', ) autocomplete_fields = ('destination',)
def poly_destination(self, obj): def poly_destination(self, obj):
""" """
@ -161,5 +161,5 @@ class TemplateCategoryAdmin(admin.ModelAdmin):
""" """
Admin customisation for TransactionTemplate Admin customisation for TransactionTemplate
""" """
list_display = ('name', ) list_display = ('name',)
list_filter = ('name', ) list_filter = ('name',)

View File

@ -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, \
TemplateTransaction, SpecialTransaction
class NoteSerializer(serializers.ModelSerializer): class NoteSerializer(serializers.ModelSerializer):
@ -13,15 +14,10 @@ class NoteSerializer(serializers.ModelSerializer):
REST API Serializer for Notes. REST API Serializer for Notes.
The djangorestframework plugin will analyse the model `Note` and parse all fields in the API. The djangorestframework plugin will analyse the model `Note` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Note model = Note
fields = '__all__' fields = '__all__'
extra_kwargs = {
'url': {
'view_name': 'project-detail',
'lookup_field': 'pk'
},
}
class NoteClubSerializer(serializers.ModelSerializer): class NoteClubSerializer(serializers.ModelSerializer):
@ -29,40 +25,60 @@ 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__'
def get_name(self, obj):
return str(obj)
class NoteSpecialSerializer(serializers.ModelSerializer): 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__'
def get_name(self, obj):
return str(obj)
class NoteUserSerializer(serializers.ModelSerializer): 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__'
def get_name(self, obj):
return str(obj)
class AliasSerializer(serializers.ModelSerializer): class AliasSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Aliases. REST API Serializer for Aliases.
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
""" """
note = serializers.SerializerMethodField()
class Meta: class Meta:
model = Alias model = Alias
fields = '__all__' fields = '__all__'
def get_note(self, alias):
return NotePolymorphicSerializer().to_representation(alias.note)
class NotePolymorphicSerializer(PolymorphicSerializer): class NotePolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = { model_serializer_mapping = {
@ -73,11 +89,23 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
} }
class TemplateCategorySerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transaction templates.
The djangorestframework plugin will analyse the model `TemplateCategory` and parse all fields in the API.
"""
class Meta:
model = TemplateCategory
fields = '__all__'
class TransactionTemplateSerializer(serializers.ModelSerializer): class TransactionTemplateSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Transaction templates. REST API Serializer for Transaction templates.
The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API. The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API.
""" """
class Meta: class Meta:
model = TransactionTemplate model = TransactionTemplate
fields = '__all__' fields = '__all__'
@ -88,16 +116,49 @@ class TransactionSerializer(serializers.ModelSerializer):
REST API Serializer for Transactions. REST API Serializer for Transactions.
The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Transaction model = Transaction
fields = '__all__' fields = '__all__'
class TemplateTransactionSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transactions.
The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API.
"""
class Meta:
model = TemplateTransaction
fields = '__all__'
class MembershipTransactionSerializer(serializers.ModelSerializer): class MembershipTransactionSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Membership transactions. REST API Serializer for Membership transactions.
The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API. The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API.
""" """
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,
TemplateTransaction: TemplateTransactionSerializer,
MembershipTransaction: MembershipTransactionSerializer,
SpecialTransaction: SpecialTransactionSerializer,
}

View File

@ -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)

View File

@ -2,13 +2,15 @@
# 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 django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import OrderingFilter, SearchFilter
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \ NoteUserSerializer, AliasSerializer, \
TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NoteViewSet(viewsets.ModelViewSet): class NoteViewSet(viewsets.ModelViewSet):
@ -59,6 +61,9 @@ 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):
""" """
@ -69,8 +74,8 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
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.lower()))
note_type = self.request.query_params.get("type", None) note_type = self.request.query_params.get("type", None)
if note_type: if note_type:
@ -80,12 +85,11 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
elif "club" in types: elif "club" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteclub") queryset = queryset.filter(polymorphic_ctype__model="noteclub")
elif "special" in types: elif "special" in types:
queryset = queryset.filter( queryset = queryset.filter(polymorphic_ctype__model="notespecial")
polymorphic_ctype__model="notespecial")
else: else:
queryset = queryset.none() queryset = queryset.none()
return queryset return queryset.distinct()
class AliasViewSet(viewsets.ModelViewSet): class AliasViewSet(viewsets.ModelViewSet):
@ -96,6 +100,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):
""" """
@ -107,7 +114,7 @@ class AliasViewSet(viewsets.ModelViewSet):
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.lower()))
note_id = self.request.query_params.get("note", None) note_id = self.request.query_params.get("note", None)
if note_id: if note_id:
@ -131,6 +138,18 @@ class AliasViewSet(viewsets.ModelViewSet):
return queryset return queryset
class TemplateCategoryViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/category/
"""
queryset = TemplateCategory.objects.all()
serializer_class = TemplateCategorySerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
class TransactionTemplateViewSet(viewsets.ModelViewSet): class TransactionTemplateViewSet(viewsets.ModelViewSet):
""" """
REST API View set. REST API View set.
@ -139,6 +158,8 @@ 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(viewsets.ModelViewSet):
@ -148,14 +169,6 @@ class TransactionViewSet(viewsets.ModelViewSet):
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

View File

@ -3,7 +3,7 @@
"model": "note.note", "model": "note.note",
"pk": 1, "pk": 1,
"fields": { "fields": {
"polymorphic_ctype": 22, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -14,7 +14,7 @@
"model": "note.note", "model": "note.note",
"pk": 2, "pk": 2,
"fields": { "fields": {
"polymorphic_ctype": 22, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -25,7 +25,7 @@
"model": "note.note", "model": "note.note",
"pk": 3, "pk": 3,
"fields": { "fields": {
"polymorphic_ctype": 22, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -36,7 +36,7 @@
"model": "note.note", "model": "note.note",
"pk": 4, "pk": 4,
"fields": { "fields": {
"polymorphic_ctype": 22, "polymorphic_ctype": 40,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -47,7 +47,7 @@
"model": "note.note", "model": "note.note",
"pk": 5, "pk": 5,
"fields": { "fields": {
"polymorphic_ctype": 21, "polymorphic_ctype": 39,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",
@ -58,7 +58,7 @@
"model": "note.note", "model": "note.note",
"pk": 6, "pk": 6,
"fields": { "fields": {
"polymorphic_ctype": 21, "polymorphic_ctype": 39,
"balance": 0, "balance": 0,
"is_active": true, "is_active": true,
"display_image": "", "display_image": "",

View File

@ -3,31 +3,25 @@
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import os from .models import Alias
from .models import TransactionTemplate
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout, HTML
from .models import Transaction, TransactionTemplate, TemplateTransaction
from .models import Note, Alias
class AliasForm(forms.ModelForm): class AliasForm(forms.ModelForm):
class Meta: class Meta:
model = Alias model = Alias
fields = ("name",) fields = ("name",)
def __init__(self,*args,**kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs) super().__init__(*args, **kwargs)
self.fields["name"].label = False self.fields["name"].label = False
self.fields["name"].widget.attrs={"placeholder":_('New Alias')} self.fields["name"].widget.attrs = {"placeholder": _('New Alias')}
class ImageForm(forms.Form): class ImageForm(forms.Form):
image = forms.ImageField(required = False, image = forms.ImageField(required=False,
label=_('select an image'), label=_('select an image'),
help_text=_('Maximal size: 2MB')) help_text=_('Maximal size: 2MB'))
x = forms.FloatField(widget=forms.HiddenInput()) x = forms.FloatField(widget=forms.HiddenInput())
@ -35,7 +29,7 @@ class ImageForm(forms.Form):
width = forms.FloatField(widget=forms.HiddenInput()) width = forms.FloatField(widget=forms.HiddenInput())
height = forms.FloatField(widget=forms.HiddenInput()) height = forms.FloatField(widget=forms.HiddenInput())
class TransactionTemplateForm(forms.ModelForm): class TransactionTemplateForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionTemplate model = TransactionTemplate
@ -48,92 +42,11 @@ class TransactionTemplateForm(forms.ModelForm):
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = { widgets = {
'destination': 'destination':
autocomplete.ModelSelect2( autocomplete.ModelSelect2(
url='note:note_autocomplete', url='note:note_autocomplete',
attrs={ attrs={
'data-placeholder': 'Note ...', 'data-placeholder': 'Note ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
}, },
), ),
}
class TransactionForm(forms.ModelForm):
def save(self, commit=True):
super().save(commit)
def clean(self):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
Transactions between a note and the same note are not authorized.
"""
cleaned_data = super().clean()
if not "source" in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds"
cleaned_data["source"] = self.user.note
if cleaned_data["source"].pk == cleaned_data["destination"].pk:
self.add_error("destination", _("Source and destination must be different."))
return cleaned_data
class Meta:
model = Transaction
fields = (
'source',
'destination',
'reason',
'amount',
)
# Voir ci-dessus
widgets = {
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
}
class ConsoForm(forms.ModelForm):
def save(self, commit=True):
button: TransactionTemplate = TransactionTemplate.objects.filter(
name=self.data['button']).get()
self.instance.destination = button.destination
self.instance.amount = button.amount
self.instance.reason = '{} ({})'.format(button.name, button.category)
self.instance.name = button.name
self.instance.category = button.category
super().save(commit)
class Meta:
model = TemplateTransaction
fields = ('source', )
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les aliases de note valides
widgets = {
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
} }

View File

@ -9,6 +9,7 @@ from django.core.validators import RegexValidator
from django.db import models from django.db import models
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
""" """
Defines each note types Defines each note types
""" """
@ -27,7 +28,7 @@ class Note(PolymorphicModel):
help_text=_('in centimes, money credited for this instance'), help_text=_('in centimes, money credited for this instance'),
default=0, default=0,
) )
last_negative= models.DateTimeField( last_negative = models.DateTimeField(
verbose_name=_('last negative date'), verbose_name=_('last negative date'),
help_text=_('last time the balance was negative'), help_text=_('last time the balance was negative'),
null=True, null=True,
@ -98,7 +99,7 @@ class Note(PolymorphicModel):
# Alias exists, so check if it is linked to this note # Alias exists, so check if it is linked to this note
if aliases.first().note != self: if aliases.first().note != self:
raise ValidationError(_('This alias is already taken.'), raise ValidationError(_('This alias is already taken.'),
code="same_alias",) code="same_alias", )
else: else:
# Alias does not exist yet, so check if it can exist # Alias does not exist yet, so check if it can exist
a = Alias(name=str(self)) a = Alias(name=str(self))
@ -208,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
@ -230,13 +235,13 @@ 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:
pass pass
self.normalized_name = normalized_name self.normalized_name = normalized_name
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if self.name == str(self.note): if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."), raise ValidationError(_("You can't delete your main alias."),

View File

@ -2,12 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse
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
@ -44,7 +44,7 @@ class TransactionTemplate(models.Model):
verbose_name=_('name'), verbose_name=_('name'),
max_length=255, max_length=255,
unique=True, unique=True,
error_messages={'unique':_("A template with this name already exist")}, error_messages={'unique': _("A template with this name already exist")},
) )
destination = models.ForeignKey( destination = models.ForeignKey(
NoteClub, NoteClub,
@ -63,11 +63,12 @@ class TransactionTemplate(models.Model):
max_length=31, max_length=31,
) )
display = models.BooleanField( display = models.BooleanField(
default = True, default=True,
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'), verbose_name=_('description'),
max_length=255, max_length=255,
blank=True,
) )
class Meta: class Meta:
@ -75,7 +76,7 @@ class TransactionTemplate(models.Model):
verbose_name_plural = _("transaction templates") verbose_name_plural = _("transaction templates")
def get_absolute_url(self): def get_absolute_url(self):
return reverse('note:template_update', args=(self.pk, )) return reverse('note:template_update', args=(self.pk,))
class Transaction(PolymorphicModel): class Transaction(PolymorphicModel):
@ -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
@ -151,11 +161,14 @@ class Transaction(PolymorphicModel):
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 TemplateTransaction(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 +181,37 @@ 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):
""" """
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
@ -183,3 +227,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')

View File

@ -1,45 +1,77 @@
# 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 .models.transactions import Transaction from django.utils.translation import gettext_lazy as _
from .models.notes import Alias from .models.notes import Alias
from .models.transactions import Transaction
from .templatetags.pretty_money import pretty_money
class HistoryTable(tables.Table): class HistoryTable(tables.Table):
class Meta: class Meta:
attrs = { attrs = {
'class': 'class':
'table table-condensed table-striped table-hover' 'table table-condensed table-striped table-hover'
} }
model = Transaction model = Transaction
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):
return pretty_money(value)
def render_total(self, value):
return pretty_money(value)
def render_type(self, value):
return _(value)
# Django-tables escape strings. That's a wrong thing.
def render_reason(self, value):
return html.unescape(value)
def render_valid(self, value):
return "" if value else ""
class AliasTable(tables.Table): class AliasTable(tables.Table):
class Meta: class Meta:
attrs = { attrs = {
'class': 'class':
'table table condensed table-striped table-hover' 'table table condensed table-striped table-hover'
} }
model = Alias model = Alias
fields =('name',) fields = ('name',)
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
show_header = False show_header = False
name = tables.Column(attrs={'td':{'class':'text-center'}}) name = tables.Column(attrs={'td': {'class': 'text-center'}})
delete = tables.LinkColumn('member:user_alias_delete', delete = tables.LinkColumn('member:user_alias_delete',
args=[A('pk')], args=[A('pk')],
attrs={ attrs={
'td': {'class':'col-sm-2'}, 'td': {'class': 'col-sm-2'},
'a': {'class': 'btn btn-danger'} }, 'a': {'class': 'btn btn-danger'}},
text='delete',accessor='pk') text='delete', accessor='pk')

View 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)

View File

@ -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,

View File

@ -3,56 +3,49 @@
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 .models import Transaction, TransactionTemplate, Alias, TemplateTransaction from .forms import TransactionTemplateForm
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial
from .models.transactions import SpecialTransaction
from .tables import HistoryTable
class TransactionCreate(LoginRequiredMixin, CreateView): class TransactionCreate(LoginRequiredMixin, SingleTableView):
""" """
Show transfer page Show transfer page
TODO: If user have sufficient rights, they can transfer from an other note TODO: If user have sufficient rights, they can transfer from an other note
""" """
model = Transaction queryset = Transaction.objects.order_by("-id").all()[:50]
form_class = TransactionForm template_name = "note/transaction_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 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):
""" """
Auto complete note by aliases Auto complete note by aliases
""" """
def get_queryset(self): def get_queryset(self):
""" """
Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion.
@ -66,7 +59,7 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
# self.q est le paramètre de la recherche # 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)
@ -120,31 +113,31 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class ConsoView(LoginRequiredMixin, CreateView): class ConsoView(LoginRequiredMixin, SingleTableView):
""" """
Consume Consume
""" """
model = TemplateTransaction queryset = Transaction.objects.order_by("-id").all()[:50]
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_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(display=True) \
context['title'] = _("Consommations") .annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name')
context['transaction_templates'] = buttons
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
context['title'] = _("Consumptions")
context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk
# select2 compatibility # 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')

View File

@ -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

View File

@ -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-02-27 17:39+0100\n" "POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "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:184 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:11 #: 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:160 #: 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,43 +87,59 @@ 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:20 apps/note/models/notes.py:105 #: apps/logs/models.py:21 apps/note/models/notes.py:117
msgid "user" msgid "user"
msgstr "" msgstr ""
#: apps/logs/models.py:27 #: apps/logs/models.py:27
msgid "IP Address"
msgstr ""
#: apps/logs/models.py:35
msgid "model" msgid "model"
msgstr "" msgstr ""
#: apps/logs/models.py:34 #: apps/logs/models.py:42
msgid "identifier" msgid "identifier"
msgstr "" msgstr ""
#: apps/logs/models.py:39 #: apps/logs/models.py:47
msgid "previous data" msgid "previous data"
msgstr "" msgstr ""
#: apps/logs/models.py:44 #: apps/logs/models.py:52
msgid "new data" msgid "new data"
msgstr "" msgstr ""
#: apps/logs/models.py:51 #: apps/logs/models.py:60
msgid "create"
msgstr ""
#: apps/logs/models.py:61
msgid "edit"
msgstr ""
#: apps/logs/models.py:62
msgid "delete"
msgstr ""
#: apps/logs/models.py:65
msgid "action" msgid "action"
msgstr "" msgstr ""
#: apps/logs/models.py:59 #: apps/logs/models.py:73
msgid "timestamp" msgid "timestamp"
msgstr "" msgstr ""
#: apps/logs/models.py:63 #: apps/logs/models.py:77
msgid "Logs cannot be destroyed." msgid "Logs cannot be destroyed."
msgstr "" msgstr ""
#: apps/member/apps.py:10 #: apps/member/apps.py:14
msgid "member" msgid "member"
msgstr "" msgstr ""
@ -130,7 +147,7 @@ msgstr ""
msgid "phone number" msgid "phone number"
msgstr "" msgstr ""
#: apps/member/models.py:29 templates/member/profile_detail.html:24 #: apps/member/models.py:29 templates/member/profile_detail.html:28
msgid "section" msgid "section"
msgstr "" msgstr ""
@ -138,7 +155,7 @@ msgstr ""
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "" msgstr ""
#: apps/member/models.py:36 templates/member/profile_detail.html:27 #: apps/member/models.py:36 templates/member/profile_detail.html:31
msgid "address" msgid "address"
msgstr "" msgstr ""
@ -150,199 +167,207 @@ 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:135 #: 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 ""
#: apps/member/views.py:63 templates/member/profile_detail.html:42 #: apps/member/views.py:69 templates/member/profile_detail.html:46
msgid "Update Profile" msgid "Update Profile"
msgstr "" msgstr ""
#: apps/member/views.py:79 #: apps/member/views.py:82
msgid "An alias with a similar name already exists." msgid "An alias with a similar name already exists."
msgstr "" msgstr ""
#: apps/member/views.py:130 #: apps/member/views.py:132
#, python-format #, python-format
msgid "Account #%(id)s: %(username)s" msgid "Account #%(id)s: %(username)s"
msgstr "" msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:93 #: apps/member/views.py:202
msgid "Alias successfully deleted"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source" 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:54 #: apps/note/apps.py:14 apps/note/models/notes.py:58
msgid "note" msgid "note"
msgstr "" msgstr ""
#: apps/note/forms.py:49 #: apps/note/forms.py:20
msgid "Source and destination must be different." msgid "New Alias"
msgstr "" msgstr ""
#: apps/note/models/notes.py:26 #: apps/note/forms.py:25
msgid "account balance" msgid "select an image"
msgstr ""
#: apps/note/forms.py:26
msgid "Maximal size: 2MB"
msgstr "" msgstr ""
#: apps/note/models/notes.py:27 #: apps/note/models/notes.py:27
msgid "account balance"
msgstr ""
#: 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:49 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:55 #: apps/note/models/notes.py:59
msgid "notes" msgid "notes"
msgstr "" msgstr ""
#: apps/note/models/notes.py:63 #: apps/note/models/notes.py:67
msgid "Note" msgid "Note"
msgstr "" msgstr ""
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97 #: apps/note/models/notes.py:77 apps/note/models/notes.py:101
msgid "This alias is already taken." msgid "This alias is already taken."
msgstr "" msgstr ""
#: apps/note/models/notes.py:113 #: apps/note/models/notes.py:121
msgid "user"
msgstr ""
#: apps/note/models/notes.py:117
msgid "one's note" msgid "one's note"
msgstr "" msgstr ""
#: apps/note/models/notes.py:118 #: apps/note/models/notes.py:122
msgid "users note" msgid "users note"
msgstr "" msgstr ""
#: apps/note/models/notes.py:124 #: 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:139 #: apps/note/models/notes.py:143
msgid "club note" msgid "club note"
msgstr "" msgstr ""
#: apps/note/models/notes.py:140 #: apps/note/models/notes.py:144
msgid "clubs notes" msgid "clubs notes"
msgstr "" msgstr ""
#: apps/note/models/notes.py:146 #: 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:166 #: apps/note/models/notes.py:170
msgid "special note" msgid "special note"
msgstr "" msgstr ""
#: apps/note/models/notes.py:167 #: apps/note/models/notes.py:171
msgid "special notes" msgid "special notes"
msgstr "" msgstr ""
#: apps/note/models/notes.py:190 #: apps/note/models/notes.py:194
msgid "Invalid alias" msgid "Invalid alias"
msgstr "" msgstr ""
#: apps/note/models/notes.py:206 #: apps/note/models/notes.py:210
msgid "alias" msgid "alias"
msgstr "" msgstr ""
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33 #: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
msgid "aliases" msgid "aliases"
msgstr "" msgstr ""
@ -351,10 +376,10 @@ msgid "Alias is too long."
msgstr "" msgstr ""
#: apps/note/models/notes.py:238 #: 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:246 #: apps/note/models/notes.py:247
msgid "You can't delete your main alias." msgid "You can't delete your main alias."
msgstr "" msgstr ""
@ -370,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 ""
@ -378,59 +403,96 @@ 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:155 #: note_kfet/settings/__init__.py:61
msgid "German" msgid ""
"The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials "
"again unless your session expires or you logout."
msgstr "" msgstr ""
#: note_kfet/settings/base.py:156 #: note_kfet/settings/base.py:156
msgid "English" msgid "German"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:157 #: note_kfet/settings/base.py:157
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:158
msgid "French" msgid "French"
msgstr "" msgstr ""
@ -438,6 +500,78 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "" msgstr ""
#: templates/base.html:81
msgid "Clubs"
msgstr ""
#: templates/base.html:84
msgid "Activities"
msgstr ""
#: templates/base.html:87
msgid "Buttons"
msgstr ""
#: templates/cas_server/base.html:7
msgid "Central Authentication Service"
msgstr ""
#: templates/cas_server/base.html:43
#, python-format
msgid ""
"A new version of the application is available. This instance runs "
"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider "
"upgrading."
msgstr ""
#: templates/cas_server/logged.html:4
msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central "
"Authentication Service.<br/>For security reasons, please Log Out and Exit "
"your web browser when you are done accessing services that require "
"authentication!"
msgstr ""
#: templates/cas_server/logged.html:8
msgid "Log me out from all my sessions"
msgstr ""
#: templates/cas_server/logged.html:14
msgid "Forget the identity provider"
msgstr ""
#: templates/cas_server/logged.html:18
msgid "Logout"
msgstr ""
#: templates/cas_server/login.html:6
msgid "Please log in"
msgstr ""
#: templates/cas_server/login.html:11
msgid ""
"If you don't have any Note Kfet account, please follow <a href='/accounts/"
"signup'>this link to sign up</a>."
msgstr ""
#: templates/cas_server/login.html:17
msgid "Login"
msgstr ""
#: templates/cas_server/warn.html:9
msgid "Connect to the service"
msgstr ""
#: templates/django_filters/rest_framework/crispy_form.html:4
#: templates/django_filters/rest_framework/form.html:2
msgid "Field filters"
msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr ""
#: templates/member/club_detail.html:10 #: templates/member/club_detail.html:10
msgid "Membership starts on" msgid "Membership starts on"
msgstr "" msgstr ""
@ -450,10 +584,22 @@ msgstr ""
msgid "Membership duration" msgid "Membership duration"
msgstr "" msgstr ""
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 #: templates/member/club_detail.html:18 templates/member/profile_detail.html:34
msgid "balance" msgid "balance"
msgstr "" msgstr ""
#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75
msgid "Transaction history"
msgstr ""
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr ""
#: templates/member/club_list.html:8
msgid "New club"
msgstr ""
#: templates/member/manage_auth_tokens.html:16 #: templates/member/manage_auth_tokens.html:16
msgid "Token" msgid "Token"
msgstr "" msgstr ""
@ -466,27 +612,35 @@ msgstr ""
msgid "Regenerate token" msgid "Regenerate token"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:11 #: templates/member/profile_alias.html:10
msgid "Add alias"
msgstr ""
#: templates/member/profile_detail.html:15
msgid "first name" msgid "first name"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:14 #: templates/member/profile_detail.html:18
msgid "username" msgid "username"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:17 #: templates/member/profile_detail.html:21
msgid "password" msgid "password"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:20 #: templates/member/profile_detail.html:24
msgid "Change password" msgid "Change password"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:38 #: templates/member/profile_detail.html:42
msgid "Manage auth token" msgid "Manage auth token"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:54 #: templates/member/profile_detail.html:49
msgid "View Profile"
msgstr ""
#: templates/member/profile_detail.html:62
msgid "View my memberships" msgid "View my memberships"
msgstr "" msgstr ""
@ -494,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
@ -511,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 ""
@ -523,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 ""

View File

@ -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-02-27 17:39+0100\n" "POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "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:184 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:11 #: 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:160 #: 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,47 +82,59 @@ 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:20 apps/note/models/notes.py:105 #: 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
msgid "IP Address"
msgstr "Adresse IP"
#: apps/logs/models.py:35
msgid "model" msgid "model"
msgstr "Modèle" msgstr "Modèle"
#: apps/logs/models.py:34 #: apps/logs/models.py:42
msgid "identifier" msgid "identifier"
msgstr "Identifiant" msgstr "Identifiant"
#: apps/logs/models.py:39 #: apps/logs/models.py:47
msgid "previous data" msgid "previous data"
msgstr "Données précédentes" msgstr "Données précédentes"
#: apps/logs/models.py:44 #: 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:51 #: 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:59 #: apps/logs/models.py:73
msgid "timestamp" msgid "timestamp"
msgstr "Date" msgstr "Date"
#: apps/logs/models.py:63 #: 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."
#: apps/member/apps.py:10 #: apps/member/apps.py:14
msgid "member" msgid "member"
msgstr "adhérent" msgstr "adhérent"
@ -129,7 +142,7 @@ msgstr "adhérent"
msgid "phone number" msgid "phone number"
msgstr "numéro de téléphone" msgstr "numéro de téléphone"
#: apps/member/models.py:29 templates/member/profile_detail.html:24 #: apps/member/models.py:29 templates/member/profile_detail.html:28
msgid "section" msgid "section"
msgstr "section" msgstr "section"
@ -137,7 +150,7 @@ msgstr "section"
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
#: apps/member/models.py:36 templates/member/profile_detail.html:27 #: apps/member/models.py:36 templates/member/profile_detail.html:31
msgid "address" msgid "address"
msgstr "adresse" msgstr "adresse"
@ -149,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."
@ -187,166 +200,174 @@ 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:135 #: 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"
#: apps/member/views.py:63 templates/member/profile_detail.html:42 #: apps/member/views.py:69 templates/member/profile_detail.html:46
msgid "Update Profile" msgid "Update Profile"
msgstr "Modifier le profil" msgstr "Modifier le profil"
#: apps/member/views.py:79 #: apps/member/views.py:82
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/member/views.py:130 #: apps/member/views.py:132
#, python-format #, python-format
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/note/admin.py:120 apps/note/models/transactions.py:93 #: apps/member/views.py:202
msgid "Alias successfully deleted"
msgstr "L'alias a bien été supprimé"
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source" 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:54 #: apps/note/apps.py:14 apps/note/models/notes.py:58
msgid "note" msgid "note"
msgstr "note" msgstr "note"
#: apps/note/forms.py:49 #: apps/note/forms.py:20
msgid "Source and destination must be different." msgid "New Alias"
msgstr "La source et la destination doivent être différentes." msgstr "Nouvel alias"
#: apps/note/models/notes.py:26 #: apps/note/forms.py:25
msgid "select an image"
msgstr "Choisissez une image"
#: apps/note/forms.py:26
msgid "Maximal size: 2MB"
msgstr "Taille maximale : 2 Mo"
#: apps/note/models/notes.py:27
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:49 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:55 #: apps/note/models/notes.py:59
msgid "notes" msgid "notes"
msgstr "notes" msgstr "notes"
#: apps/note/models/notes.py:63 #: apps/note/models/notes.py:67
msgid "Note" msgid "Note"
msgstr "Note" msgstr "Note"
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97 #: apps/note/models/notes.py:77 apps/note/models/notes.py:101
msgid "This alias is already taken." 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:113 #: apps/note/models/notes.py:121
msgid "user"
msgstr "utilisateur"
#: apps/note/models/notes.py:117
msgid "one's note" msgid "one's note"
msgstr "note d'un utilisateur" msgstr "note d'un utilisateur"
#: apps/note/models/notes.py:118 #: 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:124 #: 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:139 #: 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:140 #: 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:146 #: 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:166 #: 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:167 #: 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:190 #: apps/note/models/notes.py:194
msgid "Invalid alias" msgid "Invalid alias"
msgstr "Alias invalide" msgstr "Alias invalide"
#: apps/note/models/notes.py:206 #: apps/note/models/notes.py:210
msgid "alias" msgid "alias"
msgstr "alias" msgstr "alias"
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33 #: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
msgid "aliases" msgid "aliases"
msgstr "alias" msgstr "alias"
@ -355,10 +376,10 @@ msgid "Alias is too long."
msgstr "L'alias est trop long." msgstr "L'alias est trop long."
#: apps/note/models/notes.py:238 #: 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 "Un alias avec un nom similaire existe déjà." msgstr "Un alias avec un nom similaire existe déjà : {}"
#: apps/note/models/notes.py:246 #: apps/note/models/notes.py:247
msgid "You can't delete your main alias." 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."
@ -371,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"
@ -383,59 +403,96 @@ 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:155 #: note_kfet/settings/__init__.py:61
msgid "German" msgid ""
"The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials "
"again unless your session expires or you logout."
msgstr "" msgstr ""
#: note_kfet/settings/base.py:156 #: note_kfet/settings/base.py:156
msgid "English" msgid "German"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:157 #: note_kfet/settings/base.py:157
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:158
msgid "French" msgid "French"
msgstr "" msgstr ""
@ -443,6 +500,80 @@ msgstr ""
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/base.html:81
msgid "Clubs"
msgstr "Clubs"
#: templates/base.html:84
msgid "Activities"
msgstr "Activités"
#: templates/base.html:87
msgid "Buttons"
msgstr "Boutons"
#: templates/cas_server/base.html:7
msgid "Central Authentication Service"
msgstr ""
#: templates/cas_server/base.html:43
#, python-format
msgid ""
"A new version of the application is available. This instance runs "
"%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider "
"upgrading."
msgstr ""
#: templates/cas_server/logged.html:4
msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central "
"Authentication Service.<br/>For security reasons, please Log Out and Exit "
"your web browser when you are done accessing services that require "
"authentication!"
msgstr ""
#: templates/cas_server/logged.html:8
msgid "Log me out from all my sessions"
msgstr ""
#: templates/cas_server/logged.html:14
msgid "Forget the identity provider"
msgstr ""
#: templates/cas_server/logged.html:18
msgid "Logout"
msgstr ""
#: templates/cas_server/login.html:6
msgid "Please log in"
msgstr ""
#: templates/cas_server/login.html:11
msgid ""
"If you don't have any Note Kfet account, please follow <a href='/accounts/"
"signup'>this link to sign up</a>."
msgstr ""
"Si vous n'avez pas de compte Note Kfet, veuillez suivre <a href='/accounts/"
"signup'>ce lien pour vous inscrire</a>."
#: templates/cas_server/login.html:17
msgid "Login"
msgstr ""
#: templates/cas_server/warn.html:9
msgid "Connect to the service"
msgstr ""
#: templates/django_filters/rest_framework/crispy_form.html:4
#: templates/django_filters/rest_framework/form.html:2
msgid "Field filters"
msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr "Envoyer"
#: templates/member/club_detail.html:10 #: 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"
@ -455,10 +586,22 @@ msgstr "L'adhésion finie le"
msgid "Membership duration" msgid "Membership duration"
msgstr "Durée de l'adhésion" msgstr "Durée de l'adhésion"
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 #: templates/member/club_detail.html:18 templates/member/profile_detail.html:34
msgid "balance" msgid "balance"
msgstr "solde du compte" msgstr "solde du compte"
#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75
msgid "Transaction history"
msgstr "Historique des transactions"
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr "Liste des clubs"
#: templates/member/club_list.html:8
msgid "New club"
msgstr "Nouveau club"
#: templates/member/manage_auth_tokens.html:16 #: templates/member/manage_auth_tokens.html:16
msgid "Token" msgid "Token"
msgstr "Jeton" msgstr "Jeton"
@ -471,33 +614,35 @@ msgstr "Créé le"
msgid "Regenerate token" msgid "Regenerate token"
msgstr "Regénérer le jeton" msgstr "Regénérer le jeton"
#: templates/member/profile_detail.html:11 #: templates/member/profile_alias.html:10
msgid "Add alias"
msgstr "Ajouter un alias"
#: templates/member/profile_detail.html:15
msgid "first name" msgid "first name"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:14 #: templates/member/profile_detail.html:18
msgid "username" msgid "username"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:17 #: templates/member/profile_detail.html:21
#, fuzzy
#| msgid "Change password"
msgid "password" msgid "password"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:20 #: templates/member/profile_detail.html:24
msgid "Change password" msgid "Change password"
msgstr "Changer le mot de passe" msgstr "Changer le mot de passe"
#: templates/member/profile_detail.html:38 #: templates/member/profile_detail.html:42
msgid "Manage auth token" msgid "Manage auth token"
msgstr "Gérer les jetons d'authentification" msgstr "Gérer les jetons d'authentification"
#: templates/member/profile_detail.html:51 #: templates/member/profile_detail.html:49
msgid "Transaction history" msgid "View Profile"
msgstr "Historique des transactions" msgstr "Voir le profil"
#: templates/member/profile_detail.html:54 #: templates/member/profile_detail.html:62
msgid "View my memberships" msgid "View my memberships"
msgstr "Voir mes adhésions" msgstr "Voir mes adhésions"
@ -505,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."
@ -522,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 ""
@ -534,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 ""

View File

@ -9,7 +9,7 @@ server {
# the port your site will be served on # the port your site will be served on
listen 80; listen 80;
# the domain name it will serve for # the domain name it will serve for
server_name note.comby.xyz; # substitute your machine's IP address or FQDN server_name note.example.org; # substitute your machine's IP address or FQDN
charset utf-8; charset utf-8;
# max upload size # max upload size

View File

@ -0,0 +1,11 @@
[
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 1,
"pattern": ".*",
"name": "REPLACEME"
}
}
]

View File

@ -6,14 +6,5 @@
"domain": "localhost", "domain": "localhost",
"name": "La Note Kfet \ud83c\udf7b" "name": "La Note Kfet \ud83c\udf7b"
} }
},
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 1,
"pattern": ".*",
"name": "REPLACEME"
}
} }
] ]

View File

@ -1,10 +1,6 @@
# 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.http import HttpResponseRedirect
from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
class TurbolinksMiddleware(object): class TurbolinksMiddleware(object):
""" """
@ -35,4 +31,3 @@ class TurbolinksMiddleware(object):
location = request.session.pop('_turbolinks_redirect_to') location = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = location response['Turbolinks-Location'] = location
return response return response

View File

@ -1,8 +1,12 @@
import os # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
import re import re
from .base import * from .base import *
def read_env(): def read_env():
"""Pulled from Honcho code with minor updates, reads local default """Pulled from Honcho code with minor updates, reads local default
environment variables from a .env file located in the project root environment variables from a .env file located in the project root
@ -25,22 +29,53 @@ def read_env():
val = re.sub(r'\\(.)', r'\1', m3.group(1)) val = re.sub(r'\\(.)', r'\1', m3.group(1))
os.environ.setdefault(key, val) os.environ.setdefault(key, val)
read_env() 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.append(os.environ.get('ALLOWED_HOSTS','localhost'))
else: else:
from .development import * from .development import *
try: try:
#in secrets.py defines everything you want
from .secrets import * from .secrets import *
except ImportError: except ImportError:
pass pass
# env variables set at the of in /env/bin/activate if "cas" in INSTALLED_APPS:
# don't forget to unset in deactivate ! MIDDLEWARE += ['cas.middleware.CASMiddleware']
# CAS Settings
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
CAS_SHOW_SERVICE_MESSAGES = True
CAS_SHOW_POWERED = False
CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
CAS_PROVIDE_URL_TO_LOGOUT = True
CAS_INFO_MESSAGES = {
"cas_explained": {
"message": _(
u"The Central Authentication Service grants you access to most of our websites by "
u"authenticating only once, so you don't need to type your credentials again unless "
u"your session expires or you logout."
),
"discardable": True,
"type": "info", # one of info, success, info, warning, danger
},
}
CAS_INFO_MESSAGES_ORDER = [
'cas_explained',
]
AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',)
if "logs" in INSTALLED_APPS:
MIDDLEWARE += ('logs.middlewares.LogsMiddleware',)
if "debug_toolbar" in INSTALLED_APPS:
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ['127.0.0.1']

View File

@ -37,9 +37,10 @@ INSTALLED_APPS = [
# External apps # External apps
'polymorphic', 'polymorphic',
'reversion',
'crispy_forms', 'crispy_forms',
'django_tables2', 'django_tables2',
'cas_server',
'cas',
# Django contrib # Django contrib
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.admindocs', 'django.contrib.admindocs',
@ -55,9 +56,6 @@ INSTALLED_APPS = [
# Autocomplete # Autocomplete
'dal', 'dal',
'dal_select2', 'dal_select2',
# CAS
'cas_server',
'cas',
# Note apps # Note apps
'activity', 'activity',
@ -81,7 +79,6 @@ MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware', 'note_kfet.middlewares.TurbolinksMiddleware',
'cas.middleware.CASMiddleware',
] ]
ROOT_URLCONF = 'note_kfet.urls' ROOT_URLCONF = 'note_kfet.urls'
@ -98,7 +95,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request', 'django.template.context_processors.request',
# 'django.template.context_processors.media', # 'django.template.context_processors.media',
], ],
}, },
}, },
@ -133,7 +130,7 @@ PASSWORD_HASHERS = [
# Django Guardian object permissions # Django Guardian object permissions
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
#'django.contrib.auth.backends.ModelBackend', # this is default # 'django.contrib.auth.backends.ModelBackend', # this is default
'member.backends.PermissionBackend', 'member.backends.PermissionBackend',
'cas.backends.CASBackend', 'cas.backends.CASBackend',
) )
@ -146,12 +143,13 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
], ],
'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,
} }
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/ # https://docs.djangoproject.com/en/2.2/topics/i18n/
@ -182,7 +180,7 @@ FIXTURE_DIRS = [os.path.join(BASE_DIR, "note_kfet/fixtures")]
# Don't put anything in this directory yourself; store your static files # Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS. # in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/" # Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.join(BASE_DIR,"static/") STATIC_ROOT = os.path.join(BASE_DIR, "static/")
# STATICFILES_DIRS = [ # STATICFILES_DIRS = [
# os.path.join(BASE_DIR, 'static')] # os.path.join(BASE_DIR, 'static')]
STATICFILES_DIRS = [] STATICFILES_DIRS = []
@ -194,15 +192,9 @@ STATIC_URL = '/static/'
ALIAS_VALIDATOR_REGEX = r'' ALIAS_VALIDATOR_REGEX = r''
MEDIA_ROOT=os.path.join(BASE_DIR,"media") MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL='/media/' MEDIA_URL = '/media/'
# Profile Picture Settings # Profile Picture Settings
PIC_WIDTH = 200 PIC_WIDTH = 200
PIC_RATIO = 1 PIC_RATIO = 1
# CAS Settings
CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"

View File

@ -11,17 +11,30 @@
# - and more ... # - and more ...
import os
# Database # Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
from . import * from . import *
import os
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
@ -38,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
@ -51,4 +64,8 @@ SESSION_COOKIE_AGE = 60 * 60 * 3
# CAS Client settings # CAS Client settings
# Can be modified in secrets.py # Can be modified in secrets.py
CAS_SERVER_URL = "https://note.comby.xyz/cas/" CAS_SERVER_URL = "http://localhost:8000/cas/"
STATIC_ROOT = '' # not needed in development settings
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')]

View File

@ -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 = ['127.0.0.1','note.comby.xyz'] ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
# Emails # 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/"

View File

@ -1,8 +1,9 @@
# 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' # CAS
OPTIONAL_APPS = [
# TODO User interface # 'cas_server',
urlpatterns = [ # 'cas',
# 'debug_toolbar'
] ]

View File

@ -1,13 +1,11 @@
# 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.conf.urls.static import static
from django.contrib import admin 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 django.conf.urls.static import static
from django.conf import settings
from cas import views as cas_views
urlpatterns = [ urlpatterns = [
# Dev so redirect to something random # Dev so redirect to something random
@ -16,25 +14,34 @@ urlpatterns = [
# Include project routers # Include project routers
path('note/', include('note.urls')), path('note/', include('note.urls')),
# Include CAS Client routers
path('accounts/login/', cas_views.login, name='login'),
path('accounts/logout/', cas_views.logout, name='logout'),
# Include Django Contrib and Core routers # 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('member.urls')),
path('accounts/', include('django.contrib.auth.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),
# Include CAS Server routers
path('cas/', include('cas_server.urls', namespace="cas_server")),
# Include Django REST API
path('api/', include('api.urls')), path('api/', include('api.urls')),
path('logs/', include('logs.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL,document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if "cas_server" in settings.INSTALLED_APPS:
urlpatterns += [
# Include CAS Server routers
path('cas/', include('cas_server.urls', namespace="cas_server")),
]
if "cas" in settings.INSTALLED_APPS:
from cas import views as cas_views
urlpatterns += [
# Include CAS Client routers
path('accounts/login/cas/', cas_views.login, name='cas_login'),
path('accounts/logout/cas/', cas_views.logout, name='cas_logout'),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

3
requirements/api.txt Normal file
View File

@ -0,0 +1,3 @@
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8

View File

@ -4,18 +4,12 @@ defusedxml==0.6.0
Django~=2.2 Django~=2.2
django-allauth==0.39.1 django-allauth==0.39.1
django-autocomplete-light==3.5.1 django-autocomplete-light==3.5.1
django-cas-client==1.5.3
django-cas-server==1.1.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-extensions==2.1.9 django-extensions==2.1.9
django-filter==2.2.0 django-filter==2.2.0
django-polymorphic==2.0.3 django-polymorphic==2.0.3
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8
django-reversion==3.0.3
django-tables2==2.1.0 django-tables2==2.1.0
docutils==0.14 docutils==0.14
psycopg2==2.8.4
idna==2.8 idna==2.8
oauthlib==3.1.0 oauthlib==3.1.0
Pillow==6.1.0 Pillow==6.1.0

2
requirements/cas.txt Normal file
View File

@ -0,0 +1,2 @@
django-cas-client==1.5.3
django-cas-server==1.1.0

View File

@ -0,0 +1 @@
psycopg2==2.8.4

281
static/js/base.js Normal file
View File

@ -0,0 +1,281 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Convert balance in cents to a human readable amount
* @param value the balance, in cents
* @returns {string}
*/
function pretty_money(value) {
if (value % 100 === 0)
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
else
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "."
+ (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €";
}
/**
* Add a message on the top of the page.
* @param msg The message to display
* @param alert_type The type of the alert. Choices: info, success, warning, danger
*/
function addMsg(msg, alert_type) {
let msgDiv = $("#messages");
let html = msgDiv.html();
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" +
"<button class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>"
+ msg + "</div>\n";
msgDiv.html(html);
}
/**
* Reload the balance of the user on the right top corner
*/
function refreshBalance() {
$("#user_balance").load("/ #user_balance");
}
/**
* Query the 20 first matched notes with a given pattern
* @param pattern The pattern that is queried
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
}
/**
* Generate a <li> entry with a given id and text
*/
function li(id, text) {
return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
" id=\"" + id + "\">" + text + "</li>\n";
}
/**
* Render note name and picture
* @param note The note to render
* @param alias The alias to be displayed
* @param user_note_field
* @param profile_pic_field
*/
function displayNote(note, alias, user_note_field=null, profile_pic_field=null) {
let img = note == null ? null : note.display_image;
if (img == null)
img = '/media/pic/default.png';
if (note !== null && alias !== note.name)
alias += " (aka. " + note.name + ")";
if (note !== null && user_note_field !== null)
$("#" + user_note_field).text(alias + " : " + pretty_money(note.balance));
if (profile_pic_field != null)
$("#" + profile_pic_field).attr('src', img);
}
/**
* Remove a note from the emitters.
* @param d The note to remove
* @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events
*/
function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) {
return (function() {
let new_notes_display = [];
let html = "";
notes_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0;
new_notes_display.push(disp);
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
}
});
notes_display.length = 0;
new_notes_display.forEach(function(disp) {
notes_display.push(disp);
});
$("#" + note_list_id).html(html);
notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id);
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function() {
if (disp.note)
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
});
});
}
/**
* Generate an auto-complete field to query a note with its alias
* @param field_id The identifier of the text field where the alias is typed
* @param alias_matched_id The div block identifier where the matched aliases are displayed
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param notes An array containing the note objects of the buyers
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param alias_prefix The prefix of the <li> blocks for the matched aliases
* @param note_prefix The prefix of the <li> blocks for the notes of the buyers
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true,
* the associated note is not displayed.
* Useful for a consumption if the item is selected before.
*/
function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias",
note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) {
let field = $("#" + field_id);
// When the user clicks on the search field, it is immediately cleared
field.click(function() {
field.val("");
});
let old_pattern = null;
// When the user type "Enter", the first alias is clicked
field.keypress(function(event) {
if (event.originalEvent.charCode === 13)
$("#" + alias_matched_id + " li").first().trigger("click");
});
// When the user type something, the matched aliases are refreshed
field.keyup(function(e) {
if (e.originalEvent.charCode === 13)
return;
let pattern = field.val();
// If the pattern is not modified, we don't query the API
if (pattern === old_pattern || pattern === "")
return;
old_pattern = pattern;
// Clear old matched notes
notes.length = 0;
let aliases_matched_obj = $("#" + alias_matched_id);
let aliases_matched_html = "";
// Get matched notes with the given pattern
getMatchedNotes(pattern, function(aliases) {
// The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val())
return;
aliases.results.forEach(function (alias) {
let note = alias.note;
aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name);
note.alias = alias;
notes.push(note);
});
// Display the list of matched aliases
aliases_matched_obj.html(aliases_matched_html);
notes.forEach(function (note) {
let alias = note.alias;
let alias_obj = $("#" + alias_prefix + "_" + alias.id);
// When an alias is hovered, the profile picture and the balance are displayed at the right place
alias_obj.hover(function () {
displayNote(note, alias.name, user_note_field, profile_pic_field);
});
// When the user click on an alias, the associated note is added to the emitters
alias_obj.click(function () {
field.val("");
// If the note is already an emitter, we increase the quantity
var disp = null;
notes_display.forEach(function (d) {
// We compare the note ids
if (d.id === note.id) {
d.quantity += 1;
disp = d;
}
});
// In the other case, we add a new emitter
if (disp == null) {
disp = {
name: alias.name,
id: note.id,
note: note,
quantity: 1
};
notes_display.push(disp);
}
// If the function alias_click exists, it is called. If it doesn't return true, then the notes are
// note displayed. Useful for a consumption when a button is already clicked
if (alias_click && !alias_click())
return;
let note_list = $("#" + note_list_id);
let html = "";
notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
});
// Emitters are displayed
note_list.html(html);
notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id);
// Hover an emitter display also the profile picture
line_obj.hover(function () {
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
// When an emitter is clicked, it is removed
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field));
});
});
});
});
});
}
// When a validate button is clicked, we switch the validation status
function de_validate(id, validated) {
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
// Perform a PATCH request to the API in order to update the transaction
// If the user has insuffisent rights, an error message will appear
$.ajax({
"url": "/api/note/transaction/transaction/" + id + "/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
"resourcetype": "TemplateTransaction",
valid: !validated
},
success: function () {
// Refresh jQuery objects
$(".validate").click(de_validate);
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
},
error: function(err) {
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + err.responseText, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
}
});
}

205
static/js/consos.js Normal file
View File

@ -0,0 +1,205 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Refresh the history table on the consumptions page.
*/
function refreshHistory() {
$("#history").load("/note/consos/ #history");
$("#most_used").load("/note/consos/ #most_used");
}
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function() {
location.hash = this.getAttribute("href");
});
// Switching in double consumptions mode should update the layout
let double_conso_obj = $("#double_conso");
double_conso_obj.click(function() {
$("#consos_list_div").show();
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
$("#note_infos_div").attr('class', 'col-xl-3');
$("#user_select_div").attr('class', 'col-xl-4');
$("#buttons_div").attr('class', 'col-sm-7 col-xl-6');
let note_list_obj = $("#note_list");
if (buttons.length > 0 && note_list_obj.text().length > 0) {
$("#consos_list").html(note_list_obj.html());
note_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"consos_list"));
});
}
});
let single_conso_obj = $("#single_conso");
single_conso_obj.click(function() {
$("#consos_list_div").hide();
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
$("#note_infos_div").attr('class', 'col-xl-5');
$("#user_select_div").attr('class', 'col-xl-7');
$("#buttons_div").attr('class', 'col-sm-7 col-md-8');
let consos_list_obj = $("#consos_list");
if (buttons.length > 0) {
if (notes_display.length === 0 && consos_list_obj.text().length > 0) {
$("#note_list").html(consos_list_obj.html());
consos_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"note_list"));
});
}
else {
buttons.length = 0;
consos_list_obj.html("");
}
}
});
// Ensure we begin in single consumption. Removing these lines may cause problems when reloading.
single_conso_obj.prop('checked', 'true');
double_conso_obj.removeAttr('checked');
$("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary');
$("#consos_list_div").hide();
$("#consume_all").click(consumeAll);
});
notes = [];
notes_display = [];
buttons = [];
// When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() {
if (buttons.length > 0 && $("#single_conso").is(":checked")) {
consumeAll();
return false;
}
return true;
});
/**
* Add a transaction from a button.
* @param dest Where the money goes
* @param amount The price of the item
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category_id The category identifier
* @param category_name The category name
* @param template_id The identifier of the button
* @param template_name The name of the button
*/
function addConso(dest, amount, type, category_id, category_name, template_id, template_name) {
var button = null;
buttons.forEach(function(b) {
if (b.id === template_id) {
b.quantity += 1;
button = b;
}
});
if (button == null) {
button = {
id: template_id,
name: template_name,
dest: dest,
quantity: 1,
amount: amount,
type: type,
category_id: category_id,
category_name: category_name
};
buttons.push(button);
}
let dc_obj = $("#double_conso");
if (dc_obj.is(":checked") || notes_display.length === 0) {
let list = dc_obj.is(":checked") ? "consos_list" : "note_list";
let html = "";
buttons.forEach(function(button) {
html += li("conso_button_" + button.id, button.name
+ "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>");
});
$("#" + list).html(html);
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, list));
});
}
else
consumeAll();
}
/**
* Reset the page as its initial state.
*/
function reset() {
notes_display.length = 0;
notes.length = 0;
buttons.length = 0;
$("#note_list").html("");
$("#alias_matched").html("");
$("#consos_list").html("");
displayNote(null, "");
refreshHistory();
refreshBalance();
}
/**
* Apply all transactions: all notes in `notes` buy each item in `buttons`
*/
function consumeAll() {
notes_display.forEach(function(note_display) {
buttons.forEach(function(button) {
consume(note_display.id, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
});
});
}
/**
* Create a new transaction from a button through the API.
* @param source The note that paid the item (type: int)
* @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int)
* @param amount The price of one item, in cents (type: int)
* @param reason The transaction details (type: str)
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category The category id of the button (type: int)
* @param template The button id (type: int)
*/
function consume(source, dest, quantity, amount, reason, type, category, template) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": type,
"resourcetype": "TemplateTransaction",
"source": source,
"destination": dest,
"category": category,
"template": template
}, reset).fail(function (e) {
reset();
addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger");
});
}

157
static/js/transfer.js Normal file
View File

@ -0,0 +1,157 @@
sources = [];
sources_notes_display = [];
dests = [];
dests_notes_display = [];
function refreshHistory() {
$("#history").load("/note/transfer/ #history");
}
function reset() {
sources_notes_display.length = 0;
sources.length = 0;
dests_notes_display.length = 0;
dests.length = 0;
$("#source_note_list").html("");
$("#dest_note_list").html("");
$("#source_alias_matched").html("");
$("#dest_alias_matched").html("");
$("#amount").val("");
$("#reason").val("");
$("#last_name").val("");
$("#first_name").val("");
$("#bank").val("");
refreshBalance();
refreshHistory();
}
$(document).ready(function() {
autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display,
"source_alias", "source_note", "user_note", "profile_pic");
autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display,
"dest_alias", "dest_note", "user_note", "profile_pic", function() {
let last = dests_notes_display[dests_notes_display.length - 1];
dests_notes_display.length = 0;
dests_notes_display.push(last);
last.quantity = 1;
$.getJSON("/api/user/" + last.note.user + "/", function(user) {
$("#last_name").val(user.last_name);
$("#first_name").val(user.first_name);
});
return true;
});
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
$("#type_gift").prop('checked', 'true');
$("#type_transfer").removeAttr('checked');
$("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
});
$("#transfer").click(function() {
if ($("#type_gift").is(':checked')) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": user_id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
}
else if ($("#type_transfer").is(':checked')) {
sources_notes_display.forEach(function (source) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
});
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note = dests_notes_display[0].id;
let given_reason = $("#reason").val();
let source, dest, reason;
if ($("#type_credit").is(':checked')) {
source = special_note;
dest = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
source = user_note;
dest = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": 100 * $("#amount").val(),
"reason": reason,
"valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source,
"destination": dest,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}, function () {
addMsg("Le crédit/retrait a bien été effectué !", "success");
reset();
}).fail(function (err) {
addMsg("Le crédit/transfert a échoué : " + err.responseText, "danger");
reset();
});
}
});

View File

@ -1,4 +1,4 @@
{% load static i18n pretty_money static %} {% load static i18n pretty_money static getenv %}
{% 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">
@ -67,23 +75,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
<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"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> Consos</a> <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li> </li>
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> Clubs</a> <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
</li> </li>
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> Activités</a> <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li> </li>
<li class="nav-item active"> <li class="nav-item active">
<a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> Bouton</a> <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
</li> </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">
@ -112,6 +124,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 %}
@ -125,7 +138,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 &mdash; NoteKfet2020 &mdash;
<a href="mailto:tresorie.bde@lists.crans.org" <a href="mailto:{{ "CONTACT_EMAIL" | getenv }}"
class="text-muted">Nous contacter</a> &mdash; class="text-muted">Nous contacter</a> &mdash;
</span> </span>
{% csrf_token %} {% csrf_token %}
@ -155,6 +168,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>

View File

@ -0,0 +1,99 @@
{% load i18n %}{% load static %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
<html{% if LANGUAGE_CODE %} lang="{{LANGUAGE_CODE}}"{% endif %}>
<head>
<meta charset="utf-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% trans "Central Authentication Service" %}{% endblock %}</title>
<link href="{{settings.CAS_COMPONENT_URLS.bootstrap3_css}}" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
<![endif]-->
{% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %}
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
</head>
<body>
<div id="wrap">
<div class="container">
{% if auto_submit %}<noscript>{% endif %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h1 id="app-name">
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}" alt="cas-logo" />{% endif %}
Authentification Note Kfet 2020</h1>
</div>
</div>
{% if auto_submit %}</noscript>{% endif %}
<div class="row">
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% if auto_submit %}<noscript>{% endif %}
{% for msg in CAS_INFO_RENDER %}
<div class="alert alert-{{msg.type}}{% if msg.discardable %} alert-dismissable{% endif %}">
{% if msg.discardable %}<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="info-{{msg.name}}">&#215;</button>{% endif %}
<p>{{msg.message}}</p>
</div>
{% endfor %}
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">&#215;</button>
<p>{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}</p>
</div>
{% endif %}
{% block ante_messages %}{% endblock %}
{% for message in messages %}
<div {% spaceless %}
{% if message.level == message_levels.DEBUG %}
class="alert alert-warning"
{% elif message.level == message_levels.INFO %}
class="alert alert-info"
{% elif message.level == message_levels.SUCCESS %}
class="alert alert-success"
{% elif message.level == message_levels.WARNING %}
class="alert alert-warning"
{% else %}
class="alert alert-danger"
{% endif %}
{% endspaceless %}>
<p>{{message}}</p>
</div>
{% endfor %}
{% if auto_submit %}</noscript>{% endif %}
{% block content %}{% endblock %}
</div>
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
</div>
</div> <!-- /container -->
</div>
<div style="clear: both;"></div>
{% if settings.CAS_SHOW_POWERED %}
<div id="footer">
<p><a class="text-muted" href="https://pypi.org/project/django-cas-server/">django-cas-server powered</a></p>
</div>
{% endif %}
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
<script src="{% static "cas_server/functions.js" %}"></script>
<script type="text/javascript">
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
discard_and_remember("#alert-version", "cas-alert-version", "{{LAST_VERSION}}");
{% endif %}
{% for msg in CAS_INFO_RENDER %}
{% if msg.discardable %}
discard_and_remember("#info-{{msg.name}}", "cas-info-{{msg.name}}", "{{msg.hash}}");
{% endif %}
{% endfor %}
{% block javascript_inline %}{% endblock %}
</script>
{% block javascript %}{% endblock %}
</body>
</html>
<!--
Powered by django-cas-server version {{VERSION}}
Pypi: https://pypi.org/project/django-cas-server/
github: https://github.com/nitmir/django-cas-server
-->

View File

@ -0,0 +1,26 @@
{% load cas_server %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
{{error}}
</div>
{% endfor %}
{% for field in form %}{% if not field|is_hidden %}
<div class="form-group
{% if not form.non_field_errors %}
{% if field.errors %} has-error
{% elif form.cleaned_data %} has-success
{% endif %}
{% endif %}"
>{% spaceless %}
{% if field|is_checkbox %}
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div>
{% else %}
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
{{field}}
{% endif %}
{% for error in field.errors %}
<span class="help-block">{{error}}</span>
{% endfor %}
{% endspaceless %}</div>
{% else %}{{field}}{% endif %}{% endfor %}

View File

@ -0,0 +1,21 @@
{% extends "cas_server/base.html" %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div>
<form class="form-signin" method="get" action="logout">
<div class="checkbox">
<label>
<input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %}
</label>
</div>
{% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %}
<div class="checkbox">
<label>
<input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %}
</label>
</div>
{% endif %}
<button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "cas_server/base.html" %}
{% load i18n %}
{% block ante_messages %}
{% if auto_submit %}<noscript>{% endif %}
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
{% if auto_submit %}</noscript>{% endif %}
{% endblock %}
{% block content %}
<div class="alert alert-warning">
{% trans "If you don't have any Note Kfet account, please follow <a href='/accounts/signup'>this link to sign up</a>." %}
</div>
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
{% csrf_token %}
{% include "cas_server/form.html" %}
{% if auto_submit %}<noscript>{% endif %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
{% if auto_submit %}</noscript>{% endif %}
</form>
{% endblock %}
{% block javascript_inline %}
jQuery(function( $ ){
$("#id_warn").click(function(e){
if($("#id_warn").is(':checked')){
createCookie("warn", "on", 10 * 365);
} else {
eraseCookie("warn");
}
});
});{% if auto_submit %}
document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "cas_server/base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{{logout_msg}}</div>
{% endblock %}

View File

@ -0,0 +1,5 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:proxySuccess>
<cas:proxyTicket>{{ticket}}</cas:proxyTicket>
</cas:proxySuccess>
</cas:serviceResponse>

View File

@ -0,0 +1,59 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"
IssueInstant="{{ IssueInstant }}"
MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}"
ResponseID="{{ ResponseID }}">
<Status>
<StatusCode Value="samlp:Success">
</StatusCode>
</Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="{{ResponseID}}"
IssueInstant="{{IssueInstant}}" Issuer="localhost" MajorVersion="1"
MinorVersion="1">
<Conditions NotBefore="{{IssueInstant}}" NotOnOrAfter="{{expireInstant}}">
<AudienceRestrictionCondition>
<Audience>
{{Recipient}}
</Audience>
</AudienceRestrictionCondition>
</Conditions>
<AttributeStatement>
<Subject>
<NameIdentifier>{{username}}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
<Attribute AttributeName="authenticationDate" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{auth_date}}</AttributeValue>
</Attribute>
<Attribute AttributeName="longTermAuthenticationRequestTokenUsed" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>false</AttributeValue>{# we do not support long-term (Remember-Me) auth #}
</Attribute>
<Attribute AttributeName="isFromNewLogin" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{is_new_login}}</AttributeValue>
</Attribute>
{% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>{{value}}</AttributeValue>
</Attribute>
{% endfor %} </AttributeStatement>
<AuthenticationStatement AuthenticationInstant="{{IssueInstant}}"
AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password">
<Subject>
<NameIdentifier>{{username}}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
</AuthenticationStatement>
</Assertion>
</Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View File

@ -0,0 +1,14 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"
IssueInstant="{{ IssueInstant }}"
MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}"
ResponseID="{{ ResponseID }}">
<Status>
<StatusCode Value="samlp:{{code}}">{{msg}}</StatusCode>
</Status>
</Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View File

@ -0,0 +1,19 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationSuccess>
<cas:user>{{username}}</cas:user>
<cas:attributes>
<cas:authenticationDate>{{auth_date}}</cas:authenticationDate>
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>{# we do not support long-term (Remember-Me) auth #}
<cas:isFromNewLogin>{{is_new_login}}</cas:isFromNewLogin>
{% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}>
{% endfor %} </cas:attributes>
<cas:attribute name="authenticationDate" value="{{auth_date}}"/>
<cas:attribute name="longTermAuthenticationRequestTokenUsed" value="false"/>
<cas:attribute name="isFromNewLogin" value="{{is_new_login}}"/>
{% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/>
{% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket>
{% endif %}{% if proxies %} <cas:proxies>
{% for proxy in proxies %} <cas:proxy>{{proxy}}</cas:proxy>
{% endfor %} </cas:proxies>
{% endif %} </cas:authenticationSuccess>
</cas:serviceResponse>

View File

@ -0,0 +1,3 @@
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
<cas:authenticationFailure code="{{code}}">{{msg}}</cas:authenticationFailure>
</cas:serviceResponse>

View File

@ -0,0 +1,11 @@
{% extends "cas_server/base.html" %}
{% load static %}
{% load i18n %}
{% block content %}
<form class="form-signin" method="post">
{% csrf_token %}
{% include "cas_server/form.html" %}
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% load crispy_forms_tags %}
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}

View 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>

View File

@ -0,0 +1 @@
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -3,35 +3,188 @@
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 %}
{% 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> <label for="type_credit" class="btn btn-sm btn-outline-primary">
{{ field.label_tag }} <input type="radio" name="transaction_type" id="type_credit">
{% if field.is_readonly %} {% trans "Credit" %}
<div class="readonly">{{ field.contents }}</div> </label>
{% else %} <label type="type_debit" class="btn btn-sm btn-outline-primary">
{{ field }} <input type="radio" name="transaction_type" id="type_debit">
{% endif %} {% trans "Debit" %}
{% if field.field.help_text %} </label>
<div class="help">{{ field.field.help_text|safe }}</div> </div>
{% endif %} </div>
</div>
<div class="row">
<div class="col-md-4" id="emitters_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Select emitters" %}
</p>
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="source_note" />
<ul class="list-group list-group-flush" id="source_alias_matched">
</ul>
</div>
</div>
</div>
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
<span id="user_note"></span>
</div>
</div>
</div>
<div class="col-md-4" id="external_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "External payment" %}
</p>
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<div class="form-row">
<div class="col-md-12">
<label for="credit_type">{% trans "Transfer type" %} :</label>
<select id="credit_type" class="custom-select">
{% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="last_name">{% trans "Name" %} :</label>
<input type="text" id="last_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="first_name">{% trans "First name" %} :</label>
<input type="text" id="first_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="bank">{% trans "Bank" %} :</label>
<input type="text" id="bank" class="form-control" />
</div>
</div> </div>
</div> </div>
{% endfor %} </div>
</fieldset> </div>
<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 %}

View File

@ -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}}

View File

@ -15,7 +15,7 @@
<td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td> <td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td>
<td>{{ object.destination }}</td> <td>{{ object.destination }}</td>
<td>{{ object.amount | pretty_money }}</td> <td>{{ object.amount | pretty_money }}</td>
<td>{{ object.template_type }}</td> <td>{{ object.category }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -17,6 +17,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
</p> </p>
{% endif %} {% endif %}
<div class="alert alert-info">
Vous pouvez aussi vous connecter via l'authentification centralisée <a href="{% url 'cas_login' %}">en suivant ce lien.</a>
</div>
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} <form 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">

12
tox.ini
View File

@ -9,7 +9,10 @@ skipsdist = True
setenv = setenv =
PYTHONWARNINGS = all PYTHONWARNINGS = all
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements/base.txt
-r{toxinidir}/requirements/api.txt
-r{toxinidir}/requirements/cas.txt
-r{toxinidir}/requirements/production.txt
coverage coverage
commands = commands =
./manage.py makemigrations ./manage.py makemigrations
@ -18,7 +21,10 @@ commands =
[testenv:linters] [testenv:linters]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements/base.txt
-r{toxinidir}/requirements/api.txt
-r{toxinidir}/requirements/cas.txt
-r{toxinidir}/requirements/production.txt
flake8 flake8
flake8-colors flake8-colors
flake8-import-order flake8-import-order
@ -26,7 +32,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