mirror of https://gitlab.crans.org/bde/nk20
Merge remote-tracking branch 'origin/master' into activity
# Conflicts: # note_kfet/urls.py # templates/base.html
This commit is contained in:
commit
841d56d5b3
|
@ -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"
|
|
@ -37,7 +37,7 @@ coverage
|
|||
# Local data
|
||||
secrets.py
|
||||
*.log
|
||||
|
||||
media/
|
||||
# Virtualenv
|
||||
env/
|
||||
venv/
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "apps/scripts"]
|
||||
path = apps/scripts
|
||||
url = git@gitlab.crans.org:bde/nk20-scripts.git
|
12
Dockerfile
12
Dockerfile
|
@ -9,10 +9,18 @@ RUN apt update && \
|
|||
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt /code/
|
||||
RUN pip install -r requirements.txt
|
||||
# Install LaTeX requirements
|
||||
RUN apt update && \
|
||||
apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . /code/
|
||||
|
||||
# Comment what is not needed
|
||||
RUN pip install -r requirements/base.txt
|
||||
RUN pip install -r requirements/api.txt
|
||||
RUN pip install -r requirements/cas.txt
|
||||
RUN pip install -r requirements/production.txt
|
||||
|
||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||
EXPOSE 8000
|
||||
|
|
70
README.md
70
README.md
|
@ -6,13 +6,17 @@
|
|||
|
||||
## Installation sur un serveur
|
||||
|
||||
On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout nu ou bien configuré.
|
||||
On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré.
|
||||
|
||||
1. Paquets nécessaires
|
||||
|
||||
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi
|
||||
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
|
||||
|
||||
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante :
|
||||
|
||||
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
|
||||
|
||||
2. Clonage du dépot
|
||||
|
||||
on se met au bon endroit :
|
||||
|
@ -31,7 +35,8 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
|
|||
|
||||
$ python3 -m venv env
|
||||
$ source env/bin/activate
|
||||
(env)$ pip3 install -r requirements.txt
|
||||
(env)$ pip3 install -r requirements/base.txt
|
||||
(env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite un base postgres
|
||||
(env)$ deactivate
|
||||
|
||||
4. uwsgi et Nginx
|
||||
|
@ -40,14 +45,13 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
|
|||
|
||||
$ cp nginx_note.conf_example nginx_note.conf
|
||||
|
||||
***Modifier le fichier pour être en accord avec le reste de votre config***
|
||||
***Modifier le fichier pour être en accord avec le reste de votre config***
|
||||
|
||||
On utilise uwsgi et Nginx pour gérer le coté serveu :
|
||||
On utilise uwsgi et Nginx pour gérer le coté serveur :
|
||||
|
||||
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
|
||||
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
|
||||
|
||||
|
||||
Si l'on a un emperor (plusieurs instance uwsgi):
|
||||
Si l'on a un emperor (plusieurs instance uwsgi):
|
||||
|
||||
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
|
||||
|
||||
|
@ -85,7 +89,7 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
|
|||
postgres=# CREATE DATABASE note_db OWNER note;
|
||||
CREATE DATABASE
|
||||
|
||||
Si tout va bien:
|
||||
Si tout va bien :
|
||||
|
||||
postgres=#\list
|
||||
List of databases
|
||||
|
@ -96,22 +100,29 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
|
|||
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres
|
||||
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres
|
||||
(4 rows)
|
||||
|
||||
Dans un fichier `.env` à la racine du projet on renseigne des secrets:
|
||||
|
||||
DJANGO_APP_STAGE='prod'
|
||||
DJANGO_DB_PASSWORD='le_mot_de_passe_de_la_bdd'
|
||||
DJANGO_SECRET_KEY='une_secret_key_longue_et_compliquee'
|
||||
ALLOWED_HOSTS='le_ndd_de_votre_instance'
|
||||
|
||||
|
||||
6. Variable d'environnement et Migrations
|
||||
|
||||
On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet
|
||||
et on renseigne des secrets et des paramètres :
|
||||
|
||||
DJANGO_APP_STAGE="dev" # ou "prod"
|
||||
DJANGO_DEV_STORE_METHOD="sqllite" # ou "postgres"
|
||||
DJANGO_DB_HOST="localhost"
|
||||
DJANGO_DB_NAME="note_db"
|
||||
DJANGO_DB_USER="note"
|
||||
DJANGO_DB_PASSWORD="CHANGE_ME"
|
||||
DJANGO_DB_PORT=""
|
||||
DJANGO_SECRET_KEY="CHANGE_ME"
|
||||
DJANGO_SETTINGS_MODULE="note_kfet.settings"
|
||||
DOMAIN="localhost" # note.example.com
|
||||
CONTACT_EMAIL="tresorerie.bde@localhost"
|
||||
NOTE_URL="localhost" # serveur cas note.example.com si auto-hébergé.
|
||||
|
||||
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
|
||||
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
|
||||
|
||||
$ source /env/bin/activate
|
||||
(env)$ ./manage.py check # pas de bétise qui traine
|
||||
(env)$ ./manage.py check # pas de bêtise qui traine
|
||||
(env)$ ./manage.py makemigrations
|
||||
(env)$ ./manage.py migrate
|
||||
|
||||
|
@ -126,17 +137,21 @@ Il est possible de travailler sur une instance Docker.
|
|||
|
||||
$ git clone git@gitlab.crans.org:bde/nk20.git
|
||||
|
||||
2. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
|
||||
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
|
||||
et mettez à jour vos variables d'environnement
|
||||
|
||||
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
|
||||
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
|
||||
|
||||
nk20:
|
||||
build: /chemin/vers/nk20
|
||||
volumes:
|
||||
- /chemin/vers/nk20:/code/
|
||||
env_file: /chemin/vers/nk20/.env
|
||||
restart: always
|
||||
labels:
|
||||
- traefik.domain=ndd.exemple.com
|
||||
- traefik.frontend.rule=Host:ndd.exemple.com
|
||||
- traefik.domain=ndd.example.com
|
||||
- traefik.frontend.rule=Host:ndd.example.com
|
||||
- traefik.port=8000
|
||||
|
||||
3. Enjoy :
|
||||
|
@ -157,19 +172,22 @@ un serveur de développement par exemple sur son ordinateur.
|
|||
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate
|
||||
(env)$ pip install -r requirements.txt
|
||||
(env)$ pip install -r requirements/base.txt
|
||||
|
||||
3. Migrations et chargement des données initiales :
|
||||
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
|
||||
ce qu'il faut
|
||||
|
||||
4. Migrations et chargement des données initiales :
|
||||
|
||||
(env)$ ./manage.py makemigrations
|
||||
(env)$ ./manage.py migrate
|
||||
(env)$ ./manage.py loaddata initial
|
||||
|
||||
4. Créer un super-utilisateur :
|
||||
5. Créer un super-utilisateur :
|
||||
|
||||
(env)$ ./manage.py createsuperuser
|
||||
|
||||
5. Enjoy :
|
||||
6. Enjoy :
|
||||
|
||||
(env)$ ./manage.py runserver 0.0.0.0:8000
|
||||
|
||||
|
@ -184,4 +202,4 @@ Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
|
|||
## Documentation
|
||||
|
||||
La documentation est générée par django et son module admindocs.
|
||||
**Commenter votre code !**
|
||||
**Commentez votre code !**
|
||||
|
|
|
@ -11,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin):
|
|||
Admin customisation for Activity
|
||||
"""
|
||||
list_display = ('name', 'activity_type', 'organizer')
|
||||
list_filter = ('activity_type', )
|
||||
list_filter = ('activity_type',)
|
||||
search_fields = ['name', 'organizer__name']
|
||||
|
||||
# Organize activities by start date
|
||||
|
|
|
@ -11,6 +11,7 @@ class ActivityTypeSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Activity types.
|
||||
The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ActivityType
|
||||
fields = '__all__'
|
||||
|
@ -21,6 +22,7 @@ class ActivitySerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Activities.
|
||||
The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = '__all__'
|
||||
|
@ -31,6 +33,7 @@ class GuestSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Guests.
|
||||
The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Guest
|
||||
fields = '__all__'
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import viewsets
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from ..models import ActivityType, Activity, Guest
|
||||
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
|
||||
from ..models import ActivityType, Activity, Guest
|
||||
|
||||
|
||||
class ActivityTypeViewSet(viewsets.ModelViewSet):
|
||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -15,9 +17,11 @@ class ActivityTypeViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = ActivityType.objects.all()
|
||||
serializer_class = ActivityTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'can_invite', ]
|
||||
|
||||
|
||||
class ActivityViewSet(viewsets.ModelViewSet):
|
||||
class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -25,9 +29,11 @@ class ActivityViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Activity.objects.all()
|
||||
serializer_class = ActivitySerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'description', 'activity_type', ]
|
||||
|
||||
|
||||
class GuestViewSet(viewsets.ModelViewSet):
|
||||
class GuestViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -35,3 +41,5 @@ class GuestViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Guest.objects.all()
|
||||
serializer_class = GuestSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'api.apps.APIConfig'
|
|
@ -0,0 +1,10 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class APIConfig(AppConfig):
|
||||
name = 'api'
|
||||
verbose_name = _('API')
|
|
@ -3,10 +3,18 @@
|
|||
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import routers, serializers
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
from activity.api.urls import register_activity_urls
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from member.api.urls import register_members_urls
|
||||
from note.api.urls import register_note_urls
|
||||
from treasury.api.urls import register_treasury_urls
|
||||
from logs.api.urls import register_logs_urls
|
||||
from permission.api.urls import register_permission_urls
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
@ -14,6 +22,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = (
|
||||
|
@ -23,7 +32,18 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class UserViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -31,15 +51,33 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
||||
search_fields = ['$username', '$first_name', '$last_name', ]
|
||||
|
||||
|
||||
# This ViewSet is the only one that is accessible from all authenticated users!
|
||||
class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/users/
|
||||
"""
|
||||
queryset = ContentType.objects.all()
|
||||
serializer_class = ContentTypeSerializer
|
||||
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
# Register each app API router and user viewset
|
||||
router = routers.DefaultRouter()
|
||||
router.register('models', ContentTypeViewSet)
|
||||
router.register('user', UserViewSet)
|
||||
register_members_urls(router, 'members')
|
||||
register_activity_urls(router, 'activity')
|
||||
register_note_urls(router, 'note')
|
||||
register_treasury_urls(router, 'treasury')
|
||||
register_permission_urls(router, 'permission')
|
||||
register_logs_urls(router, 'logs')
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from permission.backends import PermissionBackend
|
||||
from rest_framework import viewsets
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
|
||||
|
||||
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
user = get_current_authenticated_user()
|
||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
||||
|
||||
|
||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
user = get_current_authenticated_user()
|
||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,23 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
|
||||
from .serializers import ChangelogSerializer
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
queryset = Changelog.objects.all()
|
||||
serializer_class = ChangelogSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
||||
ordering_fields = ['timestamp', ]
|
||||
ordering = ['-timestamp', ]
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import pre_save, post_save, post_delete
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class LogsConfig(AppConfig):
|
||||
name = 'logs'
|
||||
verbose_name = _('Logs')
|
||||
|
||||
def ready(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals
|
||||
pre_save.connect(signals.pre_save_object)
|
||||
post_save.connect(signals.save_object)
|
||||
post_delete.connect(signals.delete_object)
|
|
@ -0,0 +1,77 @@
|
|||
# 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.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Changelog(models.Model):
|
||||
"""
|
||||
Store each modification in the database (except sessions and logging),
|
||||
including creating, editing and deleting models.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
verbose_name=_('user'),
|
||||
)
|
||||
|
||||
ip = models.GenericIPAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("IP Address")
|
||||
)
|
||||
|
||||
model = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('model'),
|
||||
)
|
||||
|
||||
instance_pk = models.CharField(
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('identifier'),
|
||||
)
|
||||
|
||||
previous = models.TextField(
|
||||
null=True,
|
||||
verbose_name=_('previous data'),
|
||||
)
|
||||
|
||||
data = models.TextField(
|
||||
null=True,
|
||||
verbose_name=_('new data'),
|
||||
)
|
||||
|
||||
action = models.CharField( # create, edit or delete
|
||||
max_length=16,
|
||||
null=False,
|
||||
blank=False,
|
||||
choices=[
|
||||
('create', _('create')),
|
||||
('edit', _('edit')),
|
||||
('delete', _('delete')),
|
||||
],
|
||||
default='edit',
|
||||
verbose_name=_('action'),
|
||||
)
|
||||
|
||||
timestamp = models.DateTimeField(
|
||||
null=False,
|
||||
blank=False,
|
||||
auto_now_add=True,
|
||||
name='timestamp',
|
||||
verbose_name=_('timestamp'),
|
||||
)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
|
@ -0,0 +1,140 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from note.models import NoteUser, Alias
|
||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
|
||||
|
||||
from .models import Changelog
|
||||
|
||||
import getpass
|
||||
|
||||
|
||||
# Ces modèles ne nécessitent pas de logs
|
||||
EXCLUDED = [
|
||||
'admin.logentry',
|
||||
'authtoken.token',
|
||||
'cas_server.proxygrantingticket',
|
||||
'cas_server.proxyticket',
|
||||
'cas_server.serviceticket',
|
||||
'cas_server.user',
|
||||
'cas_server.userattributes',
|
||||
'contenttypes.contenttype',
|
||||
'logs.changelog', # Never remove this line
|
||||
'migrations.migration',
|
||||
'note.note' # We only store the subclasses
|
||||
'note.transaction',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
||||
def pre_save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get saved, we get the previous instance that is currently in the database
|
||||
"""
|
||||
qs = sender.objects.filter(pk=instance.pk).all()
|
||||
if qs.exists():
|
||||
instance._previous = qs.get()
|
||||
else:
|
||||
instance._previous = None
|
||||
|
||||
|
||||
def save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||
in order to store each modification made
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
|
||||
if user is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
ip = "127.0.0.1"
|
||||
username = Alias.normalize(getpass.getuser())
|
||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||
# if not note.exists():
|
||||
# print("WARNING: A model attempted to be saved in the DB, but the actor is unknown: " + username)
|
||||
# else:
|
||||
if note.exists():
|
||||
user = note.get().user
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
if user is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||
# On n'enregistre pas les connexions
|
||||
if instance.last_login != previous.last_login:
|
||||
return
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||
class CustomSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = instance.__class__
|
||||
fields = '__all__'
|
||||
|
||||
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None
|
||||
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
|
||||
|
||||
if previous_json == instance_json:
|
||||
# Pas de log s'il n'y a pas de modification
|
||||
return
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=previous_json,
|
||||
data=instance_json,
|
||||
action=("edit" if previous else "create")
|
||||
).save()
|
||||
|
||||
|
||||
def delete_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is deleted, an entry in the table `Changelog` is added in the database
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
# 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")
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=instance_json,
|
||||
data=None,
|
||||
action="delete"
|
||||
).save()
|
|
@ -18,9 +18,9 @@ class ProfileInline(admin.StackedInline):
|
|||
|
||||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
inlines = (ProfileInline, )
|
||||
inlines = (ProfileInline,)
|
||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||
list_select_related = ('profile', )
|
||||
list_select_related = ('profile',)
|
||||
form = ProfileForm
|
||||
|
||||
def get_inline_instances(self, request, obj=None):
|
||||
|
|
|
@ -11,9 +11,11 @@ class ProfileSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Profiles.
|
||||
The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
read_only_fields = ('user', )
|
||||
|
||||
|
||||
class ClubSerializer(serializers.ModelSerializer):
|
||||
|
@ -21,6 +23,7 @@ class ClubSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Clubs.
|
||||
The djangorestframework plugin will analyse the model `Club` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = '__all__'
|
||||
|
@ -31,6 +34,7 @@ class RoleSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Roles.
|
||||
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
@ -41,6 +45,7 @@ class MembershipSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Memberships.
|
||||
The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = '__all__'
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
|
||||
|
||||
class ProfileViewSet(viewsets.ModelViewSet):
|
||||
class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -17,7 +18,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ProfileSerializer
|
||||
|
||||
|
||||
class ClubViewSet(viewsets.ModelViewSet):
|
||||
class ClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -25,9 +26,11 @@ class ClubViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Club.objects.all()
|
||||
serializer_class = ClubSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class RoleViewSet(viewsets.ModelViewSet):
|
||||
class RoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -35,9 +38,11 @@ class RoleViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class MembershipViewSet(viewsets.ModelViewSet):
|
||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
||||
|
|
|
@ -2,9 +2,22 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .signals import save_user_profile
|
||||
|
||||
|
||||
class MemberConfig(AppConfig):
|
||||
name = 'member'
|
||||
verbose_name = _('member')
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Define app internal signals to interact with other apps
|
||||
"""
|
||||
post_save.connect(
|
||||
save_user_profile,
|
||||
sender=settings.AUTH_USER_MODEL,
|
||||
)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters import FilterSet, CharFilter
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import CharField
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import CharField
|
||||
from django_filters import FilterSet, CharFilter
|
||||
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"fields": {
|
||||
"name": "BDE",
|
||||
"email": "tresorerie.bde@example.com",
|
||||
"membership_fee": 5,
|
||||
"membership_fee": 500,
|
||||
"membership_duration": "396 00:00:00",
|
||||
"membership_start": "213 00:00:00",
|
||||
"membership_end": "273 00:00:00"
|
||||
|
@ -17,7 +17,7 @@
|
|||
"fields": {
|
||||
"name": "Kfet",
|
||||
"email": "tresorerie.bde@example.com",
|
||||
"membership_fee": 35,
|
||||
"membership_fee": 3500,
|
||||
"membership_duration": "396 00:00:00",
|
||||
"membership_start": "213 00:00:00",
|
||||
"membership_end": "273 00:00:00"
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from crispy_forms.bootstrap import Div
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout
|
||||
from dal import autocomplete
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from django.contrib.auth.models import User
|
||||
from permission.models import PermissionMask
|
||||
|
||||
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 CustomAuthenticationForm(AuthenticationForm):
|
||||
permission_mask = forms.ModelChoiceField(
|
||||
label="Masque de permissions",
|
||||
queryset=PermissionMask.objects.order_by("rank"),
|
||||
empty_label=None,
|
||||
)
|
||||
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
def __init__(self,*args,**kwargs):
|
||||
super().__init__(*args,**kwargs)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['username'].widget.attrs.pop("autofocus", None)
|
||||
self.fields['first_name'].widget.attrs.update({"autofocus":"autofocus"})
|
||||
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -28,6 +36,7 @@ class ProfileForm(forms.ModelForm):
|
|||
"""
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
|
@ -42,7 +51,7 @@ class ClubForm(forms.ModelForm):
|
|||
|
||||
class AddMembersForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ('', )
|
||||
fields = ('',)
|
||||
|
||||
|
||||
class MembershipForm(forms.ModelForm):
|
||||
|
@ -54,13 +63,13 @@ class MembershipForm(forms.ModelForm):
|
|||
# et récupère les noms d'utilisateur valides
|
||||
widgets = {
|
||||
'user':
|
||||
autocomplete.ModelSelect2(
|
||||
url='member:user_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Nom ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
autocomplete.ModelSelect2(
|
||||
url='member:user_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Nom ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
|
@ -46,9 +48,10 @@ class Profile(models.Model):
|
|||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profile')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('user_detail', args=(self.pk, ))
|
||||
return reverse('user_detail', args=(self.pk,))
|
||||
|
||||
|
||||
class Club(models.Model):
|
||||
|
@ -97,7 +100,7 @@ class Club(models.Model):
|
|||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('member:club_detail', args=(self.pk, ))
|
||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
|
@ -149,16 +152,13 @@ class Membership(models.Model):
|
|||
verbose_name=_('fee'),
|
||||
)
|
||||
|
||||
def valid(self):
|
||||
if self.date_end is not None:
|
||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
|
||||
else:
|
||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
|
||||
|
||||
# @receiver(post_save, sender=settings.AUTH_USER_MODEL)
|
||||
# def save_user_profile(instance, created, **_kwargs):
|
||||
# """
|
||||
# Hook to save an user profile when an user is updated
|
||||
# """
|
||||
# if created:
|
||||
# Profile.objects.create(user=instance)
|
||||
# instance.profile.save()
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
|
|
@ -1,2 +1,16 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
def save_user_profile(instance, created, raw, **_kwargs):
|
||||
"""
|
||||
Hook to create and save a profile when an user is updated if it is not registered with the signup form
|
||||
"""
|
||||
if raw:
|
||||
# When provisionning data, do not try to autocreate
|
||||
return
|
||||
|
||||
if created:
|
||||
from .models import Profile
|
||||
Profile.objects.get_or_create(user=instance)
|
||||
instance.profile.save()
|
||||
|
|
|
@ -17,6 +17,7 @@ class ClubTable(tables.Table):
|
|||
fields = ('id', 'name', 'email')
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,15 @@ urlpatterns = [
|
|||
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
|
||||
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
|
||||
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
|
||||
path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"),
|
||||
path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
|
||||
path('user/', views.UserListView.as_view(), name="user_list"),
|
||||
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
|
||||
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
|
||||
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||
path('user/<int:pk>/aliases', views.AliasView.as_view(), name="user_alias"),
|
||||
path('user/aliases/delete/<int:pk>', views.DeleteAliasView.as_view(), name="user_alias_delete"),
|
||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||
|
||||
# API for the user autocompleter
|
||||
path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"),
|
||||
]
|
||||
|
|
|
@ -1,24 +1,44 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
from dal import autocomplete
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.views import LoginView
|
||||
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.transactions import Transaction
|
||||
from note.tables import HistoryTable
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
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 .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
|
||||
CustomAuthenticationForm
|
||||
from .models import Club, Membership
|
||||
from .tables import ClubTable, UserTable
|
||||
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
form_class = CustomAuthenticationForm
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UserCreateView(CreateView):
|
||||
|
@ -40,10 +60,10 @@ class UserCreateView(CreateView):
|
|||
def form_valid(self, form):
|
||||
profile_form = ProfileForm(self.request.POST)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
user = form.save()
|
||||
profile = profile_form.save(commit=False)
|
||||
profile.user = user
|
||||
profile.save()
|
||||
user = form.save(commit=False)
|
||||
user.profile = profile_form.save(commit=False)
|
||||
user.save()
|
||||
user.profile.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
|
@ -52,30 +72,25 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
|
|||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
template_name = 'member/profile_update.html'
|
||||
context_object_name = 'user_object'
|
||||
second_form = ProfileForm
|
||||
profile_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["profile_form"] = self.second_form(
|
||||
instance=context['user_object'].profile)
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
||||
context['title'] = _("Update Profile")
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
if 'username' not in form.data:
|
||||
return form
|
||||
|
||||
new_username = form.data['username']
|
||||
|
||||
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
|
||||
note = NoteUser.objects.filter(
|
||||
alias__normalized_name=Alias.normalize(new_username))
|
||||
if note.exists() and note.get().user != self.object:
|
||||
form.add_error('username',
|
||||
_("An alias with a similar name already exists."))
|
||||
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -105,7 +120,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
|
|||
return reverse_lazy('member:user_detail',
|
||||
kwargs={'pk': kwargs['id']})
|
||||
else:
|
||||
return reverse_lazy('member:user_detail', args=(self.object.id, ))
|
||||
return reverse_lazy('member:user_detail', args=(self.object.id,))
|
||||
|
||||
|
||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
|
@ -116,11 +131,14 @@ class UserDetailView(LoginRequiredMixin, DetailView):
|
|||
context_object_name = "user_object"
|
||||
template_name = "member/profile_detail.html"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")
|
||||
context['history_list'] = HistoryTable(history_list)
|
||||
club_list = \
|
||||
Membership.objects.all().filter(user=user).only("club")
|
||||
|
@ -143,7 +161,7 @@ class UserListView(LoginRequiredMixin, SingleTableView):
|
|||
formhelper_class = UserFilterFormHelper
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset()
|
||||
qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
|
||||
self.filter = self.filter_class(self.request.GET, queryset=qs)
|
||||
self.filter.form.helper = self.formhelper_class()
|
||||
return self.filter.qs
|
||||
|
@ -154,6 +172,110 @@ class UserListView(LoginRequiredMixin, SingleTableView):
|
|||
return context
|
||||
|
||||
|
||||
class AliasView(LoginRequiredMixin, FormMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'member/profile_alias.html'
|
||||
context_object_name = 'user_object'
|
||||
form_class = AliasForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['user_object'].note
|
||||
context["aliases"] = AliasTable(note.alias_set.all())
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
alias = form.save(commit=False)
|
||||
alias.note = self.object.note
|
||||
alias.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteAliasView(LoginRequiredMixin, DeleteView):
|
||||
model = Alias
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
self.object.delete()
|
||||
except ValidationError as e:
|
||||
# TODO: pass message to redirected view.
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _("Alias successfully deleted"))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
|
||||
form_class = ImageForm
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['form'] = self.form_class(self.request.POST, self.request.FILES)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
self.object = self.get_object()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
print('is_invalid')
|
||||
print(form)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
image_field = form.cleaned_data['image']
|
||||
x = form.cleaned_data['x']
|
||||
y = form.cleaned_data['y']
|
||||
w = form.cleaned_data['width']
|
||||
h = form.cleaned_data['height']
|
||||
# image crop and resize
|
||||
image_file = io.BytesIO(image_field.read())
|
||||
# ext = image_field.name.split('.')[-1].lower()
|
||||
# TODO: support GIF format
|
||||
image = Image.open(image_file)
|
||||
image = image.crop((x, y, x + w, y + h))
|
||||
image_clean = image.resize((settings.PIC_WIDTH,
|
||||
settings.PIC_RATIO * settings.PIC_WIDTH),
|
||||
Image.ANTIALIAS)
|
||||
image_file = io.BytesIO()
|
||||
image_clean.save(image_file, "PNG")
|
||||
image_field.file = image_file
|
||||
# renaming
|
||||
filename = "{}_pic.png".format(self.object.note.pk)
|
||||
image_field.name = filename
|
||||
self.object.note.display_image = image_field
|
||||
self.object.note.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ProfilePictureUpdateView(PictureUpdateView):
|
||||
model = User
|
||||
template_name = 'member/profile_picture_update.html'
|
||||
context_object_name = 'user_object'
|
||||
|
||||
|
||||
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Affiche le jeton d'authentification, et permet de le regénérer
|
||||
|
@ -181,6 +303,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
|
|||
"""
|
||||
Auto complete users by usernames
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion.
|
||||
|
@ -190,10 +313,10 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
|
|||
if not self.request.user.is_authenticated:
|
||||
return User.objects.none()
|
||||
|
||||
qs = User.objects.all()
|
||||
qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(username__regex=self.q)
|
||||
qs = qs.filter(username__regex="^" + self.q)
|
||||
|
||||
return qs
|
||||
|
||||
|
@ -209,10 +332,11 @@ class ClubCreateView(LoginRequiredMixin, CreateView):
|
|||
"""
|
||||
model = Club
|
||||
form_class = ClubForm
|
||||
success_url = reverse_lazy('member:club_list')
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
|
||||
class ClubListView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
|
@ -221,15 +345,21 @@ class ClubListView(LoginRequiredMixin, SingleTableView):
|
|||
model = Club
|
||||
table_class = ClubTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
|
||||
|
||||
|
||||
class ClubDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Club
|
||||
context_object_name = "club"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
club = context["club"]
|
||||
club_transactions = \
|
||||
club_transactions = \
|
||||
Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))
|
||||
context['history_list'] = HistoryTable(club_transactions)
|
||||
club_member = \
|
||||
|
@ -239,11 +369,33 @@ class ClubDetailView(LoginRequiredMixin, DetailView):
|
|||
return context
|
||||
|
||||
|
||||
class ClubUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Club
|
||||
context_object_name = "club"
|
||||
form_class = ClubForm
|
||||
template_name = "member/club_form.html"
|
||||
success_url = reverse_lazy("member:club_detail")
|
||||
|
||||
|
||||
class ClubPictureUpdateView(PictureUpdateView):
|
||||
model = Club
|
||||
template_name = 'member/club_picture_update.html'
|
||||
context_object_name = 'club'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
|
||||
|
||||
|
||||
class ClubAddMemberView(LoginRequiredMixin, CreateView):
|
||||
model = Membership
|
||||
form_class = MembershipForm
|
||||
template_name = 'member/add_members.html'
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")
|
||||
| PermissionBackend.filter_queryset(self.request.user, Membership,
|
||||
"change"))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['formset'] = MemberFormSet()
|
||||
|
|
|
@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
|
|||
|
||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
||||
TemplateTransaction, MembershipTransaction
|
||||
RecurrentTransaction, MembershipTransaction
|
||||
|
||||
|
||||
class AliasInlines(admin.TabularInline):
|
||||
|
@ -47,11 +47,11 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
|
|||
"""
|
||||
Child for a club note, see NoteAdmin
|
||||
"""
|
||||
inlines = (AliasInlines, )
|
||||
inlines = (AliasInlines,)
|
||||
|
||||
# We can't change club after creation or the balance
|
||||
readonly_fields = ('club', 'balance')
|
||||
search_fields = ('club', )
|
||||
search_fields = ('club',)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""
|
||||
|
@ -71,7 +71,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin):
|
|||
"""
|
||||
Child for a special note, see NoteAdmin
|
||||
"""
|
||||
readonly_fields = ('balance', )
|
||||
readonly_fields = ('balance',)
|
||||
|
||||
|
||||
@admin.register(NoteUser)
|
||||
|
@ -79,7 +79,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
|||
"""
|
||||
Child for an user note, see NoteAdmin
|
||||
"""
|
||||
inlines = (AliasInlines, )
|
||||
inlines = (AliasInlines,)
|
||||
|
||||
# We can't change user after creation or the balance
|
||||
readonly_fields = ('user', 'balance')
|
||||
|
@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
|
|||
"""
|
||||
Admin customisation for Transaction
|
||||
"""
|
||||
child_models = (TemplateTransaction, MembershipTransaction)
|
||||
child_models = (RecurrentTransaction, MembershipTransaction)
|
||||
list_display = ('created_at', 'poly_source', 'poly_destination',
|
||||
'quantity', 'amount', 'valid')
|
||||
list_filter = ('valid',)
|
||||
|
@ -133,7 +133,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
|
|||
Else the amount of money would not be transferred
|
||||
"""
|
||||
if obj: # user is editing an existing object
|
||||
return 'created_at', 'source', 'destination', 'quantity',\
|
||||
return 'created_at', 'source', 'destination', 'quantity', \
|
||||
'amount'
|
||||
return []
|
||||
|
||||
|
@ -143,9 +143,9 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
Admin customisation for TransactionTemplate
|
||||
"""
|
||||
list_display = ('name', 'poly_destination', 'amount', 'category', 'display', )
|
||||
list_display = ('name', 'poly_destination', 'amount', 'category', 'display',)
|
||||
list_filter = ('category', 'display')
|
||||
autocomplete_fields = ('destination', )
|
||||
autocomplete_fields = ('destination',)
|
||||
|
||||
def poly_destination(self, obj):
|
||||
"""
|
||||
|
@ -161,5 +161,5 @@ class TemplateCategoryAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
Admin customisation for TransactionTemplate
|
||||
"""
|
||||
list_display = ('name', )
|
||||
list_filter = ('name', )
|
||||
list_display = ('name',)
|
||||
list_filter = ('name',)
|
||||
|
|
|
@ -5,7 +5,8 @@ from rest_framework import serializers
|
|||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||
RecurrentTransaction, SpecialTransaction
|
||||
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
|
@ -13,15 +14,11 @@ class NoteSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Notes.
|
||||
The djangorestframework plugin will analyse the model `Note` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {
|
||||
'view_name': 'project-detail',
|
||||
'lookup_field': 'pk'
|
||||
},
|
||||
}
|
||||
read_only_fields = [f.name for f in model._meta.get_fields()] # Notes are read-only protected
|
||||
|
||||
|
||||
class NoteClubSerializer(serializers.ModelSerializer):
|
||||
|
@ -29,9 +26,15 @@ class NoteClubSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Club's notes.
|
||||
The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
|
||||
"""
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NoteClub
|
||||
fields = '__all__'
|
||||
read_only_fields = ('note', 'club', )
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class NoteSpecialSerializer(serializers.ModelSerializer):
|
||||
|
@ -39,9 +42,15 @@ class NoteSpecialSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for special notes.
|
||||
The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
|
||||
"""
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NoteSpecial
|
||||
fields = '__all__'
|
||||
read_only_fields = ('note', )
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class NoteUserSerializer(serializers.ModelSerializer):
|
||||
|
@ -49,9 +58,15 @@ class NoteUserSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for User's notes.
|
||||
The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
|
||||
"""
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NoteUser
|
||||
fields = '__all__'
|
||||
read_only_fields = ('note', 'user', )
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
|
@ -59,9 +74,11 @@ class AliasSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Aliases.
|
||||
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = '__all__'
|
||||
read_only_fields = ('note', )
|
||||
|
||||
|
||||
class NotePolymorphicSerializer(PolymorphicSerializer):
|
||||
|
@ -72,12 +89,27 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
|
|||
NoteSpecial: NoteSpecialSerializer
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
|
||||
|
||||
class TemplateCategorySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transaction templates.
|
||||
The djangorestframework plugin will analyse the model `TemplateCategory` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = TemplateCategory
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TransactionTemplateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transaction templates.
|
||||
The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = TransactionTemplate
|
||||
fields = '__all__'
|
||||
|
@ -88,16 +120,52 @@ class TransactionSerializer(serializers.ModelSerializer):
|
|||
REST API Serializer for Transactions.
|
||||
The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RecurrentTransactionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transactions.
|
||||
The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RecurrentTransaction
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MembershipTransactionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Membership transactions.
|
||||
The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MembershipTransaction
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class SpecialTransactionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Special transactions.
|
||||
The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = SpecialTransaction
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TransactionPolymorphicSerializer(PolymorphicSerializer):
|
||||
model_serializer_mapping = {
|
||||
Transaction: TransactionSerializer,
|
||||
RecurrentTransaction: RecurrentTransactionSerializer,
|
||||
MembershipTransaction: MembershipTransactionSerializer,
|
||||
SpecialTransaction: SpecialTransactionSerializer,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, \
|
||||
TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
|
||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
|
||||
|
||||
|
||||
def register_note_urls(router, path):
|
||||
|
@ -12,6 +12,6 @@ def register_note_urls(router, path):
|
|||
router.register(path + '/note', NotePolymorphicViewSet)
|
||||
router.register(path + '/alias', AliasViewSet)
|
||||
|
||||
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
||||
router.register(path + '/transaction/transaction', TransactionViewSet)
|
||||
router.register(path + '/transaction/template', TransactionTemplateViewSet)
|
||||
router.register(path + '/transaction/membership', MembershipTransactionViewSet)
|
||||
|
|
|
@ -2,56 +2,18 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||
from rest_framework import viewsets
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
|
||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
|
||||
NoteUserSerializer, AliasSerializer, \
|
||||
TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
|
||||
TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||
from ..models.notes import Note, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||
|
||||
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/note/
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
serializer_class = NoteSerializer
|
||||
|
||||
|
||||
class NoteClubViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/club/
|
||||
"""
|
||||
queryset = NoteClub.objects.all()
|
||||
serializer_class = NoteClubSerializer
|
||||
|
||||
|
||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/special/
|
||||
"""
|
||||
queryset = NoteSpecial.objects.all()
|
||||
serializer_class = NoteSpecialSerializer
|
||||
|
||||
|
||||
class NoteUserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/user/
|
||||
"""
|
||||
queryset = NoteUser.objects.all()
|
||||
serializer_class = NoteUserSerializer
|
||||
|
||||
|
||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
||||
class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
|
||||
|
@ -59,36 +21,27 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Note.objects.all()
|
||||
serializer_class = NotePolymorphicSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter]
|
||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
|
||||
ordering_fields = ['alias__name', 'alias__normalized_name']
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested notes
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
queryset = super().get_queryset()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
Q(alias__name__regex=alias)
|
||||
| Q(alias__normalized_name__regex=alias.lower()))
|
||||
Q(alias__name__regex="^" + alias)
|
||||
| Q(alias__normalized_name__regex="^" + Alias.normalize(alias))
|
||||
| Q(alias__normalized_name__regex="^" + alias.lower()))
|
||||
|
||||
note_type = self.request.query_params.get("type", None)
|
||||
if note_type:
|
||||
types = str(note_type).lower()
|
||||
if "user" in types:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="noteuser")
|
||||
elif "club" in types:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="noteclub")
|
||||
elif "special" in types:
|
||||
queryset = queryset.filter(
|
||||
polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
|
||||
return queryset
|
||||
return queryset.distinct()
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
class AliasViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
|
@ -96,6 +49,9 @@ class AliasViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = Alias.objects.all()
|
||||
serializer_class = AliasSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
ordering_fields = ['name', 'normalized_name']
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
|
@ -103,34 +59,29 @@ class AliasViewSet(viewsets.ModelViewSet):
|
|||
:return: The filtered set of requested aliases
|
||||
"""
|
||||
|
||||
queryset = Alias.objects.all()
|
||||
queryset = super().get_queryset()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
|
||||
|
||||
note_id = self.request.query_params.get("note", None)
|
||||
if note_id:
|
||||
queryset = queryset.filter(id=note_id)
|
||||
|
||||
note_type = self.request.query_params.get("type", None)
|
||||
if note_type:
|
||||
types = str(note_type).lower()
|
||||
if "user" in types:
|
||||
queryset = queryset.filter(
|
||||
note__polymorphic_ctype__model="noteuser")
|
||||
elif "club" in types:
|
||||
queryset = queryset.filter(
|
||||
note__polymorphic_ctype__model="noteclub")
|
||||
elif "special" in types:
|
||||
queryset = queryset.filter(
|
||||
note__polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
Q(name__regex="^" + alias)
|
||||
| Q(normalized_name__regex="^" + Alias.normalize(alias))
|
||||
| Q(normalized_name__regex="^" + alias.lower()))
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/category/
|
||||
"""
|
||||
queryset = TemplateCategory.objects.all()
|
||||
serializer_class = TemplateCategorySerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
|
@ -139,23 +90,18 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = TransactionTemplate.objects.all()
|
||||
serializer_class = TransactionTemplateSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'amount', 'display', 'category', ]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class TransactionViewSet(viewsets.ModelViewSet):
|
||||
class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/transaction/
|
||||
"""
|
||||
queryset = Transaction.objects.all()
|
||||
serializer_class = TransactionSerializer
|
||||
|
||||
|
||||
class MembershipTransactionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/membership/
|
||||
"""
|
||||
queryset = MembershipTransaction.objects.all()
|
||||
serializer_class = MembershipTransactionSerializer
|
||||
serializer_class = TransactionPolymorphicSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$reason', ]
|
||||
|
|
|
@ -1,220 +1,244 @@
|
|||
[
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 22,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:02:48.778Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 22,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:39.546Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 22,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:43.049Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 22,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:50.996Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 21,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:09:38.615Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"polymorphic_ctype": 21,
|
||||
"balance": 0,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:16:14.753Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"special_type": "Esp\u00e8ces"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"special_type": "Carte bancaire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"special_type": "Ch\u00e8que"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"special_type": "Virement bancaire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.noteclub",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"club": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.noteclub",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"club": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Esp\u00e8ces",
|
||||
"normalized_name": "especes",
|
||||
"note": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Carte bancaire",
|
||||
"normalized_name": "cartebancaire",
|
||||
"note": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Ch\u00e8que",
|
||||
"normalized_name": "cheque",
|
||||
"note": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Virement bancaire",
|
||||
"normalized_name": "virementbancaire",
|
||||
"note": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "BDE",
|
||||
"normalized_name": "bde",
|
||||
"note": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Kfet",
|
||||
"normalized_name": "kfet",
|
||||
"note": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Soft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Pulls"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Gala"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Clubs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Bouffe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "BDA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Autre"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Alcool"
|
||||
}
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"notespecial"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:02:48.778Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"notespecial"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:39.546Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"notespecial"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:43.049Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"notespecial"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "",
|
||||
"created_at": "2020-02-20T20:06:50.996Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"noteclub"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "pic/default.png",
|
||||
"created_at": "2020-02-20T20:09:38.615Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.note",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"polymorphic_ctype": [
|
||||
"note",
|
||||
"noteclub"
|
||||
],
|
||||
"balance": 0,
|
||||
"last_negative": null,
|
||||
"is_active": true,
|
||||
"display_image": "pic/default.png",
|
||||
"created_at": "2020-02-20T20:16:14.753Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.noteclub",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"club": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.noteclub",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"club": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"special_type": "Esp\u00e8ces"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"special_type": "Carte bancaire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"special_type": "Ch\u00e8que"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.notespecial",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"special_type": "Virement bancaire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Esp\u00e8ces",
|
||||
"normalized_name": "especes",
|
||||
"note": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Carte bancaire",
|
||||
"normalized_name": "cartebancaire",
|
||||
"note": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Ch\u00e8que",
|
||||
"normalized_name": "cheque",
|
||||
"note": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Virement bancaire",
|
||||
"normalized_name": "virementbancaire",
|
||||
"note": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "BDE",
|
||||
"normalized_name": "bde",
|
||||
"note": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.alias",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Kfet",
|
||||
"normalized_name": "kfet",
|
||||
"note": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Soft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Pulls"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Gala"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Clubs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Bouffe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "BDA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Autre"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "note.templatecategory",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Alcool"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -5,7 +5,29 @@ from dal import autocomplete
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Transaction, TransactionTemplate, TemplateTransaction
|
||||
from .models import Alias
|
||||
from .models import TransactionTemplate
|
||||
|
||||
|
||||
class AliasForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = ("name",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["name"].label = False
|
||||
self.fields["name"].widget.attrs = {"placeholder": _('New Alias')}
|
||||
|
||||
|
||||
class ImageForm(forms.Form):
|
||||
image = forms.ImageField(required=False,
|
||||
label=_('select an image'),
|
||||
help_text=_('Maximal size: 2MB'))
|
||||
x = forms.FloatField(widget=forms.HiddenInput())
|
||||
y = forms.FloatField(widget=forms.HiddenInput())
|
||||
width = forms.FloatField(widget=forms.HiddenInput())
|
||||
height = forms.FloatField(widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class TransactionTemplateForm(forms.ModelForm):
|
||||
|
@ -20,92 +42,11 @@ class TransactionTemplateForm(forms.ModelForm):
|
|||
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
|
||||
widgets = {
|
||||
'destination':
|
||||
autocomplete.ModelSelect2(
|
||||
url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
super().save(commit)
|
||||
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
If the user has no right to transfer funds, then it will be the source of the transfer by default.
|
||||
Transactions between a note and the same note are not authorized.
|
||||
"""
|
||||
|
||||
cleaned_data = super().clean()
|
||||
if not "source" in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds"
|
||||
cleaned_data["source"] = self.user.note
|
||||
|
||||
if cleaned_data["source"].pk == cleaned_data["destination"].pk:
|
||||
self.add_error("destination", _("Source and destination must be different."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = (
|
||||
'source',
|
||||
'destination',
|
||||
'reason',
|
||||
'amount',
|
||||
)
|
||||
|
||||
# Voir ci-dessus
|
||||
widgets = {
|
||||
'source':
|
||||
autocomplete.ModelSelect2(
|
||||
url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
'destination':
|
||||
autocomplete.ModelSelect2(
|
||||
url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ConsoForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
button: TransactionTemplate = TransactionTemplate.objects.filter(
|
||||
name=self.data['button']).get()
|
||||
self.instance.destination = button.destination
|
||||
self.instance.amount = button.amount
|
||||
self.instance.reason = '{} ({})'.format(button.name, button.category)
|
||||
self.instance.name = button.name
|
||||
self.instance.category = button.category
|
||||
super().save(commit)
|
||||
|
||||
class Meta:
|
||||
model = TemplateTransaction
|
||||
fields = ('source', )
|
||||
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les aliases de note valides
|
||||
widgets = {
|
||||
'source':
|
||||
autocomplete.ModelSelect2(
|
||||
url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
autocomplete.ModelSelect2(
|
||||
url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||
from .transactions import MembershipTransaction, Transaction, \
|
||||
TemplateCategory, TransactionTemplate, TemplateTransaction
|
||||
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
||||
|
||||
__all__ = [
|
||||
# Notes
|
||||
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
||||
# Transactions
|
||||
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
||||
'TemplateTransaction',
|
||||
'RecurrentTransaction', 'SpecialTransaction',
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.core.validators import RegexValidator
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
"""
|
||||
Defines each note types
|
||||
"""
|
||||
|
@ -27,7 +28,7 @@ class Note(PolymorphicModel):
|
|||
help_text=_('in centimes, money credited for this instance'),
|
||||
default=0,
|
||||
)
|
||||
last_negative= models.DateTimeField(
|
||||
last_negative = models.DateTimeField(
|
||||
verbose_name=_('last negative date'),
|
||||
help_text=_('last time the balance was negative'),
|
||||
null=True,
|
||||
|
@ -43,7 +44,10 @@ class Note(PolymorphicModel):
|
|||
display_image = models.ImageField(
|
||||
verbose_name=_('display image'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
blank=False,
|
||||
null=False,
|
||||
upload_to='pic/',
|
||||
default='pic/default.png'
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
verbose_name=_('created at'),
|
||||
|
@ -95,7 +99,7 @@ class Note(PolymorphicModel):
|
|||
# Alias exists, so check if it is linked to this note
|
||||
if aliases.first().note != self:
|
||||
raise ValidationError(_('This alias is already taken.'),
|
||||
code="same_alias",)
|
||||
code="same_alias", )
|
||||
else:
|
||||
# Alias does not exist yet, so check if it can exist
|
||||
a = Alias(name=str(self))
|
||||
|
@ -205,6 +209,10 @@ class Alias(models.Model):
|
|||
class Meta:
|
||||
verbose_name = _("alias")
|
||||
verbose_name_plural = _("aliases")
|
||||
indexes = [
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['normalized_name']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -219,14 +227,6 @@ class Alias(models.Model):
|
|||
if all(not unicodedata.category(char).startswith(cat)
|
||||
for cat in {'M', 'P', 'Z', 'C'})).casefold()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Handle normalized_name
|
||||
"""
|
||||
self.normalized_name = Alias.normalize(self.name)
|
||||
if len(self.normalized_name) < 256:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
normalized_name = Alias.normalize(self.name)
|
||||
if len(normalized_name) >= 255:
|
||||
|
@ -235,11 +235,12 @@ class Alias(models.Model):
|
|||
try:
|
||||
sim_alias = Alias.objects.get(normalized_name=normalized_name)
|
||||
if self != sim_alias:
|
||||
raise ValidationError(_('An alias with a similar name already exists:'),
|
||||
code="same_alias"
|
||||
)
|
||||
raise ValidationError(_('An alias with a similar name already exists: {} ').format(sim_alias),
|
||||
code="same_alias"
|
||||
)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
self.normalized_name = normalized_name
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.name == str(self.note):
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .notes import Note, NoteClub
|
||||
from .notes import Note, NoteClub, NoteSpecial
|
||||
|
||||
"""
|
||||
Defines transactions
|
||||
|
@ -44,7 +44,7 @@ class TransactionTemplate(models.Model):
|
|||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
error_messages={'unique':_("A template with this name already exist")},
|
||||
error_messages={'unique': _("A template with this name already exist")},
|
||||
)
|
||||
destination = models.ForeignKey(
|
||||
NoteClub,
|
||||
|
@ -63,11 +63,12 @@ class TransactionTemplate(models.Model):
|
|||
max_length=31,
|
||||
)
|
||||
display = models.BooleanField(
|
||||
default = True,
|
||||
default=True,
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -75,7 +76,7 @@ class TransactionTemplate(models.Model):
|
|||
verbose_name_plural = _("transaction templates")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('note:template_update', args=(self.pk, ))
|
||||
return reverse('note:template_update', args=(self.pk,))
|
||||
|
||||
|
||||
class Transaction(PolymorphicModel):
|
||||
|
@ -106,7 +107,10 @@ class Transaction(PolymorphicModel):
|
|||
verbose_name=_('quantity'),
|
||||
default=1,
|
||||
)
|
||||
amount = models.PositiveIntegerField(verbose_name=_('amount'), )
|
||||
amount = models.PositiveIntegerField(
|
||||
verbose_name=_('amount'),
|
||||
)
|
||||
|
||||
reason = models.CharField(
|
||||
verbose_name=_('reason'),
|
||||
max_length=255,
|
||||
|
@ -119,6 +123,11 @@ class Transaction(PolymorphicModel):
|
|||
class Meta:
|
||||
verbose_name = _("transaction")
|
||||
verbose_name_plural = _("transactions")
|
||||
indexes = [
|
||||
models.Index(fields=['created_at']),
|
||||
models.Index(fields=['source']),
|
||||
models.Index(fields=['destination']),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
|
@ -127,6 +136,7 @@ class Transaction(PolymorphicModel):
|
|||
|
||||
if self.source.pk == self.destination.pk:
|
||||
# When source == destination, no money is transfered
|
||||
super().save(*args, **kwargs)
|
||||
return
|
||||
|
||||
created = self.pk is None
|
||||
|
@ -142,20 +152,25 @@ class Transaction(PolymorphicModel):
|
|||
self.source.balance -= to_transfer
|
||||
self.destination.balance += to_transfer
|
||||
|
||||
# We save first the transaction, in case of the user has no right to transfer money
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Save notes
|
||||
self.source.save()
|
||||
self.destination.save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.amount * self.quantity
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('Transfer')
|
||||
|
||||
class TemplateTransaction(Transaction):
|
||||
|
||||
class RecurrentTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
||||
|
||||
"""
|
||||
|
||||
template = models.ForeignKey(
|
||||
|
@ -168,6 +183,37 @@ class TemplateTransaction(Transaction):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('Template')
|
||||
|
||||
|
||||
class SpecialTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to transactions with special notes
|
||||
"""
|
||||
|
||||
last_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
first_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("first_name"),
|
||||
)
|
||||
|
||||
bank = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("bank"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
||||
|
||||
|
||||
class MembershipTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
||||
|
@ -183,3 +229,7 @@ class MembershipTransaction(Transaction):
|
|||
class Meta:
|
||||
verbose_name = _("membership transaction")
|
||||
verbose_name_plural = _("membership transactions")
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('membership transaction')
|
||||
|
|
|
@ -1,26 +1,115 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import html
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.db.models import F
|
||||
from django_tables2.utils import A
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models.transactions import Transaction
|
||||
from .models.notes import Alias
|
||||
from .models.transactions import Transaction, TransactionTemplate
|
||||
from .templatetags.pretty_money import pretty_money
|
||||
|
||||
|
||||
class HistoryTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class':
|
||||
'table table-condensed table-striped table-hover'
|
||||
'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = Transaction
|
||||
exclude = ("id", "polymorphic_ctype", )
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
sequence = ('...', 'total', 'valid')
|
||||
sequence = ('...', 'type', 'total', 'valid', )
|
||||
orderable = False
|
||||
|
||||
type = tables.Column()
|
||||
|
||||
total = tables.Column() # will use Transaction.total() !!
|
||||
|
||||
valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id),
|
||||
"class": lambda record: str(record.valid).lower() + ' validate',
|
||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', '
|
||||
+ str(record.valid).lower() + ')'}})
|
||||
|
||||
def order_total(self, queryset, is_descending):
|
||||
# needed for rendering
|
||||
queryset = queryset.annotate(total=F('amount') * F('quantity')) \
|
||||
.order_by(('-' if is_descending else '') + 'total')
|
||||
return (queryset, True)
|
||||
return queryset, True
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
def render_total(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
def render_type(self, value):
|
||||
return _(value)
|
||||
|
||||
# Django-tables escape strings. That's a wrong thing.
|
||||
def render_reason(self, value):
|
||||
return html.unescape(value)
|
||||
|
||||
def render_valid(self, value):
|
||||
return "✔" if value else "✖"
|
||||
|
||||
|
||||
# function delete_button(id) provided in template file
|
||||
DELETE_TEMPLATE = """
|
||||
<button id="{{ record.pk }}" class="btn btn-danger" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
|
||||
"""
|
||||
|
||||
|
||||
class AliasTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class':
|
||||
'table table condensed table-striped table-hover'
|
||||
}
|
||||
model = Alias
|
||||
fields = ('name',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
show_header = False
|
||||
name = tables.Column(attrs={'td': {'class': 'text-center'}})
|
||||
# delete = tables.TemplateColumn(template_code=delete_template,
|
||||
# attrs={'td':{'class': 'col-sm-1'}})
|
||||
|
||||
delete = tables.LinkColumn('member:user_alias_delete',
|
||||
args=[A('pk')],
|
||||
attrs={
|
||||
'td': {'class': 'col-sm-2'},
|
||||
'a': {'class': 'btn btn-danger'}},
|
||||
text='delete', accessor='pk')
|
||||
|
||||
|
||||
class ButtonTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class':
|
||||
'table table-bordered condensed table-hover'
|
||||
}
|
||||
row_attrs = {
|
||||
'class': lambda record: 'table-row ' + 'table-success' if record.display else 'table-danger',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
model = TransactionTemplate
|
||||
|
||||
edit = tables.LinkColumn('note:template_update',
|
||||
args=[A('pk')],
|
||||
attrs={'td': {'class': 'col-sm-1'},
|
||||
'a': {'class': 'btn btn-primary'}},
|
||||
text=_('edit'),
|
||||
accessor='pk')
|
||||
|
||||
delete = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={'td': {'class': 'col-sm-1'}})
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import template
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def getenv(value):
|
||||
return os.getenv(value)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
register.filter('getenv', getenv)
|
|
@ -11,12 +11,17 @@ def pretty_money(value):
|
|||
abs(value) // 100,
|
||||
)
|
||||
else:
|
||||
return "{:s}{:d} € {:02d}".format(
|
||||
return "{:s}{:d}.{:02d} €".format(
|
||||
"- " if value < 0 else "",
|
||||
abs(value) // 100,
|
||||
abs(value) % 100,
|
||||
)
|
||||
|
||||
|
||||
def cents_to_euros(value):
|
||||
return "{:.02f}".format(value / 100) if value else ""
|
||||
|
||||
|
||||
register = template.Library()
|
||||
register.filter('pretty_money', pretty_money)
|
||||
register.filter('cents_to_euros', cents_to_euros)
|
||||
|
|
|
@ -8,7 +8,7 @@ from .models import Note
|
|||
|
||||
app_name = 'note'
|
||||
urlpatterns = [
|
||||
path('transfer/', views.TransactionCreate.as_view(), name='transfer'),
|
||||
path('transfer/', views.TransactionCreateView.as_view(), name='transfer'),
|
||||
path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'),
|
||||
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
|
||||
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
|
||||
|
|
|
@ -3,60 +3,60 @@
|
|||
|
||||
from dal import autocomplete
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, ListView, UpdateView
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
from django_tables2 import SingleTableView
|
||||
from django.urls import reverse_lazy
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
|
||||
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
|
||||
from .forms import TransactionTemplateForm
|
||||
from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
|
||||
from .models.transactions import SpecialTransaction
|
||||
from .tables import HistoryTable, ButtonTable
|
||||
|
||||
|
||||
class TransactionCreate(LoginRequiredMixin, CreateView):
|
||||
class TransactionCreateView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Show transfer page
|
||||
|
||||
TODO: If user have sufficient rights, they can transfer from an other note
|
||||
View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`.
|
||||
e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
|
||||
"""
|
||||
template_name = "note/transaction_form.html"
|
||||
|
||||
model = Transaction
|
||||
form_class = TransactionForm
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
table_pagination = {"per_page": 50}
|
||||
|
||||
def get_queryset(self):
|
||||
return Transaction.objects.filter(PermissionBackend.filter_queryset(
|
||||
self.request.user, Transaction, "view")
|
||||
).order_by("-id").all()[:50]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add some context variables in template such as page title
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _('Transfer money from your account '
|
||||
'to one or others')
|
||||
|
||||
context['no_cache'] = True
|
||||
context['title'] = _('Transfer money')
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||
context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""
|
||||
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
|
||||
"""
|
||||
form = super().get_form(form_class)
|
||||
|
||||
if False: # TODO: fix it with "if %user has no right to transfer funds"
|
||||
del form.fields['source']
|
||||
form.user = self.request.user
|
||||
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('note:transfer')
|
||||
|
||||
|
||||
class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
||||
"""
|
||||
Auto complete note by aliases
|
||||
Auto complete note by aliases. Used in every search field for note
|
||||
ex: :view:`ConsoView`, :view:`TransactionCreateView`
|
||||
"""
|
||||
|
||||
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.
|
||||
Cette fonction récupère la requête, et renvoie la liste filtrée des aliases.
|
||||
When someone look for an :models:`note.Alias`, a query is sent to the dedicated API.
|
||||
This function handles the result and return a filtered list of aliases.
|
||||
"""
|
||||
# Un utilisateur non connecté n'a accès à aucune information
|
||||
if not self.request.user.is_authenticated:
|
||||
|
@ -66,7 +66,7 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
|||
|
||||
# self.q est le paramètre de la recherche
|
||||
if self.q:
|
||||
qs = qs.filter(Q(name__regex=self.q) | Q(normalized_name__regex=Alias.normalize(self.q)))\
|
||||
qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \
|
||||
.order_by('normalized_name').distinct()
|
||||
|
||||
# Filtrage par type de note (user, club, special)
|
||||
|
@ -85,6 +85,10 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
|||
return qs
|
||||
|
||||
def get_result_label(self, result):
|
||||
"""
|
||||
Show the selected alias and the username associated
|
||||
<Alias> (aka. <Username> )
|
||||
"""
|
||||
# Gère l'affichage de l'alias dans la recherche
|
||||
res = result.name
|
||||
note_name = str(result.note)
|
||||
|
@ -93,7 +97,9 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
|||
return res
|
||||
|
||||
def get_result_value(self, result):
|
||||
# Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias
|
||||
"""
|
||||
The value used for the transactions will be the id of the Note.
|
||||
"""
|
||||
return str(result.note.pk)
|
||||
|
||||
|
||||
|
@ -103,14 +109,15 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
|
|||
"""
|
||||
model = TransactionTemplate
|
||||
form_class = TransactionTemplateForm
|
||||
success_url = reverse_lazy('note:template_list')
|
||||
|
||||
|
||||
class TransactionTemplateListView(LoginRequiredMixin, ListView):
|
||||
class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List TransactionsTemplates
|
||||
"""
|
||||
model = TransactionTemplate
|
||||
form_class = TransactionTemplateForm
|
||||
table_class = ButtonTable
|
||||
|
||||
|
||||
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
@ -118,33 +125,40 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
|
|||
"""
|
||||
model = TransactionTemplate
|
||||
form_class = TransactionTemplateForm
|
||||
success_url = reverse_lazy('note:template_list')
|
||||
|
||||
|
||||
class ConsoView(LoginRequiredMixin, CreateView):
|
||||
class ConsoView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Consume
|
||||
The Magic View that make people pay their beer and burgers.
|
||||
(Most of the magic happens in the dark world of Javascript see consos.js)
|
||||
"""
|
||||
model = TemplateTransaction
|
||||
template_name = "note/conso_form.html"
|
||||
form_class = ConsoForm
|
||||
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
table_pagination = {"per_page": 50}
|
||||
|
||||
def get_queryset(self):
|
||||
return Transaction.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
||||
).order_by("-id").all()[:50]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add some context variables in template such as page title
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
|
||||
.order_by('category')
|
||||
context['title'] = _("Consommations")
|
||||
from django.db.models import Count
|
||||
buttons = TransactionTemplate.objects.filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name')
|
||||
context['transaction_templates'] = buttons
|
||||
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
|
||||
context['title'] = _("Consumptions")
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||
|
||||
# select2 compatibility
|
||||
context['no_cache'] = True
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
When clicking a button, reload the same page
|
||||
"""
|
||||
return reverse('note:consos')
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'permission.apps.PermissionConfig'
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Permission, PermissionMask, RolePermissions
|
||||
|
||||
|
||||
@admin.register(PermissionMask)
|
||||
class PermissionMaskAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for PermissionMask
|
||||
"""
|
||||
list_display = ('description', 'rank', )
|
||||
|
||||
|
||||
@admin.register(Permission)
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Permission
|
||||
"""
|
||||
list_display = ('type', 'model', 'field', 'mask', 'description', )
|
||||
|
||||
|
||||
@admin.register(RolePermissions)
|
||||
class RolePermissionsAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for RolePermissions
|
||||
"""
|
||||
list_display = ('role', )
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Permission
|
||||
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Permission types.
|
||||
The djangorestframework plugin will analyse the model `Permission` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = '__all__'
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import PermissionViewSet
|
||||
|
||||
|
||||
def register_permission_urls(router, path):
|
||||
"""
|
||||
Configure router for permission REST API.
|
||||
"""
|
||||
router.register(path, PermissionViewSet)
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
|
||||
from .serializers import PermissionSerializer
|
||||
from ..models import Permission
|
||||
|
||||
|
||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
queryset = Permission.objects.all()
|
||||
serializer_class = PermissionSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['model', 'type', ]
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import pre_save, pre_delete
|
||||
|
||||
|
||||
class PermissionConfig(AppConfig):
|
||||
name = 'permission'
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
||||
pre_save.connect(signals.pre_save_object)
|
||||
pre_delete.connect(signals.pre_delete_object)
|
|
@ -0,0 +1,116 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q, F
|
||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from member.models import Membership, Club
|
||||
|
||||
from .models import Permission
|
||||
|
||||
|
||||
class PermissionBackend(ModelBackend):
|
||||
"""
|
||||
Manage permissions of users
|
||||
"""
|
||||
supports_object_permissions = True
|
||||
supports_anonymous_user = False
|
||||
supports_inactive_user = False
|
||||
|
||||
@staticmethod
|
||||
def permissions(user, model, type):
|
||||
"""
|
||||
List all permissions of the given user that applies to a given model and a give type
|
||||
:param user: The owner of the permissions
|
||||
:param model: The model that the permissions shoud apply
|
||||
:param type: The type of the permissions: view, change, add or delete
|
||||
:return: A generator of the requested permissions
|
||||
"""
|
||||
for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
|
||||
.filter(
|
||||
rolepermissions__role__membership__user=user,
|
||||
model__app_label=model.app_label, # For polymorphic models, we don't filter on model type
|
||||
type=type,
|
||||
).all():
|
||||
if not isinstance(model, permission.model.__class__):
|
||||
continue
|
||||
|
||||
club = Club.objects.get(pk=permission.club)
|
||||
permission = permission.about(
|
||||
user=user,
|
||||
club=club,
|
||||
User=User,
|
||||
Club=Club,
|
||||
Membership=Membership,
|
||||
Note=Note,
|
||||
NoteUser=NoteUser,
|
||||
NoteClub=NoteClub,
|
||||
NoteSpecial=NoteSpecial,
|
||||
F=F,
|
||||
Q=Q
|
||||
)
|
||||
if permission.mask.rank <= get_current_session().get("permission_mask", 0):
|
||||
yield permission
|
||||
|
||||
@staticmethod
|
||||
def filter_queryset(user, model, t, field=None):
|
||||
"""
|
||||
Filter a queryset by considering the permissions of a given user.
|
||||
:param user: The owner of the permissions that are fetched
|
||||
:param model: The concerned model of the queryset
|
||||
:param t: The type of modification (view, add, change, delete)
|
||||
:param field: The field of the model to test, if concerned
|
||||
:return: A query that corresponds to the filter to give to a queryset
|
||||
"""
|
||||
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
# Anonymous users can't do anything
|
||||
return Q(pk=-1)
|
||||
|
||||
if user.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
|
||||
# Superusers have all rights
|
||||
return Q()
|
||||
|
||||
if not isinstance(model, ContentType):
|
||||
model = ContentType.objects.get_for_model(model)
|
||||
|
||||
# Never satisfied
|
||||
query = Q(pk=-1)
|
||||
perms = PermissionBackend.permissions(user, model, t)
|
||||
for perm in perms:
|
||||
if perm.field and field != perm.field:
|
||||
continue
|
||||
if perm.type != t or perm.model != model:
|
||||
continue
|
||||
perm.update_query()
|
||||
query = query | perm.query
|
||||
return query
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
if user_obj is None or isinstance(user_obj, AnonymousUser):
|
||||
return False
|
||||
|
||||
if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
|
||||
return True
|
||||
|
||||
if obj is None:
|
||||
return True
|
||||
|
||||
perm = perm.split('.')[-1].split('_', 2)
|
||||
perm_type = perm[0]
|
||||
perm_field = perm[2] if len(perm) == 3 else None
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
if any(permission.applies(obj, perm_type, perm_field)
|
||||
for permission in self.permissions(user_obj, ct, perm_type)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_module_perms(self, user_obj, app_label):
|
||||
return False
|
||||
|
||||
def get_all_permissions(self, user_obj, obj=None):
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
return list(self.permissions(user_obj, ct, "view"))
|
|
@ -0,0 +1,653 @@
|
|||
[
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Adh\u00e9rent BDE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Adh\u00e9rent Kfet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Pr\u00e9sident\u00b7e BDE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Tr\u00e9sorier\u00b7\u00e8re BDE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Respo info"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "GC Kfet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Pr\u00e9sident\u00b7e de club"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.role",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Tr\u00e9sorier\u00b7\u00e8re de club"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permissionmask",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"rank": 0,
|
||||
"description": "Droits basiques"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permissionmask",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"rank": 1,
|
||||
"description": "Droits note seulement"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permissionmask",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"rank": 42,
|
||||
"description": "Tous mes droits"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View our User object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"profile"
|
||||
],
|
||||
"query": "{\"user\": [\"user\"]}",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View our profile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"noteuser"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"note\", \"pk\"]}",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View our own note"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"model": [
|
||||
"authtoken",
|
||||
"token"
|
||||
],
|
||||
"query": "{\"user\": [\"user\"]}",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View our API token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View our own transactions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"alias"
|
||||
],
|
||||
"query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View aliases of clubs and members of Kfet club"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "last_login",
|
||||
"description": "Change myself's last login"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "username",
|
||||
"description": "Change myself's username"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "first_name",
|
||||
"description": "Change myself's first name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "last_name",
|
||||
"description": "Change myself's last name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"model": [
|
||||
"auth",
|
||||
"user"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "email",
|
||||
"description": "Change myself's email"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"model": [
|
||||
"authtoken",
|
||||
"token"
|
||||
],
|
||||
"query": "{\"user\": [\"user\"]}",
|
||||
"type": "delete",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "Delete API Token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"model": [
|
||||
"authtoken",
|
||||
"token"
|
||||
],
|
||||
"query": "{\"user\": [\"user\"]}",
|
||||
"type": "add",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "Create API Token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"alias"
|
||||
],
|
||||
"query": "{\"note\": [\"user\", \"note\"]}",
|
||||
"type": "delete",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "Remove alias"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 15,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"alias"
|
||||
],
|
||||
"query": "{\"note\": [\"user\", \"note\"]}",
|
||||
"type": "add",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "Add alias"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"noteuser"
|
||||
],
|
||||
"query": "{\"pk\": [\"user\", \"note\", \"pk\"]}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "display_image",
|
||||
"description": "Change myself's display image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, {\"amount__lte\": [\"user\", \"note\", \"balance\"]}]",
|
||||
"type": "add",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "Transfer from myself's note"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 18,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"note"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "balance",
|
||||
"description": "Update a note balance with a transaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 19,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"note"
|
||||
],
|
||||
"query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "View notes of club members"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 20,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
|
||||
"type": "add",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "Create transactions with a club"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 21,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"recurrenttransaction"
|
||||
],
|
||||
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
|
||||
"type": "add",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "Create transactions from buttons with a club"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 22,
|
||||
"fields": {
|
||||
"model": [
|
||||
"member",
|
||||
"club"
|
||||
],
|
||||
"query": "{\"pk\": [\"club\", \"pk\"]}",
|
||||
"type": "view",
|
||||
"mask": 1,
|
||||
"field": "",
|
||||
"description": "View club infos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 23,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "change",
|
||||
"mask": 1,
|
||||
"field": "valid",
|
||||
"description": "Update validation status of a transaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 24,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "View all transactions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 25,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"notespecial"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "Display credit/debit interface"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 26,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"specialtransaction"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "add",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "Create credit/debit transaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 27,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"templatecategory"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "View button categories"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 28,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"templatecategory"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"description": "Change button category"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 29,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"templatecategory"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"description": "Add button category"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 30,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "View buttons"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 31,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"description": "Add buttons"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 32,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
"description": "Update buttons"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 33,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"transaction"
|
||||
],
|
||||
"query": "{}",
|
||||
"type": "add",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"description": "Create any transaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.rolepermissions",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"role": 1,
|
||||
"permissions": [
|
||||
1,
|
||||
2,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.rolepermissions",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"role": 2,
|
||||
"permissions": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.rolepermissions",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"role": 8,
|
||||
"permissions": [
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.rolepermissions",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"role": 4,
|
||||
"permissions": [
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,282 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import functools
|
||||
import json
|
||||
import operator
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q, Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Role
|
||||
|
||||
|
||||
class InstancedPermission:
|
||||
|
||||
def __init__(self, model, query, type, field, mask, **kwargs):
|
||||
self.model = model
|
||||
self.raw_query = query
|
||||
self.query = None
|
||||
self.type = type
|
||||
self.field = field
|
||||
self.mask = mask
|
||||
self.kwargs = kwargs
|
||||
|
||||
def applies(self, obj, permission_type, field_name=None):
|
||||
"""
|
||||
Returns True if the permission applies to
|
||||
the field `field_name` object `obj`
|
||||
"""
|
||||
|
||||
if not isinstance(obj, self.model.model_class()):
|
||||
# The permission does not apply to the model
|
||||
return False
|
||||
|
||||
if self.type == 'add':
|
||||
if permission_type == self.type:
|
||||
self.update_query()
|
||||
|
||||
# Don't increase indexes
|
||||
obj.pk = 0
|
||||
# Force insertion, no data verification, no trigger
|
||||
Model.save(obj, force_insert=True)
|
||||
ret = obj in self.model.model_class().objects.filter(self.query).all()
|
||||
# Delete testing object
|
||||
Model.delete(obj)
|
||||
return ret
|
||||
|
||||
if permission_type == self.type:
|
||||
if self.field and field_name != self.field:
|
||||
return False
|
||||
self.update_query()
|
||||
return obj in self.model.model_class().objects.filter(self.query).all()
|
||||
else:
|
||||
return False
|
||||
|
||||
def update_query(self):
|
||||
"""
|
||||
The query is not analysed in a first time. It is analysed at most once if needed.
|
||||
:return:
|
||||
"""
|
||||
if not self.query:
|
||||
# noinspection PyProtectedMember
|
||||
self.query = Permission._about(self.raw_query, **self.kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
if self.field:
|
||||
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
||||
else:
|
||||
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class PermissionMask(models.Model):
|
||||
"""
|
||||
Permissions that are hidden behind a mask
|
||||
"""
|
||||
|
||||
rank = models.PositiveSmallIntegerField(
|
||||
unique=True,
|
||||
verbose_name=_('rank'),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name=_('description'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
|
||||
class Permission(models.Model):
|
||||
|
||||
PERMISSION_TYPES = [
|
||||
('add', 'add'),
|
||||
('view', 'view'),
|
||||
('change', 'change'),
|
||||
('delete', 'delete')
|
||||
]
|
||||
|
||||
model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+')
|
||||
|
||||
# A json encoded Q object with the following grammar
|
||||
# query -> [] | {} (the empty query representing all objects)
|
||||
# query -> ["AND", query, …] AND multiple queries
|
||||
# | ["OR", query, …] OR multiple queries
|
||||
# | ["NOT", query] Opposite of query
|
||||
# query -> {key: value, …} A list of fields and values of a Q object
|
||||
# key -> string A field name
|
||||
# value -> int | string | bool | null Literal values
|
||||
# | [parameter, …] A parameter. See compute_param for more details.
|
||||
# | {"F": oper} An F object
|
||||
# oper -> [string, …] A parameter. See compute_param for more details.
|
||||
# | ["ADD", oper, …] Sum multiple F objects or literal
|
||||
# | ["SUB", oper, oper] Substract two F objects or literal
|
||||
# | ["MUL", oper, …] Multiply F objects or literals
|
||||
# | int | string | bool | null Literal values
|
||||
# | ["F", string] A field
|
||||
#
|
||||
# Examples:
|
||||
# Q(is_superuser=True) := {"is_superuser": true}
|
||||
# ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}]
|
||||
query = models.TextField()
|
||||
|
||||
type = models.CharField(max_length=15, choices=PERMISSION_TYPES)
|
||||
|
||||
mask = models.ForeignKey(
|
||||
PermissionMask,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
field = models.CharField(max_length=255, blank=True)
|
||||
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('model', 'query', 'type', 'field')
|
||||
|
||||
def clean(self):
|
||||
self.query = json.dumps(json.loads(self.query))
|
||||
if self.field and self.type not in {'view', 'change'}:
|
||||
raise ValidationError(_("Specifying field applies only to view and change permission types."))
|
||||
|
||||
def save(self, **kwargs):
|
||||
self.full_clean()
|
||||
super().save()
|
||||
|
||||
@staticmethod
|
||||
def compute_f(oper, **kwargs):
|
||||
if isinstance(oper, list):
|
||||
if oper[0] == 'ADD':
|
||||
return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'SUB':
|
||||
return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs)
|
||||
elif oper[0] == 'MUL':
|
||||
return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]])
|
||||
elif oper[0] == 'F':
|
||||
return F(oper[1])
|
||||
else:
|
||||
field = kwargs[oper[0]]
|
||||
for i in range(1, len(oper)):
|
||||
field = getattr(field, oper[i])
|
||||
return field
|
||||
else:
|
||||
return oper
|
||||
|
||||
@staticmethod
|
||||
def compute_param(value, **kwargs):
|
||||
"""
|
||||
A parameter is given by a list. The first argument is the name of the parameter.
|
||||
The parameters are the user, the club, and some classes (Note, ...)
|
||||
If there are more arguments in the list, then attributes are queried.
|
||||
For example, ["user", "note", "balance"] will return the balance of the note of the user.
|
||||
If an argument is a list, then this is interpreted with a function call:
|
||||
First argument is the name of the function, next arguments are parameters, and if there is a dict,
|
||||
then the dict is given as kwargs.
|
||||
For example: NoteUser.objects.filter(user__memberships__club__name="Kfet").all() is translated by:
|
||||
["NoteUser", "objects", ["filter", {"user__memberships__club__name": "Kfet"}], ["all"]]
|
||||
"""
|
||||
|
||||
if not isinstance(value, list):
|
||||
return value
|
||||
|
||||
field = kwargs[value[0]]
|
||||
for i in range(1, len(value)):
|
||||
if isinstance(value[i], list):
|
||||
if value[i][0] in kwargs:
|
||||
field = Permission.compute_param(value[i], **kwargs)
|
||||
continue
|
||||
|
||||
field = getattr(field, value[i][0])
|
||||
params = []
|
||||
call_kwargs = {}
|
||||
for j in range(1, len(value[i])):
|
||||
param = Permission.compute_param(value[i][j], **kwargs)
|
||||
if isinstance(param, dict):
|
||||
for key in param:
|
||||
val = Permission.compute_param(param[key], **kwargs)
|
||||
call_kwargs[key] = val
|
||||
else:
|
||||
params.append(param)
|
||||
field = field(*params, **call_kwargs)
|
||||
else:
|
||||
field = getattr(field, value[i])
|
||||
return field
|
||||
|
||||
@staticmethod
|
||||
def _about(query, **kwargs):
|
||||
"""
|
||||
Translate JSON query into a Q query.
|
||||
:param query: The JSON query
|
||||
:param kwargs: Additional params
|
||||
:return: A Q object
|
||||
"""
|
||||
if len(query) == 0:
|
||||
# The query is either [] or {} and
|
||||
# applies to all objects of the model
|
||||
# to represent this we return a trivial request
|
||||
return Q(pk=F("pk"))
|
||||
if isinstance(query, list):
|
||||
if query[0] == 'AND':
|
||||
return functools.reduce(operator.and_, [Permission._about(query, **kwargs) for query in query[1:]])
|
||||
elif query[0] == 'OR':
|
||||
return functools.reduce(operator.or_, [Permission._about(query, **kwargs) for query in query[1:]])
|
||||
elif query[0] == 'NOT':
|
||||
return ~Permission._about(query[1], **kwargs)
|
||||
else:
|
||||
return Q(pk=F("pk"))
|
||||
elif isinstance(query, dict):
|
||||
q_kwargs = {}
|
||||
for key in query:
|
||||
value = query[key]
|
||||
if isinstance(value, list):
|
||||
# It is a parameter we query its return value
|
||||
q_kwargs[key] = Permission.compute_param(value, **kwargs)
|
||||
elif isinstance(value, dict):
|
||||
# It is an F object
|
||||
q_kwargs[key] = Permission.compute_f(value['F'], **kwargs)
|
||||
else:
|
||||
q_kwargs[key] = value
|
||||
return Q(**q_kwargs)
|
||||
else:
|
||||
# TODO: find a better way to crash here
|
||||
raise Exception("query {} is wrong".format(query))
|
||||
|
||||
def about(self, **kwargs):
|
||||
"""
|
||||
Return an InstancedPermission with the parameters
|
||||
replaced by their values and the query interpreted
|
||||
"""
|
||||
query = json.loads(self.query)
|
||||
# query = self._about(query, **kwargs)
|
||||
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.field:
|
||||
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
||||
else:
|
||||
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
||||
|
||||
|
||||
class RolePermissions(models.Model):
|
||||
"""
|
||||
Permissions associated with a Role
|
||||
"""
|
||||
role = models.ForeignKey(
|
||||
Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+',
|
||||
verbose_name=_('role'),
|
||||
)
|
||||
permissions = models.ManyToManyField(
|
||||
Permission,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.role)
|
|
@ -0,0 +1,65 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from .backends import PermissionBackend
|
||||
|
||||
SAFE_METHODS = ('HEAD', 'OPTIONS', )
|
||||
|
||||
|
||||
class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
||||
"""
|
||||
Default DjangoObjectPermissions grant view permission to all.
|
||||
This is a simple patch of this class that controls view access.
|
||||
"""
|
||||
|
||||
perms_map = {
|
||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
||||
'OPTIONS': [],
|
||||
'HEAD': [],
|
||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||
}
|
||||
|
||||
def get_required_object_permissions(self, method, model_cls):
|
||||
kwargs = {
|
||||
'app_label': model_cls._meta.app_label,
|
||||
'model_name': model_cls._meta.model_name
|
||||
}
|
||||
|
||||
if method not in self.perms_map:
|
||||
from rest_framework import exceptions
|
||||
raise exceptions.MethodNotAllowed(method)
|
||||
|
||||
return [perm % kwargs for perm in self.perms_map[method]]
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# authentication checks have already executed via has_permission
|
||||
queryset = self._queryset(view)
|
||||
model_cls = queryset.model
|
||||
user = request.user
|
||||
|
||||
perms = self.get_required_object_permissions(request.method, model_cls)
|
||||
# if not user.has_perms(perms, obj):
|
||||
if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms):
|
||||
# If the user does not have permissions we need to determine if
|
||||
# they have read permissions to see 403, or not, and simply see
|
||||
# a 404 response.
|
||||
from django.http import Http404
|
||||
|
||||
if request.method in SAFE_METHODS:
|
||||
# Read permissions already checked and failed, no need
|
||||
# to make another lookup.
|
||||
raise Http404
|
||||
|
||||
read_perms = self.get_required_object_permissions('GET', model_cls)
|
||||
if not user.has_perms(read_perms, obj):
|
||||
raise Http404
|
||||
|
||||
# Has read permissions.
|
||||
return False
|
||||
|
||||
return True
|
|
@ -0,0 +1,105 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
|
||||
from logs import signals as logs_signals
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
|
||||
EXCLUDED = [
|
||||
'cas_server.proxygrantingticket',
|
||||
'cas_server.proxyticket',
|
||||
'cas_server.serviceticket',
|
||||
'cas_server.user',
|
||||
'cas_server.userattributes',
|
||||
'contenttypes.contenttype',
|
||||
'logs.changelog',
|
||||
'migrations.migration',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
||||
def pre_save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get saved, we check the permissions
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
user = get_current_authenticated_user()
|
||||
if user is None:
|
||||
# Action performed on shell is always granted
|
||||
return
|
||||
|
||||
qs = sender.objects.filter(pk=instance.pk).all()
|
||||
model_name_full = instance._meta.label_lower.split(".")
|
||||
app_label = model_name_full[0]
|
||||
model_name = model_name_full[1]
|
||||
|
||||
if qs.exists():
|
||||
# We check if the user can change the model
|
||||
|
||||
# If the user has all right on a model, then OK
|
||||
if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance):
|
||||
return
|
||||
|
||||
# In the other case, we check if he/she has the right to change one field
|
||||
previous = qs.get()
|
||||
for field in instance._meta.fields:
|
||||
field_name = field.name
|
||||
old_value = getattr(previous, field.name)
|
||||
new_value = getattr(instance, field.name)
|
||||
# If the field wasn't modified, no need to check the permissions
|
||||
if old_value == new_value:
|
||||
continue
|
||||
if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
|
||||
raise PermissionDenied
|
||||
else:
|
||||
# We check if the user can add the model
|
||||
|
||||
# While checking permissions, the object will be inserted in the DB, then removed.
|
||||
# We disable temporary the connectors
|
||||
pre_save.disconnect(pre_save_object)
|
||||
pre_delete.disconnect(pre_delete_object)
|
||||
# We disable also logs connectors
|
||||
pre_save.disconnect(logs_signals.pre_save_object)
|
||||
post_save.disconnect(logs_signals.save_object)
|
||||
post_delete.disconnect(logs_signals.delete_object)
|
||||
|
||||
# We check if the user has right to add the object
|
||||
has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
|
||||
|
||||
# Then we reconnect all
|
||||
pre_save.connect(pre_save_object)
|
||||
pre_delete.connect(pre_delete_object)
|
||||
pre_save.connect(logs_signals.pre_save_object)
|
||||
post_save.connect(logs_signals.save_object)
|
||||
post_delete.connect(logs_signals.delete_object)
|
||||
|
||||
if not has_perm:
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
def pre_delete_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get deleted, we check the permissions
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
user = get_current_authenticated_user()
|
||||
if user is None:
|
||||
# Action performed on shell is always granted
|
||||
return
|
||||
|
||||
model_name_full = instance._meta.label_lower.split(".")
|
||||
app_label = model_name_full[0]
|
||||
model_name = model_name_full[1]
|
||||
|
||||
# We check if the user has rights to delete the object
|
||||
if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance):
|
||||
raise PermissionDenied
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django import template
|
||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
|
||||
@stringfilter
|
||||
def not_empty_model_list(model_name):
|
||||
"""
|
||||
Return True if and only if the current user has right to see any object of the given model.
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
session = get_current_session()
|
||||
if user is None:
|
||||
return False
|
||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||
return True
|
||||
if session.get("not_empty_model_list_" + model_name, None):
|
||||
return session.get("not_empty_model_list_" + model_name, None) == 1
|
||||
spl = model_name.split(".")
|
||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all()
|
||||
session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2
|
||||
return session.get("not_empty_model_list_" + model_name) == 1
|
||||
|
||||
|
||||
@stringfilter
|
||||
def not_empty_model_change_list(model_name):
|
||||
"""
|
||||
Return True if and only if the current user has right to change any object of the given model.
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
session = get_current_session()
|
||||
if user is None:
|
||||
return False
|
||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||
return True
|
||||
if session.get("not_empty_model_change_list_" + model_name, None):
|
||||
return session.get("not_empty_model_change_list_" + model_name, None) == 1
|
||||
spl = model_name.split(".")
|
||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
|
||||
session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2
|
||||
return session.get("not_empty_model_change_list_" + model_name) == 1
|
||||
|
||||
|
||||
register = template.Library()
|
||||
register.filter('not_empty_model_list', not_empty_model_list)
|
||||
register.filter('not_empty_model_change_list', not_empty_model_change_list)
|
|
@ -0,0 +1 @@
|
|||
Subproject commit b9fdced3c2ce34168b8f0d6004a20a69ca16e0de
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'treasury.apps.TreasuryConfig'
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import RemittanceType, Remittance
|
||||
|
||||
|
||||
@admin.register(RemittanceType)
|
||||
class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for RemiitanceType
|
||||
"""
|
||||
list_display = ('note', )
|
||||
|
||||
|
||||
@admin.register(Remittance)
|
||||
class RemittanceAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Remittance
|
||||
"""
|
||||
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not obj:
|
||||
return True
|
||||
return not obj.closed and super().has_change_permission(request, obj)
|
|
@ -0,0 +1,62 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
from note.api.serializers import SpecialTransactionSerializer
|
||||
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Product types.
|
||||
The djangorestframework plugin will analyse the model `Product` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class InvoiceSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Invoice types.
|
||||
The djangorestframework plugin will analyse the model `Invoice` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = '__all__'
|
||||
read_only_fields = ('bde',)
|
||||
|
||||
products = serializers.SerializerMethodField()
|
||||
|
||||
def get_products(self, obj):
|
||||
return serializers.ListSerializer(child=ProductSerializer())\
|
||||
.to_representation(Product.objects.filter(invoice=obj).all())
|
||||
|
||||
|
||||
class RemittanceTypeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for RemittanceType types.
|
||||
The djangorestframework plugin will analyse the model `RemittanceType` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RemittanceType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RemittanceSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Remittance types.
|
||||
The djangorestframework plugin will analyse the model `Remittance` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
transactions = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Remittance
|
||||
fields = '__all__'
|
||||
|
||||
def get_transactions(self, obj):
|
||||
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions)
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet
|
||||
|
||||
|
||||
def register_treasury_urls(router, path):
|
||||
"""
|
||||
Configure router for treasury REST API.
|
||||
"""
|
||||
router.register(path + '/invoice', InvoiceViewSet)
|
||||
router.register(path + '/product', ProductViewSet)
|
||||
router.register(path + '/remittance_type', RemittanceTypeViewSet)
|
||||
router.register(path + '/remittance', RemittanceViewSet)
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer
|
||||
from ..models import Invoice, Product, RemittanceType, Remittance
|
||||
|
||||
|
||||
class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/invoice/
|
||||
"""
|
||||
queryset = Invoice.objects.all()
|
||||
serializer_class = InvoiceSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['bde', ]
|
||||
|
||||
|
||||
class ProductViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/product/
|
||||
"""
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$designation', ]
|
||||
|
||||
|
||||
class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
||||
then render it on /api/treasury/remittance_type/
|
||||
"""
|
||||
queryset = RemittanceType.objects.all()
|
||||
serializer_class = RemittanceTypeSerializer
|
||||
|
||||
|
||||
class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/treasury/remittance/
|
||||
"""
|
||||
queryset = Remittance.objects.all()
|
||||
serializer_class = RemittanceSerializer
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models import Q
|
||||
from django.db.models.signals import post_save, post_migrate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class TreasuryConfig(AppConfig):
|
||||
name = 'treasury'
|
||||
verbose_name = _('Treasury')
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Define app internal signals to interact with other apps
|
||||
"""
|
||||
|
||||
from . import signals
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from treasury.models import SpecialTransactionProxy
|
||||
post_save.connect(signals.save_special_transaction, sender=SpecialTransaction)
|
||||
|
||||
def setup_specialtransactions_proxies(**kwargs):
|
||||
# If the treasury app was disabled for any reason during a certain amount of time,
|
||||
# we ensure that each special transaction is linked to a proxy
|
||||
for transaction in SpecialTransaction.objects.filter(
|
||||
source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||
specialtransactionproxy=None,
|
||||
):
|
||||
SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None)
|
||||
|
||||
post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy)
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"model": "treasury.remittancetype",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"note": 3
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,156 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import datetime
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||
|
||||
|
||||
class InvoiceForm(forms.ModelForm):
|
||||
"""
|
||||
Create and generate invoices.
|
||||
"""
|
||||
|
||||
# Django forms don't support date fields. We have to add it manually
|
||||
date = forms.DateField(
|
||||
initial=datetime.date.today,
|
||||
widget=forms.TextInput(attrs={'type': 'date'})
|
||||
)
|
||||
|
||||
def clean_date(self):
|
||||
self.instance.date = self.data.get("date")
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
exclude = ('bde', )
|
||||
|
||||
|
||||
# Add a subform per product in the invoice form, and manage correctly the link between the invoice and
|
||||
# its products. The FormSet will search automatically the ForeignKey in the Product model.
|
||||
ProductFormSet = forms.inlineformset_factory(
|
||||
Invoice,
|
||||
Product,
|
||||
fields='__all__',
|
||||
extra=1,
|
||||
)
|
||||
|
||||
|
||||
class ProductFormSetHelper(FormHelper):
|
||||
"""
|
||||
Specify some template informations for the product form.
|
||||
"""
|
||||
|
||||
def __init__(self, form=None):
|
||||
super().__init__(form)
|
||||
self.form_tag = False
|
||||
self.form_method = 'POST'
|
||||
self.form_class = 'form-inline'
|
||||
self.template = 'bootstrap4/table_inline_formset.html'
|
||||
|
||||
|
||||
class RemittanceForm(forms.ModelForm):
|
||||
"""
|
||||
Create remittances.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.helper = FormHelper()
|
||||
|
||||
# We can't update the type of the remittance once created.
|
||||
if self.instance.pk:
|
||||
self.fields["remittance_type"].disabled = True
|
||||
self.fields["remittance_type"].required = False
|
||||
|
||||
# We display the submit button iff the remittance is open,
|
||||
# the close button iff it is open and has a linked transaction
|
||||
if not self.instance.closed:
|
||||
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
||||
if self.instance.transactions:
|
||||
self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success'))
|
||||
else:
|
||||
# If the remittance is closed, we can't change anything
|
||||
self.fields["comment"].disabled = True
|
||||
self.fields["comment"].required = False
|
||||
|
||||
def clean(self):
|
||||
# We can't update anything if the remittance is already closed.
|
||||
if self.instance.closed:
|
||||
self.add_error("comment", _("Remittance is already closed."))
|
||||
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if self.instance.pk and cleaned_data.get("remittance_type") != self.instance.remittance_type:
|
||||
self.add_error("remittance_type", _("You can't change the type of the remittance."))
|
||||
|
||||
# The close button is manually handled
|
||||
if "close" in self.data:
|
||||
self.instance.closed = True
|
||||
self.cleaned_data["closed"] = True
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Remittance
|
||||
fields = ('remittance_type', 'comment',)
|
||||
|
||||
|
||||
class LinkTransactionToRemittanceForm(forms.ModelForm):
|
||||
"""
|
||||
Attach a special transaction to a remittance.
|
||||
"""
|
||||
|
||||
# Since we use a proxy model for special transactions, we add manually the fields related to the transaction
|
||||
last_name = forms.CharField(label=_("Last name"))
|
||||
|
||||
first_name = forms.Field(label=_("First name"))
|
||||
|
||||
bank = forms.Field(label=_("Bank"))
|
||||
|
||||
amount = forms.IntegerField(label=_("Amount"), min_value=0)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
# Add submit button
|
||||
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
||||
|
||||
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
|
||||
|
||||
def clean_last_name(self):
|
||||
"""
|
||||
Replace the first name in the information of the transaction.
|
||||
"""
|
||||
self.instance.transaction.last_name = self.data.get("last_name")
|
||||
self.instance.transaction.clean()
|
||||
|
||||
def clean_first_name(self):
|
||||
"""
|
||||
Replace the last name in the information of the transaction.
|
||||
"""
|
||||
self.instance.transaction.first_name = self.data.get("first_name")
|
||||
self.instance.transaction.clean()
|
||||
|
||||
def clean_bank(self):
|
||||
"""
|
||||
Replace the bank in the information of the transaction.
|
||||
"""
|
||||
self.instance.transaction.bank = self.data.get("bank")
|
||||
self.instance.transaction.clean()
|
||||
|
||||
def clean_amount(self):
|
||||
"""
|
||||
Replace the amount of the transaction.
|
||||
"""
|
||||
self.instance.transaction.amount = self.data.get("amount")
|
||||
self.instance.transaction.clean()
|
||||
|
||||
class Meta:
|
||||
model = SpecialTransactionProxy
|
||||
fields = ('remittance', )
|
|
@ -0,0 +1,189 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, SpecialTransaction
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
"""
|
||||
An invoice model that can generates a true invoice.
|
||||
"""
|
||||
|
||||
id = models.PositiveIntegerField(
|
||||
primary_key=True,
|
||||
verbose_name=_("Invoice identifier"),
|
||||
)
|
||||
|
||||
bde = models.CharField(
|
||||
max_length=32,
|
||||
default='Saperlistpopette.png',
|
||||
choices=(
|
||||
('Saperlistpopette.png', 'Saper[list]popette'),
|
||||
('Finalist.png', 'Fina[list]'),
|
||||
('Listorique.png', '[List]orique'),
|
||||
('Satellist.png', 'Satel[list]'),
|
||||
('Monopolist.png', 'Monopo[list]'),
|
||||
('Kataclist.png', 'Katac[list]'),
|
||||
),
|
||||
verbose_name=_("BDE"),
|
||||
)
|
||||
|
||||
object = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Object"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
verbose_name=_("Description")
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
|
||||
address = models.TextField(
|
||||
verbose_name=_("Address"),
|
||||
)
|
||||
|
||||
date = models.DateField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("Place"),
|
||||
)
|
||||
|
||||
acquitted = models.BooleanField(
|
||||
verbose_name=_("Acquitted"),
|
||||
)
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
"""
|
||||
Product that appears on an invoice.
|
||||
"""
|
||||
|
||||
invoice = models.ForeignKey(
|
||||
Invoice,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
designation = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Designation"),
|
||||
)
|
||||
|
||||
quantity = models.PositiveIntegerField(
|
||||
verbose_name=_("Quantity")
|
||||
)
|
||||
|
||||
amount = models.IntegerField(
|
||||
verbose_name=_("Unit price")
|
||||
)
|
||||
|
||||
@property
|
||||
def amount_euros(self):
|
||||
return self.amount / 100
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.quantity * self.amount
|
||||
|
||||
@property
|
||||
def total_euros(self):
|
||||
return self.total / 100
|
||||
|
||||
|
||||
class RemittanceType(models.Model):
|
||||
"""
|
||||
Store what kind of remittances can be stored.
|
||||
"""
|
||||
|
||||
note = models.OneToOneField(
|
||||
NoteSpecial,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.note)
|
||||
|
||||
|
||||
class Remittance(models.Model):
|
||||
"""
|
||||
Treasurers want to regroup checks or bank transfers in bank remittances.
|
||||
"""
|
||||
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("Date"),
|
||||
)
|
||||
|
||||
remittance_type = models.ForeignKey(
|
||||
RemittanceType,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("Type"),
|
||||
)
|
||||
|
||||
comment = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Comment"),
|
||||
)
|
||||
|
||||
closed = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Closed"),
|
||||
)
|
||||
|
||||
@property
|
||||
def transactions(self):
|
||||
"""
|
||||
:return: Transactions linked to this remittance.
|
||||
"""
|
||||
if not self.pk:
|
||||
return SpecialTransaction.objects.none()
|
||||
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self)
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
Linked transactions count.
|
||||
"""
|
||||
return self.transactions.count()
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
"""
|
||||
Total amount of the remittance.
|
||||
"""
|
||||
return sum(transaction.total for transaction in self.transactions.all())
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
# Check if all transactions have the right type.
|
||||
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
||||
raise ValidationError("All transactions in a remittance must have the same type")
|
||||
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
def __str__(self):
|
||||
return _("Remittance #{:d}: {}").format(self.id, self.comment, )
|
||||
|
||||
|
||||
class SpecialTransactionProxy(models.Model):
|
||||
"""
|
||||
In order to keep modularity, we don't that the Note app depends on the treasury app.
|
||||
That's why we create a proxy in this app, to link special transactions and remittances.
|
||||
If it isn't very clean, that makes what we want.
|
||||
"""
|
||||
|
||||
transaction = models.OneToOneField(
|
||||
SpecialTransaction,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
remittance = models.ForeignKey(
|
||||
Remittance,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
verbose_name=_("Remittance"),
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from treasury.models import SpecialTransactionProxy, RemittanceType
|
||||
|
||||
|
||||
def save_special_transaction(instance, created, **kwargs):
|
||||
"""
|
||||
When a special transaction is created, we create its linked proxy
|
||||
"""
|
||||
if created and RemittanceType.objects.filter(note=instance.source).exists():
|
||||
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()
|
|
@ -0,0 +1,103 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_tables2 import A
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
|
||||
from .models import Invoice, Remittance
|
||||
|
||||
|
||||
class InvoiceTable(tables.Table):
|
||||
"""
|
||||
List all invoices.
|
||||
"""
|
||||
id = tables.LinkColumn("treasury:invoice_update",
|
||||
args=[A("pk")],
|
||||
text=lambda record: _("Invoice #{:d}").format(record.id), )
|
||||
|
||||
invoice = tables.LinkColumn("treasury:invoice_render",
|
||||
verbose_name=_("Invoice"),
|
||||
args=[A("pk")],
|
||||
accessor="pk",
|
||||
text="",
|
||||
attrs={
|
||||
'a': {'class': 'fa fa-file-pdf-o'},
|
||||
'td': {'data-turbolinks': 'false'}
|
||||
})
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = Invoice
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'name', 'object', 'acquitted', 'invoice',)
|
||||
|
||||
|
||||
class RemittanceTable(tables.Table):
|
||||
"""
|
||||
List all remittances.
|
||||
"""
|
||||
|
||||
count = tables.Column(verbose_name=_("Transaction count"))
|
||||
|
||||
amount = tables.Column(verbose_name=_("Amount"))
|
||||
|
||||
view = tables.LinkColumn("treasury:remittance_update",
|
||||
verbose_name=_("View"),
|
||||
args=[A("pk")],
|
||||
text=_("View"),
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary'}
|
||||
}, )
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = Remittance
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'date', 'remittance_type', 'comment', 'count', 'amount', 'view',)
|
||||
|
||||
|
||||
class SpecialTransactionTable(tables.Table):
|
||||
"""
|
||||
List special credit transactions that are (or not, following the queryset) attached to a remittance.
|
||||
"""
|
||||
|
||||
# Display add and remove buttons. Use the `exclude` field to select what is needed.
|
||||
remittance_add = tables.LinkColumn("treasury:link_transaction",
|
||||
verbose_name=_("Remittance"),
|
||||
args=[A("specialtransactionproxy.pk")],
|
||||
text=_("Add"),
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary'}
|
||||
}, )
|
||||
|
||||
remittance_remove = tables.LinkColumn("treasury:unlink_transaction",
|
||||
verbose_name=_("Remittance"),
|
||||
args=[A("specialtransactionproxy.pk")],
|
||||
text=_("Remove"),
|
||||
attrs={
|
||||
'a': {'class': 'btn btn-primary btn-danger'}
|
||||
}, )
|
||||
|
||||
def render_id(self, record):
|
||||
return record.specialtransactionproxy.pk
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
model = SpecialTransaction
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\
|
||||
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView
|
||||
|
||||
app_name = 'treasury'
|
||||
urlpatterns = [
|
||||
# Invoice app paths
|
||||
path('invoice/', InvoiceListView.as_view(), name='invoice_list'),
|
||||
path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'),
|
||||
path('invoice/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'),
|
||||
path('invoice/render/<int:pk>/', InvoiceRenderView.as_view(), name='invoice_render'),
|
||||
|
||||
# Remittance app paths
|
||||
path('remittance/', RemittanceListView.as_view(), name='remittance_list'),
|
||||
path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'),
|
||||
path('remittance/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'),
|
||||
path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'),
|
||||
path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(),
|
||||
name='unlink_transaction'),
|
||||
]
|
|
@ -0,0 +1,316 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
from django.views.generic.base import View, TemplateView
|
||||
from django_tables2 import SingleTableView
|
||||
from note.models import SpecialTransaction, NoteSpecial
|
||||
from note_kfet.settings.base import BASE_DIR
|
||||
|
||||
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
|
||||
|
||||
|
||||
class InvoiceCreateView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create Invoice
|
||||
"""
|
||||
model = Invoice
|
||||
form_class = InvoiceForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
form = context['form']
|
||||
form.helper = FormHelper()
|
||||
# Remove form tag on the generation of the form in the template (already present on the template)
|
||||
form.helper.form_tag = False
|
||||
# The formset handles the set of the products
|
||||
form_set = ProductFormSet(instance=form.instance)
|
||||
context['formset'] = form_set
|
||||
context['helper'] = ProductFormSetHelper()
|
||||
context['no_cache'] = True
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
ret = super().form_valid(form)
|
||||
|
||||
kwargs = {}
|
||||
|
||||
# The user type amounts in cents. We convert it in euros.
|
||||
for key in self.request.POST:
|
||||
value = self.request.POST[key]
|
||||
if key.endswith("amount") and value:
|
||||
kwargs[key] = str(int(100 * float(value)))
|
||||
elif value:
|
||||
kwargs[key] = value
|
||||
|
||||
# For each product, we save it
|
||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
||||
if formset.is_valid():
|
||||
for f in formset:
|
||||
# We don't save the product if the designation is not entered, ie. if the line is empty
|
||||
if f.is_valid() and f.instance.designation:
|
||||
f.save()
|
||||
f.instance.save()
|
||||
else:
|
||||
f.instance = None
|
||||
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:invoice_list')
|
||||
|
||||
|
||||
class InvoiceListView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List existing Invoices
|
||||
"""
|
||||
model = Invoice
|
||||
table_class = InvoiceTable
|
||||
|
||||
|
||||
class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Create Invoice
|
||||
"""
|
||||
model = Invoice
|
||||
form_class = InvoiceForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
form = context['form']
|
||||
form.helper = FormHelper()
|
||||
# Remove form tag on the generation of the form in the template (already present on the template)
|
||||
form.helper.form_tag = False
|
||||
# Fill the intial value for the date field, with the initial date of the model instance
|
||||
form.fields['date'].initial = form.instance.date
|
||||
# The formset handles the set of the products
|
||||
form_set = ProductFormSet(instance=form.instance)
|
||||
context['formset'] = form_set
|
||||
context['helper'] = ProductFormSetHelper()
|
||||
context['no_cache'] = True
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
ret = super().form_valid(form)
|
||||
|
||||
kwargs = {}
|
||||
# The user type amounts in cents. We convert it in euros.
|
||||
for key in self.request.POST:
|
||||
value = self.request.POST[key]
|
||||
if key.endswith("amount") and value:
|
||||
kwargs[key] = str(int(100 * float(value)))
|
||||
elif value:
|
||||
kwargs[key] = value
|
||||
|
||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
||||
saved = []
|
||||
# For each product, we save it
|
||||
if formset.is_valid():
|
||||
for f in formset:
|
||||
# We don't save the product if the designation is not entered, ie. if the line is empty
|
||||
if f.is_valid() and f.instance.designation:
|
||||
f.save()
|
||||
f.instance.save()
|
||||
saved.append(f.instance.pk)
|
||||
else:
|
||||
f.instance = None
|
||||
# Remove old products that weren't given in the form
|
||||
Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete()
|
||||
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:invoice_list')
|
||||
|
||||
|
||||
class InvoiceRenderView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Render Invoice as a generated PDF with the given information and a LaTeX template
|
||||
"""
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
pk = kwargs["pk"]
|
||||
invoice = Invoice.objects.get(pk=pk)
|
||||
products = Product.objects.filter(invoice=invoice).all()
|
||||
|
||||
# Informations of the BDE. Should be updated when the school will move.
|
||||
invoice.place = "Cachan"
|
||||
invoice.my_name = "BDE ENS Cachan"
|
||||
invoice.my_address_street = "61 avenue du Président Wilson"
|
||||
invoice.my_city = "94230 Cachan"
|
||||
invoice.bank_code = 30003
|
||||
invoice.desk_code = 3894
|
||||
invoice.account_number = 37280662
|
||||
invoice.rib_key = 14
|
||||
invoice.bic = "SOGEFRPP"
|
||||
|
||||
# Replace line breaks with the LaTeX equivalent
|
||||
invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ")
|
||||
invoice.address = invoice.address.replace("\r", "").replace("\n", "\\\\ ")
|
||||
# Fill the template with the information
|
||||
tex = render_to_string("treasury/invoice_sample.tex", dict(obj=invoice, products=products))
|
||||
|
||||
try:
|
||||
os.mkdir(BASE_DIR + "/tmp")
|
||||
except FileExistsError:
|
||||
pass
|
||||
# We render the file in a temporary directory
|
||||
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
|
||||
|
||||
try:
|
||||
with open("{}/invoice-{:d}.tex".format(tmp_dir, pk), "wb") as f:
|
||||
f.write(tex.encode("UTF-8"))
|
||||
del tex
|
||||
|
||||
# The file has to be rendered twice
|
||||
for _ in range(2):
|
||||
error = subprocess.Popen(
|
||||
["pdflatex", "invoice-{}.tex".format(pk)],
|
||||
cwd=tmp_dir,
|
||||
stdin=open(os.devnull, "r"),
|
||||
stderr=open(os.devnull, "wb"),
|
||||
stdout=open(os.devnull, "wb"),
|
||||
).wait()
|
||||
|
||||
if error:
|
||||
raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")")
|
||||
|
||||
# Display the generated pdf as a HTTP Response
|
||||
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk)
|
||||
except IOError as e:
|
||||
raise e
|
||||
finally:
|
||||
# Delete all temporary files
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class RemittanceCreateView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create Remittance
|
||||
"""
|
||||
model = Remittance
|
||||
form_class = RemittanceForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx["table"] = RemittanceTable(data=Remittance.objects.all())
|
||||
ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class RemittanceListView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
List existing Remittances
|
||||
"""
|
||||
template_name = "treasury/remittance_list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
|
||||
ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
|
||||
|
||||
ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
|
||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||
specialtransactionproxy__remittance=None).all(),
|
||||
exclude=('remittance_remove', ))
|
||||
ctx["special_transactions_with_remittance"] = SpecialTransactionTable(
|
||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||
specialtransactionproxy__remittance__closed=False).all(),
|
||||
exclude=('remittance_add', ))
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update Remittance
|
||||
"""
|
||||
model = Remittance
|
||||
form_class = RemittanceForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx["table"] = RemittanceTable(data=Remittance.objects.all())
|
||||
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all()
|
||||
ctx["special_transactions"] = SpecialTransactionTable(
|
||||
data=data,
|
||||
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Attach a special transaction to a remittance
|
||||
"""
|
||||
|
||||
model = SpecialTransactionProxy
|
||||
form_class = LinkTransactionToRemittanceForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
form = ctx["form"]
|
||||
form.fields["last_name"].initial = self.object.transaction.last_name
|
||||
form.fields["first_name"].initial = self.object.transaction.first_name
|
||||
form.fields["bank"].initial = self.object.transaction.bank
|
||||
form.fields["amount"].initial = self.object.transaction.amount
|
||||
form.fields["remittance"].queryset = form.fields["remittance"] \
|
||||
.queryset.filter(remittance_type__note=self.object.transaction.source)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Unlink a special transaction and its remittance
|
||||
"""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
pk = kwargs["pk"]
|
||||
transaction = SpecialTransactionProxy.objects.get(pk=pk)
|
||||
|
||||
# The remittance must be open (or inexistant)
|
||||
if transaction.remittance and transaction.remittance.closed:
|
||||
raise ValidationError("Remittance is already closed.")
|
||||
|
||||
transaction.remittance = None
|
||||
transaction.save()
|
||||
|
||||
return redirect('treasury:remittance_list')
|
|
@ -2,12 +2,17 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
if [ -z ${NOTE_URL+x} ]; then
|
||||
echo "Warning: your env files are not configurated."
|
||||
else
|
||||
sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json
|
||||
sed -i -e "s/localhost/$NOTE_URL/g" /code/note_kfet/fixtures/initial.json
|
||||
sed -i -e "s/\"\.\*\"/\"https?:\/\/$NOTE_URL\/.*\"/g" /code/note_kfet/fixtures/cas.json
|
||||
sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json
|
||||
fi
|
||||
|
||||
python manage.py compilemessages
|
||||
python manage.py makemigrations
|
||||
|
||||
# Wait for database
|
||||
sleep 5
|
||||
python manage.py migrate
|
||||
|
||||
# TODO: use uwsgi in production
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-02-27 17:39+0100\n"
|
||||
"POT-Creation-Date: 2020-03-24 15:49+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -23,9 +23,10 @@ msgid "activity"
|
|||
msgstr ""
|
||||
|
||||
#: apps/activity/models.py:19 apps/activity/models.py:44
|
||||
#: apps/member/models.py:60 apps/member/models.py:111
|
||||
#: apps/note/models/notes.py:184 apps/note/models/transactions.py:24
|
||||
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11
|
||||
#: apps/member/models.py:63 apps/member/models.py:114
|
||||
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
|
||||
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198
|
||||
#: templates/member/profile_detail.html:15
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -46,10 +47,11 @@ msgid "activity types"
|
|||
msgstr ""
|
||||
|
||||
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
|
||||
#: apps/permission/models.py:90
|
||||
msgid "description"
|
||||
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
|
||||
msgid "type"
|
||||
msgstr ""
|
||||
|
@ -82,227 +84,291 @@ msgstr ""
|
|||
msgid "guests"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/apps.py:10
|
||||
#: apps/api/apps.py:10
|
||||
msgid "API"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/apps.py:11
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:21 apps/note/models/notes.py:117
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:27
|
||||
msgid "IP Address"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:35
|
||||
msgid "model"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:42
|
||||
msgid "identifier"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:47
|
||||
msgid "previous data"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:52
|
||||
msgid "new data"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:60
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:61
|
||||
msgid "edit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:62
|
||||
msgid "delete"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:65
|
||||
msgid "action"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:73
|
||||
msgid "timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:77
|
||||
msgid "Logs cannot be destroyed."
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/apps.py:14
|
||||
msgid "member"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:23
|
||||
#: apps/member/models.py:25
|
||||
msgid "phone number"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:29 templates/member/profile_detail.html:24
|
||||
#: apps/member/models.py:31 templates/member/profile_detail.html:28
|
||||
msgid "section"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:30
|
||||
#: apps/member/models.py:32
|
||||
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:36 templates/member/profile_detail.html:27
|
||||
#: apps/member/models.py:38 templates/member/profile_detail.html:31
|
||||
msgid "address"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:42
|
||||
#: apps/member/models.py:44
|
||||
msgid "paid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:47 apps/member/models.py:48
|
||||
#: apps/member/models.py:49 apps/member/models.py:50
|
||||
msgid "user profile"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:65
|
||||
#: apps/member/models.py:68
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:70
|
||||
#: apps/member/models.py:73
|
||||
msgid "membership fee"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:74
|
||||
#: apps/member/models.py:77
|
||||
msgid "membership duration"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:75
|
||||
#: apps/member/models.py:78
|
||||
msgid "The longest time a membership can last (NULL = infinite)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:80
|
||||
#: apps/member/models.py:83
|
||||
msgid "membership start"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:81
|
||||
#: apps/member/models.py:84
|
||||
msgid "How long after January 1st the members can renew their membership."
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:86
|
||||
#: apps/member/models.py:89
|
||||
msgid "membership end"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:87
|
||||
#: apps/member/models.py:90
|
||||
msgid ""
|
||||
"How long the membership can last after January 1st of the next year after "
|
||||
"members can renew their membership."
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:93 apps/note/models/notes.py:135
|
||||
#: apps/member/models.py:96 apps/note/models/notes.py:139
|
||||
msgid "club"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:94
|
||||
#: apps/member/models.py:97
|
||||
msgid "clubs"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:117
|
||||
#: apps/member/models.py:120 apps/permission/models.py:275
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:118
|
||||
#: apps/member/models.py:121
|
||||
msgid "roles"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:142
|
||||
#: apps/member/models.py:145
|
||||
msgid "membership starts on"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:145
|
||||
#: apps/member/models.py:148
|
||||
msgid "membership ends on"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:149
|
||||
#: apps/member/models.py:152
|
||||
msgid "fee"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:153
|
||||
#: apps/member/models.py:162
|
||||
msgid "membership"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/models.py:154
|
||||
#: apps/member/models.py:163
|
||||
msgid "memberships"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/views.py:63 templates/member/profile_detail.html:42
|
||||
#: apps/member/views.py:80 templates/member/profile_detail.html:46
|
||||
msgid "Update Profile"
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/views.py:79
|
||||
#: apps/member/views.py:93
|
||||
msgid "An alias with a similar name already exists."
|
||||
msgstr ""
|
||||
|
||||
#: apps/member/views.py:130
|
||||
#: apps/member/views.py:146
|
||||
#, python-format
|
||||
msgid "Account #%(id)s: %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
|
||||
#: apps/member/views.py:216
|
||||
msgid "Alias successfully deleted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
|
||||
msgid "source"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/admin.py:128 apps/note/admin.py:156
|
||||
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
|
||||
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
|
||||
msgid "destination"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/apps.py:14 apps/note/models/notes.py:54
|
||||
#: apps/note/apps.py:14 apps/note/models/notes.py:58
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/forms.py:49
|
||||
msgid "Source and destination must be different."
|
||||
#: apps/note/forms.py:20
|
||||
msgid "New Alias"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:26
|
||||
msgid "account balance"
|
||||
#: apps/note/forms.py:25
|
||||
msgid "select an image"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/forms.py:26
|
||||
msgid "Maximal size: 2MB"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:27
|
||||
msgid "account balance"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:28
|
||||
msgid "in centimes, money credited for this instance"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:31
|
||||
#: apps/note/models/notes.py:32
|
||||
msgid "last negative date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:32
|
||||
#: apps/note/models/notes.py:33
|
||||
msgid "last time the balance was negative"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:37
|
||||
#: apps/note/models/notes.py:38
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:40
|
||||
#: apps/note/models/notes.py:41
|
||||
msgid ""
|
||||
"Designates whether this note should be treated as active. Unselect this "
|
||||
"instead of deleting notes."
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:44
|
||||
#: apps/note/models/notes.py:45
|
||||
msgid "display image"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
|
||||
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:55
|
||||
#: apps/note/models/notes.py:59
|
||||
msgid "notes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:63
|
||||
#: apps/note/models/notes.py:67
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97
|
||||
#: apps/note/models/notes.py:77 apps/note/models/notes.py:101
|
||||
msgid "This alias is already taken."
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:113
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:117
|
||||
#: apps/note/models/notes.py:121
|
||||
msgid "one's note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:118
|
||||
#: apps/note/models/notes.py:122
|
||||
msgid "users note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:124
|
||||
#: apps/note/models/notes.py:128
|
||||
#, python-format
|
||||
msgid "%(user)s's note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:139
|
||||
#: apps/note/models/notes.py:143
|
||||
msgid "club note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:140
|
||||
#: apps/note/models/notes.py:144
|
||||
msgid "clubs notes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:146
|
||||
#: apps/note/models/notes.py:150
|
||||
#, python-format
|
||||
msgid "Note of %(club)s club"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:166
|
||||
#: apps/note/models/notes.py:170
|
||||
msgid "special note"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:167
|
||||
#: apps/note/models/notes.py:171
|
||||
msgid "special notes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:190
|
||||
#: apps/note/models/notes.py:194
|
||||
msgid "Invalid alias"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:206
|
||||
#: apps/note/models/notes.py:210
|
||||
msgid "alias"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
|
||||
#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
|
||||
msgid "aliases"
|
||||
msgstr ""
|
||||
|
||||
|
@ -311,10 +377,10 @@ msgid "Alias is too long."
|
|||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:238
|
||||
msgid "An alias with a similar name already exists:"
|
||||
msgid "An alias with a similar name already exists: {} "
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/notes.py:246
|
||||
#: apps/note/models/notes.py:247
|
||||
msgid "You can't delete your main alias."
|
||||
msgstr ""
|
||||
|
||||
|
@ -330,7 +396,7 @@ msgstr ""
|
|||
msgid "A template with this name already exist"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
|
||||
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
|
||||
msgid "amount"
|
||||
msgstr ""
|
||||
|
||||
|
@ -338,59 +404,245 @@ msgstr ""
|
|||
msgid "in centimes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:74
|
||||
#: apps/note/models/transactions.py:75
|
||||
msgid "transaction template"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:75
|
||||
#: apps/note/models/transactions.py:76
|
||||
msgid "transaction templates"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:106
|
||||
#: apps/note/models/transactions.py:107
|
||||
msgid "quantity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:111
|
||||
#: apps/note/models/transactions.py:115
|
||||
msgid "reason"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:115
|
||||
#: apps/note/models/transactions.py:119
|
||||
msgid "valid"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:120
|
||||
#: apps/note/models/transactions.py:124
|
||||
msgid "transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:121
|
||||
#: apps/note/models/transactions.py:125
|
||||
msgid "transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:184
|
||||
#: apps/note/models/transactions.py:168 templates/base.html:98
|
||||
#: templates/note/transaction_form.html:19
|
||||
#: templates/note/transaction_form.html:145
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:188
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:203
|
||||
msgid "first_name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:208
|
||||
msgid "bank"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24
|
||||
msgid "Credit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28
|
||||
msgid "Debit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235
|
||||
msgid "membership transaction"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/models/transactions.py:185
|
||||
#: apps/note/models/transactions.py:231
|
||||
msgid "membership transactions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/views.py:29
|
||||
msgid "Transfer money from your account to one or others"
|
||||
#: apps/note/views.py:39
|
||||
msgid "Transfer money"
|
||||
msgstr ""
|
||||
|
||||
#: apps/note/views.py:138
|
||||
msgid "Consommations"
|
||||
#: apps/note/views.py:145 templates/base.html:79
|
||||
msgid "Consumptions"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:155
|
||||
#: apps/permission/models.py:69 apps/permission/models.py:262
|
||||
#, python-brace-format
|
||||
msgid "Can {type} {model}.{field} in {query}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/permission/models.py:71 apps/permission/models.py:264
|
||||
#, python-brace-format
|
||||
msgid "Can {type} {model} in {query}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/permission/models.py:84
|
||||
msgid "rank"
|
||||
msgstr ""
|
||||
|
||||
#: apps/permission/models.py:147
|
||||
msgid "Specifying field applies only to view and change permission types."
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/apps.py:11 templates/base.html:102
|
||||
msgid "Treasury"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:56 apps/treasury/forms.py:95
|
||||
#: templates/django_filters/rest_framework/form.html:5
|
||||
#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:58
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:65
|
||||
msgid "Remittance is already closed."
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:70
|
||||
msgid "You can't change the type of the remittance."
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:84
|
||||
msgid "Last name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98
|
||||
msgid "Bank"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/forms.py:90 apps/treasury/tables.py:40
|
||||
#: templates/note/transaction_form.html:128
|
||||
#: templates/treasury/remittance_form.html:18
|
||||
msgid "Amount"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:18
|
||||
msgid "Invoice identifier"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:32
|
||||
msgid "BDE"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:37
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:41
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:46 templates/note/transaction_form.html:86
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:50
|
||||
msgid "Address"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:55
|
||||
msgid "Place"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:59
|
||||
msgid "Acquitted"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:75
|
||||
msgid "Designation"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:79
|
||||
msgid "Quantity"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:83
|
||||
msgid "Unit price"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:120
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:126
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:131
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:136
|
||||
msgid "Closed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:159
|
||||
msgid "Remittance #{:d}: {}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/models.py:178 apps/treasury/tables.py:64
|
||||
#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13
|
||||
#: templates/treasury/remittance_list.html:13
|
||||
msgid "Remittance"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:16
|
||||
msgid "Invoice #{:d}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10
|
||||
#: templates/treasury/remittance_list.html:10
|
||||
msgid "Invoice"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:38
|
||||
msgid "Transaction count"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:43 apps/treasury/tables.py:45
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:66
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/tables.py:74
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/__init__.py:63
|
||||
msgid ""
|
||||
"The Central Authentication Service grants you access to most of our websites "
|
||||
"by authenticating only once, so you don't need to type your credentials "
|
||||
"again unless your session expires or you logout."
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:153
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:156
|
||||
#: note_kfet/settings/base.py:154
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:157
|
||||
#: note_kfet/settings/base.py:155
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
|
@ -398,6 +650,73 @@ msgstr ""
|
|||
msgid "The ENS Paris-Saclay BDE note."
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:84
|
||||
msgid "Clubs"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:89
|
||||
msgid "Activities"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:94
|
||||
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/member/club_detail.html:10
|
||||
msgid "Membership starts on"
|
||||
msgstr ""
|
||||
|
@ -410,10 +729,22 @@ msgstr ""
|
|||
msgid "Membership duration"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
|
||||
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34
|
||||
msgid "balance"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75
|
||||
msgid "Transaction history"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/club_form.html:6
|
||||
msgid "Clubs list"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/club_list.html:8
|
||||
msgid "New club"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/manage_auth_tokens.html:16
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
@ -426,27 +757,35 @@ msgstr ""
|
|||
msgid "Regenerate token"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:11
|
||||
#: templates/member/profile_alias.html:10
|
||||
msgid "Add alias"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:15
|
||||
msgid "first name"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:14
|
||||
#: templates/member/profile_detail.html:18
|
||||
msgid "username"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:17
|
||||
#: templates/member/profile_detail.html:21
|
||||
msgid "password"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:20
|
||||
#: templates/member/profile_detail.html:24
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:38
|
||||
#: templates/member/profile_detail.html:42
|
||||
msgid "Manage auth token"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:54
|
||||
#: templates/member/profile_detail.html:49
|
||||
msgid "View Profile"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/profile_detail.html:62
|
||||
msgid "View my memberships"
|
||||
msgstr ""
|
||||
|
||||
|
@ -454,12 +793,75 @@ msgstr ""
|
|||
msgid "Save Changes"
|
||||
msgstr ""
|
||||
|
||||
#: templates/member/signup.html:5 templates/member/signup.html:8
|
||||
#: templates/member/signup.html:14
|
||||
msgid "Sign Up"
|
||||
msgid "Sign up"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:35
|
||||
msgid "Transfer"
|
||||
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:40
|
||||
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 templates/note/transaction_form.html:152
|
||||
msgid "Recent transactions history"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:15
|
||||
msgid "Gift"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:68
|
||||
msgid "External payment"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:76
|
||||
msgid "Transfer type"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:111
|
||||
#: templates/note/transaction_form.html:169
|
||||
#: templates/note/transaction_form.html:176
|
||||
msgid "Select receivers"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:138
|
||||
msgid "Reason"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:183
|
||||
msgid "Credit note"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transaction_form.html:190
|
||||
msgid "Debit note"
|
||||
msgstr ""
|
||||
|
||||
#: templates/note/transactiontemplate_form.html:6
|
||||
msgid "Buttons list"
|
||||
msgstr ""
|
||||
|
||||
#: templates/registration/logged_out.html:8
|
||||
|
@ -471,7 +873,7 @@ msgid "Log in again"
|
|||
msgstr ""
|
||||
|
||||
#: templates/registration/login.html:7 templates/registration/login.html:8
|
||||
#: templates/registration/login.html:22
|
||||
#: templates/registration/login.html:26
|
||||
#: templates/registration/password_reset_complete.html:10
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
@ -483,7 +885,7 @@ msgid ""
|
|||
"page. Would you like to login to a different account?"
|
||||
msgstr ""
|
||||
|
||||
#: templates/registration/login.html:23
|
||||
#: templates/registration/login.html:27
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -539,3 +941,72 @@ msgstr ""
|
|||
#: templates/registration/password_reset_form.html:11
|
||||
msgid "Reset my password"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/invoice_form.html:6
|
||||
msgid "Invoices list"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/invoice_form.html:42
|
||||
msgid "Add product"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/invoice_form.html:43
|
||||
msgid "Remove product"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/invoice_list.html:21
|
||||
msgid "New invoice"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_form.html:7
|
||||
msgid "Remittance #"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_form.html:9
|
||||
#: templates/treasury/specialtransactionproxy_form.html:7
|
||||
msgid "Remittances list"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_form.html:12
|
||||
msgid "Count"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_form.html:29
|
||||
msgid "Linked transactions"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_form.html:34
|
||||
msgid "There is no transaction linked with this remittance."
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:19
|
||||
msgid "Opened remittances"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:24
|
||||
msgid "There is no opened remittance."
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:28
|
||||
msgid "New remittance"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:32
|
||||
msgid "Transfers without remittances"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:37
|
||||
msgid "There is no transaction without any linked remittance."
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:43
|
||||
msgid "Transfers with opened remittances"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:48
|
||||
msgid "There is no transaction with an opened linked remittance."
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/remittance_list.html:54
|
||||
msgid "Closed remittances"
|
||||
msgstr ""
|
||||
|
|
|
@ -3,7 +3,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-02-27 17:39+0100\n"
|
||||
"POT-Creation-Date: 2020-03-24 15:49+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -18,9 +18,10 @@ msgid "activity"
|
|||
msgstr "activité"
|
||||
|
||||
#: apps/activity/models.py:19 apps/activity/models.py:44
|
||||
#: apps/member/models.py:60 apps/member/models.py:111
|
||||
#: apps/note/models/notes.py:184 apps/note/models/transactions.py:24
|
||||
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11
|
||||
#: apps/member/models.py:63 apps/member/models.py:114
|
||||
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
|
||||
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198
|
||||
#: templates/member/profile_detail.html:15
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
|
@ -41,10 +42,11 @@ msgid "activity types"
|
|||
msgstr "types d'activité"
|
||||
|
||||
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
|
||||
#: apps/permission/models.py:90
|
||||
msgid "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
|
||||
msgid "type"
|
||||
msgstr "type"
|
||||
|
@ -77,65 +79,121 @@ msgstr "invité"
|
|||
msgid "guests"
|
||||
msgstr "invités"
|
||||
|
||||
#: apps/member/apps.py:10
|
||||
#: apps/api/apps.py:10
|
||||
msgid "API"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/apps.py:11
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: apps/logs/models.py:21 apps/note/models/notes.py:117
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: apps/logs/models.py:27
|
||||
msgid "IP Address"
|
||||
msgstr "Adresse IP"
|
||||
|
||||
#: apps/logs/models.py:35
|
||||
msgid "model"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: apps/logs/models.py:42
|
||||
msgid "identifier"
|
||||
msgstr "Identifiant"
|
||||
|
||||
#: apps/logs/models.py:47
|
||||
msgid "previous data"
|
||||
msgstr "Données précédentes"
|
||||
|
||||
#: apps/logs/models.py:52
|
||||
msgid "new data"
|
||||
msgstr "Nouvelles données"
|
||||
|
||||
#: apps/logs/models.py:60
|
||||
msgid "create"
|
||||
msgstr "Créer"
|
||||
|
||||
#: apps/logs/models.py:61
|
||||
msgid "edit"
|
||||
msgstr "Modifier"
|
||||
|
||||
#: apps/logs/models.py:62
|
||||
msgid "delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: apps/logs/models.py:65
|
||||
msgid "action"
|
||||
msgstr "Action"
|
||||
|
||||
#: apps/logs/models.py:73
|
||||
msgid "timestamp"
|
||||
msgstr "Date"
|
||||
|
||||
#: apps/logs/models.py:77
|
||||
msgid "Logs cannot be destroyed."
|
||||
msgstr "Les logs ne peuvent pas être détruits."
|
||||
|
||||
#: apps/member/apps.py:14
|
||||
msgid "member"
|
||||
msgstr "adhérent"
|
||||
|
||||
#: apps/member/models.py:23
|
||||
#: apps/member/models.py:25
|
||||
msgid "phone number"
|
||||
msgstr "numéro de téléphone"
|
||||
|
||||
#: apps/member/models.py:29 templates/member/profile_detail.html:24
|
||||
#: apps/member/models.py:31 templates/member/profile_detail.html:28
|
||||
msgid "section"
|
||||
msgstr "section"
|
||||
|
||||
#: apps/member/models.py:30
|
||||
#: apps/member/models.py:32
|
||||
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
||||
msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
||||
|
||||
#: apps/member/models.py:36 templates/member/profile_detail.html:27
|
||||
#: apps/member/models.py:38 templates/member/profile_detail.html:31
|
||||
msgid "address"
|
||||
msgstr "adresse"
|
||||
|
||||
#: apps/member/models.py:42
|
||||
#: apps/member/models.py:44
|
||||
msgid "paid"
|
||||
msgstr "payé"
|
||||
|
||||
#: apps/member/models.py:47 apps/member/models.py:48
|
||||
#: apps/member/models.py:49 apps/member/models.py:50
|
||||
msgid "user profile"
|
||||
msgstr "profil utilisateur"
|
||||
|
||||
#: apps/member/models.py:65
|
||||
#: apps/member/models.py:68
|
||||
msgid "email"
|
||||
msgstr "courriel"
|
||||
|
||||
#: apps/member/models.py:70
|
||||
#: apps/member/models.py:73
|
||||
msgid "membership fee"
|
||||
msgstr "cotisation pour adhérer"
|
||||
|
||||
#: apps/member/models.py:74
|
||||
#: apps/member/models.py:77
|
||||
msgid "membership duration"
|
||||
msgstr "durée de l'adhésion"
|
||||
|
||||
#: apps/member/models.py:75
|
||||
#: apps/member/models.py:78
|
||||
msgid "The longest time a membership can last (NULL = infinite)."
|
||||
msgstr "La durée maximale d'une adhésion (NULL = infinie)."
|
||||
|
||||
#: apps/member/models.py:80
|
||||
#: apps/member/models.py:83
|
||||
msgid "membership start"
|
||||
msgstr "début de l'adhésion"
|
||||
|
||||
#: apps/member/models.py:81
|
||||
#: apps/member/models.py:84
|
||||
msgid "How long after January 1st the members can renew their membership."
|
||||
msgstr ""
|
||||
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
|
||||
"adhésion."
|
||||
|
||||
#: apps/member/models.py:86
|
||||
#: apps/member/models.py:89
|
||||
msgid "membership end"
|
||||
msgstr "fin de l'adhésion"
|
||||
|
||||
#: apps/member/models.py:87
|
||||
#: apps/member/models.py:90
|
||||
msgid ""
|
||||
"How long the membership can last after January 1st of the next year after "
|
||||
"members can renew their membership."
|
||||
|
@ -143,166 +201,174 @@ msgstr ""
|
|||
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
|
||||
"suivante avant que les adhérents peuvent renouveler leur adhésion."
|
||||
|
||||
#: apps/member/models.py:93 apps/note/models/notes.py:135
|
||||
#: apps/member/models.py:96 apps/note/models/notes.py:139
|
||||
msgid "club"
|
||||
msgstr "club"
|
||||
|
||||
#: apps/member/models.py:94
|
||||
#: apps/member/models.py:97
|
||||
msgid "clubs"
|
||||
msgstr "clubs"
|
||||
|
||||
#: apps/member/models.py:117
|
||||
#: apps/member/models.py:120 apps/permission/models.py:275
|
||||
msgid "role"
|
||||
msgstr "rôle"
|
||||
|
||||
#: apps/member/models.py:118
|
||||
#: apps/member/models.py:121
|
||||
msgid "roles"
|
||||
msgstr "rôles"
|
||||
|
||||
#: apps/member/models.py:142
|
||||
#: apps/member/models.py:145
|
||||
msgid "membership starts on"
|
||||
msgstr "l'adhésion commence le"
|
||||
|
||||
#: apps/member/models.py:145
|
||||
#: apps/member/models.py:148
|
||||
msgid "membership ends on"
|
||||
msgstr "l'adhésion finie le"
|
||||
|
||||
#: apps/member/models.py:149
|
||||
#: apps/member/models.py:152
|
||||
msgid "fee"
|
||||
msgstr "cotisation"
|
||||
|
||||
#: apps/member/models.py:153
|
||||
#: apps/member/models.py:162
|
||||
msgid "membership"
|
||||
msgstr "adhésion"
|
||||
|
||||
#: apps/member/models.py:154
|
||||
#: apps/member/models.py:163
|
||||
msgid "memberships"
|
||||
msgstr "adhésions"
|
||||
|
||||
#: apps/member/views.py:63 templates/member/profile_detail.html:42
|
||||
#: apps/member/views.py:80 templates/member/profile_detail.html:46
|
||||
msgid "Update Profile"
|
||||
msgstr "Modifier le profil"
|
||||
|
||||
#: apps/member/views.py:79
|
||||
#: apps/member/views.py:93
|
||||
msgid "An alias with a similar name already exists."
|
||||
msgstr "Un alias avec un nom similaire existe déjà."
|
||||
|
||||
#: apps/member/views.py:130
|
||||
#: apps/member/views.py:146
|
||||
#, python-format
|
||||
msgid "Account #%(id)s: %(username)s"
|
||||
msgstr "Compte n°%(id)s : %(username)s"
|
||||
|
||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
|
||||
#: apps/member/views.py:216
|
||||
msgid "Alias successfully deleted"
|
||||
msgstr "L'alias a bien été supprimé"
|
||||
|
||||
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
|
||||
msgid "source"
|
||||
msgstr "source"
|
||||
|
||||
#: apps/note/admin.py:128 apps/note/admin.py:156
|
||||
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
|
||||
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
|
||||
msgid "destination"
|
||||
msgstr "destination"
|
||||
|
||||
#: apps/note/apps.py:14 apps/note/models/notes.py:54
|
||||
#: apps/note/apps.py:14 apps/note/models/notes.py:58
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#: apps/note/forms.py:49
|
||||
msgid "Source and destination must be different."
|
||||
msgstr "La source et la destination doivent être différentes."
|
||||
#: apps/note/forms.py:20
|
||||
msgid "New Alias"
|
||||
msgstr "Nouvel alias"
|
||||
|
||||
#: apps/note/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"
|
||||
msgstr "solde du compte"
|
||||
|
||||
#: apps/note/models/notes.py:27
|
||||
#: apps/note/models/notes.py:28
|
||||
msgid "in centimes, money credited for this instance"
|
||||
msgstr "en centimes, argent crédité pour cette instance"
|
||||
|
||||
#: apps/note/models/notes.py:31
|
||||
#: apps/note/models/notes.py:32
|
||||
msgid "last negative date"
|
||||
msgstr "dernier date de négatif"
|
||||
|
||||
#: apps/note/models/notes.py:32
|
||||
#: apps/note/models/notes.py:33
|
||||
msgid "last time the balance was negative"
|
||||
msgstr "dernier instant où la note était en négatif"
|
||||
|
||||
#: apps/note/models/notes.py:37
|
||||
#: apps/note/models/notes.py:38
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: apps/note/models/notes.py:40
|
||||
#: apps/note/models/notes.py:41
|
||||
msgid ""
|
||||
"Designates whether this note should be treated as active. Unselect this "
|
||||
"instead of deleting notes."
|
||||
msgstr ""
|
||||
"Indique si la note est active. Désactiver cela plutôt que supprimer la note."
|
||||
|
||||
#: apps/note/models/notes.py:44
|
||||
#: apps/note/models/notes.py:45
|
||||
msgid "display image"
|
||||
msgstr "image affichée"
|
||||
|
||||
#: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
|
||||
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
|
||||
msgid "created at"
|
||||
msgstr "créée le"
|
||||
|
||||
#: apps/note/models/notes.py:55
|
||||
#: apps/note/models/notes.py:59
|
||||
msgid "notes"
|
||||
msgstr "notes"
|
||||
|
||||
#: apps/note/models/notes.py:63
|
||||
#: apps/note/models/notes.py:67
|
||||
msgid "Note"
|
||||
msgstr "Note"
|
||||
|
||||
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97
|
||||
#: apps/note/models/notes.py:77 apps/note/models/notes.py:101
|
||||
msgid "This alias is already taken."
|
||||
msgstr "Cet alias est déjà pris."
|
||||
|
||||
#: apps/note/models/notes.py:113
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: apps/note/models/notes.py:117
|
||||
#: apps/note/models/notes.py:121
|
||||
msgid "one's note"
|
||||
msgstr "note d'un utilisateur"
|
||||
|
||||
#: apps/note/models/notes.py:118
|
||||
#: apps/note/models/notes.py:122
|
||||
msgid "users note"
|
||||
msgstr "notes des utilisateurs"
|
||||
|
||||
#: apps/note/models/notes.py:124
|
||||
#: apps/note/models/notes.py:128
|
||||
#, python-format
|
||||
msgid "%(user)s's note"
|
||||
msgstr "Note de %(user)s"
|
||||
|
||||
#: apps/note/models/notes.py:139
|
||||
#: apps/note/models/notes.py:143
|
||||
msgid "club note"
|
||||
msgstr "note d'un club"
|
||||
|
||||
#: apps/note/models/notes.py:140
|
||||
#: apps/note/models/notes.py:144
|
||||
msgid "clubs notes"
|
||||
msgstr "notes des clubs"
|
||||
|
||||
#: apps/note/models/notes.py:146
|
||||
#: apps/note/models/notes.py:150
|
||||
#, python-format
|
||||
msgid "Note of %(club)s club"
|
||||
msgstr "Note du club %(club)s"
|
||||
|
||||
#: apps/note/models/notes.py:166
|
||||
#: apps/note/models/notes.py:170
|
||||
msgid "special note"
|
||||
msgstr "note spéciale"
|
||||
|
||||
#: apps/note/models/notes.py:167
|
||||
#: apps/note/models/notes.py:171
|
||||
msgid "special notes"
|
||||
msgstr "notes spéciales"
|
||||
|
||||
#: apps/note/models/notes.py:190
|
||||
#: apps/note/models/notes.py:194
|
||||
msgid "Invalid alias"
|
||||
msgstr "Alias invalide"
|
||||
|
||||
#: apps/note/models/notes.py:206
|
||||
#: apps/note/models/notes.py:210
|
||||
msgid "alias"
|
||||
msgstr "alias"
|
||||
|
||||
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
|
||||
#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37
|
||||
msgid "aliases"
|
||||
msgstr "alias"
|
||||
|
||||
|
@ -311,10 +377,10 @@ msgid "Alias is too long."
|
|||
msgstr "L'alias est trop long."
|
||||
|
||||
#: apps/note/models/notes.py:238
|
||||
msgid "An alias with a similar name already exists:"
|
||||
msgstr "Un alias avec un nom similaire existe déjà."
|
||||
msgid "An alias with a similar name already exists: {} "
|
||||
msgstr "Un alias avec un nom similaire existe déjà : {}"
|
||||
|
||||
#: apps/note/models/notes.py:246
|
||||
#: apps/note/models/notes.py:247
|
||||
msgid "You can't delete your main alias."
|
||||
msgstr "Vous ne pouvez pas supprimer votre alias principal."
|
||||
|
||||
|
@ -327,11 +393,10 @@ msgid "transaction categories"
|
|||
msgstr "catégories de transaction"
|
||||
|
||||
#: apps/note/models/transactions.py:47
|
||||
#, fuzzy
|
||||
msgid "A template with this name already exist"
|
||||
msgstr "Un modèle de transaction avec un nom similaire existe déjà."
|
||||
|
||||
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
|
||||
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
|
||||
msgid "amount"
|
||||
msgstr "montant"
|
||||
|
||||
|
@ -339,59 +404,245 @@ msgstr "montant"
|
|||
msgid "in centimes"
|
||||
msgstr "en centimes"
|
||||
|
||||
#: apps/note/models/transactions.py:74
|
||||
#: apps/note/models/transactions.py:75
|
||||
msgid "transaction template"
|
||||
msgstr "modèle de transaction"
|
||||
|
||||
#: apps/note/models/transactions.py:75
|
||||
#: apps/note/models/transactions.py:76
|
||||
msgid "transaction templates"
|
||||
msgstr "modèles de transaction"
|
||||
|
||||
#: apps/note/models/transactions.py:106
|
||||
#: apps/note/models/transactions.py:107
|
||||
msgid "quantity"
|
||||
msgstr "quantité"
|
||||
|
||||
#: apps/note/models/transactions.py:111
|
||||
#: apps/note/models/transactions.py:115
|
||||
msgid "reason"
|
||||
msgstr "raison"
|
||||
|
||||
#: apps/note/models/transactions.py:115
|
||||
#: apps/note/models/transactions.py:119
|
||||
msgid "valid"
|
||||
msgstr "valide"
|
||||
|
||||
#: apps/note/models/transactions.py:120
|
||||
#: apps/note/models/transactions.py:124
|
||||
msgid "transaction"
|
||||
msgstr "transaction"
|
||||
|
||||
#: apps/note/models/transactions.py:121
|
||||
#: apps/note/models/transactions.py:125
|
||||
msgid "transactions"
|
||||
msgstr "transactions"
|
||||
|
||||
#: apps/note/models/transactions.py:184
|
||||
#: apps/note/models/transactions.py:168 templates/base.html:98
|
||||
#: templates/note/transaction_form.html:19
|
||||
#: templates/note/transaction_form.html:145
|
||||
msgid "Transfer"
|
||||
msgstr "Virement"
|
||||
|
||||
#: apps/note/models/transactions.py:188
|
||||
msgid "Template"
|
||||
msgstr "Bouton"
|
||||
|
||||
#: apps/note/models/transactions.py:203
|
||||
msgid "first_name"
|
||||
msgstr "prénom"
|
||||
|
||||
#: apps/note/models/transactions.py:208
|
||||
msgid "bank"
|
||||
msgstr "banque"
|
||||
|
||||
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24
|
||||
msgid "Credit"
|
||||
msgstr "Crédit"
|
||||
|
||||
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28
|
||||
msgid "Debit"
|
||||
msgstr "Débit"
|
||||
|
||||
#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235
|
||||
msgid "membership transaction"
|
||||
msgstr "transaction d'adhésion"
|
||||
|
||||
#: apps/note/models/transactions.py:185
|
||||
#: apps/note/models/transactions.py:231
|
||||
msgid "membership transactions"
|
||||
msgstr "transactions d'adhésion"
|
||||
|
||||
#: apps/note/views.py:29
|
||||
msgid "Transfer money from your account to one or others"
|
||||
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
|
||||
#: apps/note/views.py:39
|
||||
msgid "Transfer money"
|
||||
msgstr "Transférer de l'argent"
|
||||
|
||||
#: apps/note/views.py:138
|
||||
msgid "Consommations"
|
||||
msgstr "transactions"
|
||||
#: apps/note/views.py:145 templates/base.html:79
|
||||
msgid "Consumptions"
|
||||
msgstr "Consommations"
|
||||
|
||||
#: note_kfet/settings/base.py:155
|
||||
#: apps/permission/models.py:69 apps/permission/models.py:262
|
||||
#, python-brace-format
|
||||
msgid "Can {type} {model}.{field} in {query}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/permission/models.py:71 apps/permission/models.py:264
|
||||
#, python-brace-format
|
||||
msgid "Can {type} {model} in {query}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/permission/models.py:84
|
||||
msgid "rank"
|
||||
msgstr "Rang"
|
||||
|
||||
#: apps/permission/models.py:147
|
||||
msgid "Specifying field applies only to view and change permission types."
|
||||
msgstr ""
|
||||
|
||||
#: apps/treasury/apps.py:11 templates/base.html:102
|
||||
msgid "Treasury"
|
||||
msgstr "Trésorerie"
|
||||
|
||||
#: apps/treasury/forms.py:56 apps/treasury/forms.py:95
|
||||
#: templates/django_filters/rest_framework/form.html:5
|
||||
#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47
|
||||
msgid "Submit"
|
||||
msgstr "Envoyer"
|
||||
|
||||
#: apps/treasury/forms.py:58
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
#: apps/treasury/forms.py:65
|
||||
msgid "Remittance is already closed."
|
||||
msgstr "La remise est déjà fermée."
|
||||
|
||||
#: apps/treasury/forms.py:70
|
||||
msgid "You can't change the type of the remittance."
|
||||
msgstr "Vous ne pouvez pas changer le type de la remise."
|
||||
|
||||
#: apps/treasury/forms.py:84
|
||||
msgid "Last name"
|
||||
msgstr "Nom de famille"
|
||||
|
||||
#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92
|
||||
msgid "First name"
|
||||
msgstr "Prénom"
|
||||
|
||||
#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98
|
||||
msgid "Bank"
|
||||
msgstr "Banque"
|
||||
|
||||
#: apps/treasury/forms.py:90 apps/treasury/tables.py:40
|
||||
#: templates/note/transaction_form.html:128
|
||||
#: templates/treasury/remittance_form.html:18
|
||||
msgid "Amount"
|
||||
msgstr "Montant"
|
||||
|
||||
#: apps/treasury/models.py:18
|
||||
msgid "Invoice identifier"
|
||||
msgstr "Numéro de facture"
|
||||
|
||||
#: apps/treasury/models.py:32
|
||||
msgid "BDE"
|
||||
msgstr "BDE"
|
||||
|
||||
#: apps/treasury/models.py:37
|
||||
msgid "Object"
|
||||
msgstr "Objet"
|
||||
|
||||
#: apps/treasury/models.py:41
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
#: apps/treasury/models.py:46 templates/note/transaction_form.html:86
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: apps/treasury/models.py:50
|
||||
msgid "Address"
|
||||
msgstr "Adresse"
|
||||
|
||||
#: apps/treasury/models.py:55
|
||||
msgid "Place"
|
||||
msgstr "Lieu"
|
||||
|
||||
#: apps/treasury/models.py:59
|
||||
msgid "Acquitted"
|
||||
msgstr "Acquittée"
|
||||
|
||||
#: apps/treasury/models.py:75
|
||||
msgid "Designation"
|
||||
msgstr "Désignation"
|
||||
|
||||
#: apps/treasury/models.py:79
|
||||
msgid "Quantity"
|
||||
msgstr "Quantité"
|
||||
|
||||
#: apps/treasury/models.py:83
|
||||
msgid "Unit price"
|
||||
msgstr "Prix unitaire"
|
||||
|
||||
#: apps/treasury/models.py:120
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
#: apps/treasury/models.py:126
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: apps/treasury/models.py:131
|
||||
msgid "Comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: apps/treasury/models.py:136
|
||||
msgid "Closed"
|
||||
msgstr "Fermée"
|
||||
|
||||
#: apps/treasury/models.py:159
|
||||
msgid "Remittance #{:d}: {}"
|
||||
msgstr "Remise n°{:d} : {}"
|
||||
|
||||
#: apps/treasury/models.py:178 apps/treasury/tables.py:64
|
||||
#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13
|
||||
#: templates/treasury/remittance_list.html:13
|
||||
msgid "Remittance"
|
||||
msgstr "Remise"
|
||||
|
||||
#: apps/treasury/tables.py:16
|
||||
msgid "Invoice #{:d}"
|
||||
msgstr "Facture n°{:d}"
|
||||
|
||||
#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10
|
||||
#: templates/treasury/remittance_list.html:10
|
||||
msgid "Invoice"
|
||||
msgstr "Facture"
|
||||
|
||||
#: apps/treasury/tables.py:38
|
||||
msgid "Transaction count"
|
||||
msgstr "Nombre de transactions"
|
||||
|
||||
#: apps/treasury/tables.py:43 apps/treasury/tables.py:45
|
||||
msgid "View"
|
||||
msgstr "Voir"
|
||||
|
||||
#: apps/treasury/tables.py:66
|
||||
msgid "Add"
|
||||
msgstr "Ajouter"
|
||||
|
||||
#: apps/treasury/tables.py:74
|
||||
msgid "Remove"
|
||||
msgstr "supprimer"
|
||||
|
||||
#: note_kfet/settings/__init__.py:63
|
||||
msgid ""
|
||||
"The Central Authentication Service grants you access to most of our websites "
|
||||
"by authenticating only once, so you don't need to type your credentials "
|
||||
"again unless your session expires or you logout."
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:153
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:156
|
||||
#: note_kfet/settings/base.py:154
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: note_kfet/settings/base.py:157
|
||||
#: note_kfet/settings/base.py:155
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
|
@ -399,6 +650,75 @@ msgstr ""
|
|||
msgid "The ENS Paris-Saclay BDE note."
|
||||
msgstr "La note du BDE de l'ENS Paris-Saclay."
|
||||
|
||||
#: templates/base.html:84
|
||||
msgid "Clubs"
|
||||
msgstr "Clubs"
|
||||
|
||||
#: templates/base.html:89
|
||||
msgid "Activities"
|
||||
msgstr "Activités"
|
||||
|
||||
#: templates/base.html:94
|
||||
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/member/club_detail.html:10
|
||||
msgid "Membership starts on"
|
||||
msgstr "L'adhésion commence le"
|
||||
|
@ -411,10 +731,22 @@ msgstr "L'adhésion finie le"
|
|||
msgid "Membership duration"
|
||||
msgstr "Durée de l'adhésion"
|
||||
|
||||
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
|
||||
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34
|
||||
msgid "balance"
|
||||
msgstr "solde du compte"
|
||||
|
||||
#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75
|
||||
msgid "Transaction history"
|
||||
msgstr "Historique des transactions"
|
||||
|
||||
#: templates/member/club_form.html:6
|
||||
msgid "Clubs list"
|
||||
msgstr "Liste des clubs"
|
||||
|
||||
#: templates/member/club_list.html:8
|
||||
msgid "New club"
|
||||
msgstr "Nouveau club"
|
||||
|
||||
#: templates/member/manage_auth_tokens.html:16
|
||||
msgid "Token"
|
||||
msgstr "Jeton"
|
||||
|
@ -427,33 +759,35 @@ msgstr "Créé le"
|
|||
msgid "Regenerate token"
|
||||
msgstr "Regénérer le jeton"
|
||||
|
||||
#: templates/member/profile_detail.html:11
|
||||
#: templates/member/profile_alias.html:10
|
||||
msgid "Add alias"
|
||||
msgstr "Ajouter un alias"
|
||||
|
||||
#: templates/member/profile_detail.html:15
|
||||
msgid "first name"
|
||||
msgstr ""
|
||||
msgstr "prénom"
|
||||
|
||||
#: templates/member/profile_detail.html:14
|
||||
#: templates/member/profile_detail.html:18
|
||||
msgid "username"
|
||||
msgstr ""
|
||||
msgstr "pseudo"
|
||||
|
||||
#: templates/member/profile_detail.html:17
|
||||
#, fuzzy
|
||||
#| msgid "Change password"
|
||||
#: templates/member/profile_detail.html:21
|
||||
msgid "password"
|
||||
msgstr ""
|
||||
msgstr "mot de passe"
|
||||
|
||||
#: templates/member/profile_detail.html:20
|
||||
#: templates/member/profile_detail.html:24
|
||||
msgid "Change password"
|
||||
msgstr "Changer le mot de passe"
|
||||
|
||||
#: templates/member/profile_detail.html:38
|
||||
#: templates/member/profile_detail.html:42
|
||||
msgid "Manage auth token"
|
||||
msgstr "Gérer les jetons d'authentification"
|
||||
|
||||
#: templates/member/profile_detail.html:51
|
||||
msgid "Transaction history"
|
||||
msgstr "Historique des transactions"
|
||||
#: templates/member/profile_detail.html:49
|
||||
msgid "View Profile"
|
||||
msgstr "Voir le profil"
|
||||
|
||||
#: templates/member/profile_detail.html:54
|
||||
#: templates/member/profile_detail.html:62
|
||||
msgid "View my memberships"
|
||||
msgstr "Voir mes adhésions"
|
||||
|
||||
|
@ -461,13 +795,76 @@ msgstr "Voir mes adhésions"
|
|||
msgid "Save Changes"
|
||||
msgstr "Sauvegarder les changements"
|
||||
|
||||
#: templates/member/signup.html:5 templates/member/signup.html:8
|
||||
#: templates/member/signup.html:14
|
||||
msgid "Sign Up"
|
||||
msgstr ""
|
||||
msgid "Sign up"
|
||||
msgstr "Inscription"
|
||||
|
||||
#: templates/note/transaction_form.html:35
|
||||
msgid "Transfer"
|
||||
msgstr "Virement"
|
||||
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:40
|
||||
msgid "Select emitters"
|
||||
msgstr "Sélection des émetteurs"
|
||||
|
||||
#: templates/note/conso_form.html:45
|
||||
msgid "Select consumptions"
|
||||
msgstr "Sélection des 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 "Consommations simples"
|
||||
|
||||
#: templates/note/conso_form.html:130
|
||||
msgid "Double consumptions"
|
||||
msgstr "Consommations doubles"
|
||||
|
||||
#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152
|
||||
msgid "Recent transactions history"
|
||||
msgstr "Historique des transactions récentes"
|
||||
|
||||
#: templates/note/transaction_form.html:15
|
||||
msgid "Gift"
|
||||
msgstr "Don"
|
||||
|
||||
#: templates/note/transaction_form.html:68
|
||||
msgid "External payment"
|
||||
msgstr "Paiement externe"
|
||||
|
||||
#: templates/note/transaction_form.html:76
|
||||
msgid "Transfer type"
|
||||
msgstr "Type de transfert"
|
||||
|
||||
#: templates/note/transaction_form.html:111
|
||||
#: templates/note/transaction_form.html:169
|
||||
#: templates/note/transaction_form.html:176
|
||||
msgid "Select receivers"
|
||||
msgstr "Sélection des destinataires"
|
||||
|
||||
#: templates/note/transaction_form.html:138
|
||||
msgid "Reason"
|
||||
msgstr "Raison"
|
||||
|
||||
#: templates/note/transaction_form.html:183
|
||||
msgid "Credit note"
|
||||
msgstr "Note à recharger"
|
||||
|
||||
#: templates/note/transaction_form.html:190
|
||||
msgid "Debit note"
|
||||
msgstr "Note à débiter"
|
||||
|
||||
#: templates/note/transactiontemplate_form.html:6
|
||||
msgid "Buttons list"
|
||||
msgstr "Liste des boutons"
|
||||
|
||||
#: templates/registration/logged_out.html:8
|
||||
msgid "Thanks for spending some quality time with the Web site today."
|
||||
|
@ -478,7 +875,7 @@ msgid "Log in again"
|
|||
msgstr ""
|
||||
|
||||
#: templates/registration/login.html:7 templates/registration/login.html:8
|
||||
#: templates/registration/login.html:22
|
||||
#: templates/registration/login.html:26
|
||||
#: templates/registration/password_reset_complete.html:10
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
@ -490,7 +887,7 @@ msgid ""
|
|||
"page. Would you like to login to a different account?"
|
||||
msgstr ""
|
||||
|
||||
#: templates/registration/login.html:23
|
||||
#: templates/registration/login.html:27
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -546,3 +943,72 @@ msgstr ""
|
|||
#: templates/registration/password_reset_form.html:11
|
||||
msgid "Reset my password"
|
||||
msgstr ""
|
||||
|
||||
#: templates/treasury/invoice_form.html:6
|
||||
msgid "Invoices list"
|
||||
msgstr "Liste des factures"
|
||||
|
||||
#: templates/treasury/invoice_form.html:42
|
||||
msgid "Add product"
|
||||
msgstr "Ajouter produit"
|
||||
|
||||
#: templates/treasury/invoice_form.html:43
|
||||
msgid "Remove product"
|
||||
msgstr "Retirer produit"
|
||||
|
||||
#: templates/treasury/invoice_list.html:21
|
||||
msgid "New invoice"
|
||||
msgstr "Nouvelle facture"
|
||||
|
||||
#: templates/treasury/remittance_form.html:7
|
||||
msgid "Remittance #"
|
||||
msgstr "Remise n°"
|
||||
|
||||
#: templates/treasury/remittance_form.html:9
|
||||
#: templates/treasury/specialtransactionproxy_form.html:7
|
||||
msgid "Remittances list"
|
||||
msgstr "Liste des remises"
|
||||
|
||||
#: templates/treasury/remittance_form.html:12
|
||||
msgid "Count"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: templates/treasury/remittance_form.html:29
|
||||
msgid "Linked transactions"
|
||||
msgstr "Transactions liées"
|
||||
|
||||
#: templates/treasury/remittance_form.html:34
|
||||
msgid "There is no transaction linked with this remittance."
|
||||
msgstr "Il n'y a pas de transaction liée à cette remise."
|
||||
|
||||
#: templates/treasury/remittance_list.html:19
|
||||
msgid "Opened remittances"
|
||||
msgstr "Remises ouvertes"
|
||||
|
||||
#: templates/treasury/remittance_list.html:24
|
||||
msgid "There is no opened remittance."
|
||||
msgstr "Il n'y a pas de remise ouverte."
|
||||
|
||||
#: templates/treasury/remittance_list.html:28
|
||||
msgid "New remittance"
|
||||
msgstr "Nouvelle remise"
|
||||
|
||||
#: templates/treasury/remittance_list.html:32
|
||||
msgid "Transfers without remittances"
|
||||
msgstr "Transactions sans remise associée"
|
||||
|
||||
#: templates/treasury/remittance_list.html:37
|
||||
msgid "There is no transaction without any linked remittance."
|
||||
msgstr "Il n'y a pas de transactions sans remise associée."
|
||||
|
||||
#: templates/treasury/remittance_list.html:43
|
||||
msgid "Transfers with opened remittances"
|
||||
msgstr "Transactions associées à une remise ouverte"
|
||||
|
||||
#: templates/treasury/remittance_list.html:48
|
||||
msgid "There is no transaction with an opened linked remittance."
|
||||
msgstr "Il n'y a pas de transaction associée à une remise ouverte."
|
||||
|
||||
#: templates/treasury/remittance_list.html:54
|
||||
msgid "Closed remittances"
|
||||
msgstr "Remises fermées"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
|
@ -9,7 +9,7 @@ server {
|
|||
# the port your site will be served on
|
||||
listen 80;
|
||||
# the domain name it will serve for
|
||||
server_name note.comby.xyz; # substitute your machine's IP address or FQDN
|
||||
server_name note.example.org; # substitute your machine's IP address or FQDN
|
||||
charset utf-8;
|
||||
|
||||
# max upload size
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"model": "cas_server.servicepattern",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"pos": 1,
|
||||
"pattern": ".*",
|
||||
"name": "REPLACEME"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -7,4 +7,4 @@
|
|||
"name": "La Note Kfet \ud83c\udf7b"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1,9 +1,65 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
|
||||
from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
|
||||
from threading import local
|
||||
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
|
||||
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
|
||||
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
|
||||
IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
|
||||
|
||||
_thread_locals = local()
|
||||
|
||||
|
||||
def _set_current_user_and_ip(user=None, session=None, ip=None):
|
||||
setattr(_thread_locals, USER_ATTR_NAME, user)
|
||||
setattr(_thread_locals, SESSION_ATTR_NAME, session)
|
||||
setattr(_thread_locals, IP_ATTR_NAME, ip)
|
||||
|
||||
|
||||
def get_current_user() -> User:
|
||||
return getattr(_thread_locals, USER_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_session() -> SessionStore:
|
||||
return getattr(_thread_locals, SESSION_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_ip() -> str:
|
||||
return getattr(_thread_locals, IP_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_authenticated_user():
|
||||
current_user = get_current_user()
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
return None
|
||||
return current_user
|
||||
|
||||
|
||||
class SessionMiddleware(object):
|
||||
"""
|
||||
This middleware get the current user with his or her IP address on each request.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
user = request.user
|
||||
if 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||
ip = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
|
||||
_set_current_user_and_ip(user, request.session, ip)
|
||||
response = self.get_response(request)
|
||||
_set_current_user_and_ip(None, None, None)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class TurbolinksMiddleware(object):
|
||||
|
@ -35,4 +91,3 @@ class TurbolinksMiddleware(object):
|
|||
location = request.session.pop('_turbolinks_redirect_to')
|
||||
response['Turbolinks-Location'] = location
|
||||
return response
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import os
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import re
|
||||
|
||||
from .base import *
|
||||
|
||||
|
||||
def read_env():
|
||||
"""Pulled from Honcho code with minor updates, reads local default
|
||||
environment variables from a .env file located in the project root
|
||||
|
@ -25,22 +29,55 @@ def read_env():
|
|||
val = re.sub(r'\\(.)', r'\1', m3.group(1))
|
||||
os.environ.setdefault(key, val)
|
||||
|
||||
|
||||
read_env()
|
||||
|
||||
app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev')
|
||||
if app_stage == 'prod':
|
||||
from .production import *
|
||||
DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS')
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS')
|
||||
ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost'))
|
||||
else:
|
||||
from .development import *
|
||||
|
||||
try:
|
||||
#in secrets.py defines everything you want
|
||||
from .secrets import *
|
||||
INSTALLED_APPS += OPTIONAL_APPS
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# env variables set at the of in /env/bin/activate
|
||||
# don't forget to unset in deactivate !
|
||||
if "cas" in INSTALLED_APPS:
|
||||
MIDDLEWARE += ['cas.middleware.CASMiddleware']
|
||||
# CAS Settings
|
||||
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
|
||||
CAS_AUTO_CREATE_USER = False
|
||||
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
|
||||
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
|
||||
CAS_SHOW_SERVICE_MESSAGES = True
|
||||
CAS_SHOW_POWERED = False
|
||||
CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False
|
||||
CAS_PROVIDE_URL_TO_LOGOUT = True
|
||||
CAS_INFO_MESSAGES = {
|
||||
"cas_explained": {
|
||||
"message": _(
|
||||
u"The Central Authentication Service grants you access to most of our websites by "
|
||||
u"authenticating only once, so you don't need to type your credentials again unless "
|
||||
u"your session expires or you logout."
|
||||
),
|
||||
"discardable": True,
|
||||
"type": "info", # one of info, success, info, warning, danger
|
||||
},
|
||||
}
|
||||
|
||||
CAS_INFO_MESSAGES_ORDER = [
|
||||
'cas_explained',
|
||||
]
|
||||
AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',)
|
||||
|
||||
|
||||
if "logs" in INSTALLED_APPS:
|
||||
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
|
||||
|
||||
if "debug_toolbar" in INSTALLED_APPS:
|
||||
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
|
|
|
@ -37,7 +37,6 @@ INSTALLED_APPS = [
|
|||
|
||||
# External apps
|
||||
'polymorphic',
|
||||
'reversion',
|
||||
'crispy_forms',
|
||||
'django_tables2',
|
||||
# Django contrib
|
||||
|
@ -60,7 +59,10 @@ INSTALLED_APPS = [
|
|||
'activity',
|
||||
'member',
|
||||
'note',
|
||||
'treasury',
|
||||
'permission',
|
||||
'api',
|
||||
'logs',
|
||||
]
|
||||
LOGIN_REDIRECT_URL = '/note/transfer/'
|
||||
|
||||
|
@ -92,6 +94,7 @@ TEMPLATES = [
|
|||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.template.context_processors.request',
|
||||
# 'django.template.context_processors.media',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -123,29 +126,23 @@ PASSWORD_HASHERS = [
|
|||
'member.hashers.CustomNK15Hasher',
|
||||
]
|
||||
|
||||
# Django Guardian object permissions
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend', # this is default
|
||||
'guardian.backends.ObjectPermissionBackend',
|
||||
'permission.backends.PermissionBackend', # Custom role-based permission system
|
||||
)
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# or allow read-only access for unauthenticated users.
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
# TODO Maybe replace it with our custom permissions system
|
||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
||||
# Control API access with our role-based permission system
|
||||
'permission.permissions.StrongDjangoObjectPermissions',
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
]
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
}
|
||||
|
||||
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
|
||||
|
||||
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
|
@ -176,10 +173,10 @@ FIXTURE_DIRS = [os.path.join(BASE_DIR, "note_kfet/fixtures")]
|
|||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/var/www/example.com/static/"
|
||||
STATIC_ROOT = os.path.realpath(__file__)
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'static')]
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||
# STATICFILES_DIRS = [
|
||||
# os.path.join(BASE_DIR, 'static')]
|
||||
STATICFILES_DIRS = []
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html'
|
||||
# URL prefix for static files.
|
||||
|
@ -188,3 +185,9 @@ STATIC_URL = '/static/'
|
|||
|
||||
ALIAS_VALIDATOR_REGEX = r''
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Profile Picture Settings
|
||||
PIC_WIDTH = 200
|
||||
PIC_RATIO = 1
|
||||
|
|
|
@ -11,17 +11,30 @@
|
|||
# - and more ...
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
from . import *
|
||||
import os
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
if os.getenv("DJANGO_DEV_STORE_METHOD", "sqllite") == "postgresql":
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': os.environ.get('DJANGO_DB_NAME', 'note_db'),
|
||||
'USER': os.environ.get('DJANGO_DB_USER', 'note'),
|
||||
'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'),
|
||||
'HOST': os.environ.get('DJANGO_DB_HOST', 'localhost'),
|
||||
'PORT': os.environ.get('DJANGO_DB_PORT', ''), # Use default port
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Break it, fix it!
|
||||
DEBUG = True
|
||||
|
@ -38,7 +51,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||
# EMAIL_HOST_USER = 'change_me'
|
||||
# EMAIL_HOST_PASSWORD = 'change_me'
|
||||
|
||||
SERVER_EMAIL = 'no-reply@example.org'
|
||||
SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com")
|
||||
|
||||
# Security settings
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = False
|
||||
|
@ -48,3 +61,11 @@ CSRF_COOKIE_SECURE = False
|
|||
CSRF_COOKIE_HTTPONLY = False
|
||||
X_FRAME_OPTIONS = 'DENY'
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 3
|
||||
|
||||
# CAS Client settings
|
||||
# Can be modified in secrets.py
|
||||
CAS_SERVER_URL = "http://localhost:8000/cas/"
|
||||
|
||||
STATIC_ROOT = '' # not needed in development settings
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'static')]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
########################
|
||||
# Production Settings #
|
||||
########################
|
||||
|
@ -14,11 +16,11 @@
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'note_db',
|
||||
'USER': 'note',
|
||||
'PASSWORD': 'update_in_env_variable',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '',
|
||||
'NAME': os.environ.get('DJANGO_DB_NAME', 'note_db'),
|
||||
'USER': os.environ.get('DJANGO_DB_USER', 'note'),
|
||||
'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'),
|
||||
'HOST': os.environ.get('DJANGO_DB_HOST', 'localhost'),
|
||||
'PORT': os.environ.get('DJANGO_DB_PORT', ''), # Use default port
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +28,9 @@ DATABASES = {
|
|||
DEBUG = True
|
||||
|
||||
# Mandatory !
|
||||
ALLOWED_HOSTS = ['127.0.0.1','note.comby.xyz']
|
||||
ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
|
||||
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||
|
||||
# Emails
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
@ -37,7 +41,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||
# EMAIL_HOST_USER = 'change_me'
|
||||
# EMAIL_HOST_PASSWORD = 'change_me'
|
||||
|
||||
SERVER_EMAIL = 'no-reply@example.org'
|
||||
SERVER_EMAIL = 'no-reply@' + os.getenv("DOMAIN", "example.com")
|
||||
|
||||
# Security settings
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = False
|
||||
|
@ -47,3 +51,6 @@ CSRF_COOKIE_SECURE = False
|
|||
CSRF_COOKIE_HTTPONLY = False
|
||||
X_FRAME_OPTIONS = 'DENY'
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 3
|
||||
|
||||
# CAS Client settings
|
||||
CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# CAS
|
||||
OPTIONAL_APPS = [
|
||||
# 'cas_server',
|
||||
# 'cas',
|
||||
# 'debug_toolbar'
|
||||
]
|
|
@ -1,10 +1,14 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from member.views import CustomLoginView
|
||||
|
||||
urlpatterns = [
|
||||
# Dev so redirect to something random
|
||||
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'),
|
||||
|
@ -13,13 +17,37 @@ urlpatterns = [
|
|||
path('note/', include('note.urls')),
|
||||
path('accounts/', include('member.urls')),
|
||||
path('activity/', include('activity.urls')),
|
||||
path('treasury/', include('treasury.urls')),
|
||||
|
||||
# Include Django Contrib and Core routers
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
# Include Django REST API
|
||||
path('accounts/', include('member.urls')),
|
||||
path('accounts/login/', CustomLoginView.as_view()),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('api/', include('api.urls')),
|
||||
]
|
||||
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.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
|
||||
|
|
|
@ -7,14 +7,9 @@ django-autocomplete-light==3.5.1
|
|||
django-crispy-forms==1.7.2
|
||||
django-extensions==2.1.9
|
||||
django-filter==2.2.0
|
||||
django-guardian==2.1.0
|
||||
django-polymorphic==2.0.3
|
||||
djangorestframework==3.9.0
|
||||
django-rest-polymorphic==0.1.8
|
||||
django-reversion==3.0.3
|
||||
django-tables2==2.1.0
|
||||
docutils==0.14
|
||||
psycopg2==2.8.4
|
||||
idna==2.8
|
||||
oauthlib==3.1.0
|
||||
Pillow==6.1.0
|
||||
|
@ -24,4 +19,6 @@ requests==2.22.0
|
|||
requests-oauthlib==1.2.0
|
||||
six==1.12.0
|
||||
sqlparse==0.3.0
|
||||
djangorestframework==3.9.0
|
||||
django-rest-polymorphic==0.1.8
|
||||
urllib3==1.25.3
|
|
@ -0,0 +1,2 @@
|
|||
django-cas-client==1.5.3
|
||||
django-cas-server==1.1.0
|
|
@ -0,0 +1 @@
|
|||
psycopg2-binary==2.8.4
|
|
@ -0,0 +1,260 @@
|
|||
select.admin-autocomplete {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single,
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||
border-color: #999;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||
background-color: #eee;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||
border: solid #999 1px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: #eee;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #79aec8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
|
@ -0,0 +1,987 @@
|
|||
/*
|
||||
DJANGO Admin styles
|
||||
*/
|
||||
|
||||
@import url(fonts.css);
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-family: "Roboto","Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* LINKS */
|
||||
|
||||
a:link, a:visited {
|
||||
color: #447e9b;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a.section:link, a.section:visited {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.section:focus, a.section:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* GLOBAL DEFAULTS */
|
||||
|
||||
p, ol, ul, dl {
|
||||
margin: .2em 0 .8em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 20px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin: 1em 0 .5em 0;
|
||||
}
|
||||
|
||||
h2.subhead {
|
||||
font-weight: normal;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: .8em 0 .3em 0;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
margin: 1em 0 .8em 0;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 10px;
|
||||
margin: 1.5em 0 .5em 0;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style-type: square;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
li ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li, dt, dd {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: 11px;
|
||||
color: #777;
|
||||
margin-left: 2px;
|
||||
padding-left: 10px;
|
||||
border-left: 5px solid #ddd;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre.literal-block {
|
||||
margin: 10px;
|
||||
background: #eee;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
code strong {
|
||||
color: #930;
|
||||
}
|
||||
|
||||
hr {
|
||||
clear: both;
|
||||
color: #eee;
|
||||
background-color: #eee;
|
||||
height: 1px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
/* TEXT STYLES & MODIFIERS */
|
||||
|
||||
.small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
p.tiny {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.mini {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
p.mini {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.help, p.help, form p.help, div.help, form div.help, div.help li {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
div.help ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.help-tooltip {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
p img, h1 img, h2 img, h3 img, h4 img, td img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.quiet, a.quiet:link, a.quiet:visited {
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.example {
|
||||
margin: 10px 0;
|
||||
padding: 5px 10px;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* TABLES */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
td, th {
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
vertical-align: top;
|
||||
padding: 8px;
|
||||
font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead th,
|
||||
tfoot td {
|
||||
color: #666;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
background: #fff;
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
thead th.required {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
tr.alt {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.row1 {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.row2 {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* SORTABLE TABLES */
|
||||
|
||||
thead th {
|
||||
padding: 5px 10px;
|
||||
line-height: normal;
|
||||
text-transform: uppercase;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
thead th a:link, thead th a:visited {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
thead th.sorted {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
thead th.sorted .text {
|
||||
padding-right: 42px;
|
||||
}
|
||||
|
||||
table thead th .text span {
|
||||
padding: 8px 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
table thead th .text a {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
table thead th .text a:focus, table thead th .text a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
thead th.sorted a.sortremove {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
table thead th.sorted:hover a.sortremove {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions {
|
||||
display: block;
|
||||
padding: 9px 5px 0 5px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortpriority {
|
||||
font-size: .8em;
|
||||
min-width: 12px;
|
||||
text-align: center;
|
||||
vertical-align: 3px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
background: url(../img/sorting-icons.svg) 0 0 no-repeat;
|
||||
background-size: 14px auto;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.sortremove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.sortremove:after {
|
||||
content: '\\';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 3px;
|
||||
font-weight: 200;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.sortremove:focus:after,
|
||||
table thead th.sorted .sortoptions a.sortremove:hover:after {
|
||||
color: #447e9b;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.sortremove:focus,
|
||||
table thead th.sorted .sortoptions a.sortremove:hover {
|
||||
background-position: 0 -14px;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.ascending {
|
||||
background-position: 0 -28px;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.ascending:focus,
|
||||
table thead th.sorted .sortoptions a.ascending:hover {
|
||||
background-position: 0 -42px;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.descending {
|
||||
top: 1px;
|
||||
background-position: 0 -56px;
|
||||
}
|
||||
|
||||
table thead th.sorted .sortoptions a.descending:focus,
|
||||
table thead th.sorted .sortoptions a.descending:hover {
|
||||
background-position: 0 -70px;
|
||||
}
|
||||
|
||||
/* FORM DEFAULTS */
|
||||
|
||||
input, textarea, select, .form-row p, form .button {
|
||||
margin: 2px 0;
|
||||
padding: 2px 3px;
|
||||
vertical-align: middle;
|
||||
font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
.form-row div.help {
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], input[type=email], input[type=url],
|
||||
input[type=number], input[type=tel], textarea, select, .vTextField {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 5px 6px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus,
|
||||
input[type=url]:focus, input[type=number]:focus, input[type=tel]:focus,
|
||||
textarea:focus, select:focus, .vTextField:focus {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
/* Allow HTML size attribute to override the height in the rule above. */
|
||||
height: auto;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* FORM BUTTONS */
|
||||
|
||||
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
||||
background: #79aec8;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.button {
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.button:active, input[type=submit]:active, input[type=button]:active,
|
||||
.button:focus, input[type=submit]:focus, input[type=button]:focus,
|
||||
.button:hover, input[type=submit]:hover, input[type=button]:hover {
|
||||
background: #609ab6;
|
||||
}
|
||||
|
||||
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.button.default, input[type=submit].default, .submit-row input.default {
|
||||
float: right;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
background: #417690;
|
||||
}
|
||||
|
||||
.button.default:active, input[type=submit].default:active,
|
||||
.button.default:focus, input[type=submit].default:focus,
|
||||
.button.default:hover, input[type=submit].default:hover {
|
||||
background: #205067;
|
||||
}
|
||||
|
||||
.button[disabled].default,
|
||||
input[type=submit][disabled].default,
|
||||
input[type=button][disabled].default {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
|
||||
/* MODULES */
|
||||
|
||||
.module {
|
||||
border: none;
|
||||
margin-bottom: 30px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module p, .module ul, .module h3, .module h4, .module dl, .module pre {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.module blockquote {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.module ul, .module ol {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.module h3 {
|
||||
margin-top: .6em;
|
||||
}
|
||||
|
||||
.module h2, .module caption, .inline-group h2 {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
background: #79aec8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.module caption,
|
||||
.inline-group h2 {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.module table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* MESSAGES & ERRORS */
|
||||
|
||||
ul.messagelist {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.messagelist li {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
padding: 10px 10px 10px 65px;
|
||||
margin: 0 0 10px 0;
|
||||
background: #dfd url(../img/icon-yes.svg) 40px 12px no-repeat;
|
||||
background-size: 16px auto;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning {
|
||||
background: #ffc url(../img/icon-alert.svg) 40px 14px no-repeat;
|
||||
background-size: 14px auto;
|
||||
}
|
||||
|
||||
ul.messagelist li.error {
|
||||
background: #ffefef url(../img/icon-no.svg) 40px 12px no-repeat;
|
||||
background-size: 16px auto;
|
||||
}
|
||||
|
||||
.errornote {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
margin: 0 0 10px 0;
|
||||
color: #ba2121;
|
||||
border: 1px solid #ba2121;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
background-position: 5px 12px;
|
||||
}
|
||||
|
||||
ul.errorlist {
|
||||
margin: 0 0 4px;
|
||||
padding: 0;
|
||||
color: #ba2121;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
ul.errorlist li {
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
ul.errorlist li:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul.errorlist li a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
td ul.errorlist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
td ul.errorlist li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row.errors {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.form-row.errors ul.errorlist li {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.errors input, .errors select, .errors textarea {
|
||||
border: 1px solid #ba2121;
|
||||
}
|
||||
|
||||
div.system-message {
|
||||
background: #ffc;
|
||||
margin: 10px;
|
||||
padding: 6px 8px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
padding: 4px 5px 4px 25px;
|
||||
margin: 0;
|
||||
color: #c11;
|
||||
background: #ffefef url(../img/icon-no.svg) 5px 5px no-repeat;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
padding: 5px 0 0 12px;
|
||||
}
|
||||
|
||||
/* BREADCRUMBS */
|
||||
|
||||
div.breadcrumbs {
|
||||
background: #79aec8;
|
||||
padding: 10px 40px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: #c4dce8;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.breadcrumbs a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
div.breadcrumbs a:focus, div.breadcrumbs a:hover {
|
||||
color: #c4dce8;
|
||||
}
|
||||
|
||||
/* ACTION ICONS */
|
||||
|
||||
.viewlink, .inlineviewlink {
|
||||
padding-left: 16px;
|
||||
background: url(../img/icon-viewlink.svg) 0 1px no-repeat;
|
||||
}
|
||||
|
||||
.addlink {
|
||||
padding-left: 16px;
|
||||
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
|
||||
}
|
||||
|
||||
.changelink, .inlinechangelink {
|
||||
padding-left: 16px;
|
||||
background: url(../img/icon-changelink.svg) 0 1px no-repeat;
|
||||
}
|
||||
|
||||
.deletelink {
|
||||
padding-left: 16px;
|
||||
background: url(../img/icon-deletelink.svg) 0 1px no-repeat;
|
||||
}
|
||||
|
||||
a.deletelink:link, a.deletelink:visited {
|
||||
color: #CC3434;
|
||||
}
|
||||
|
||||
a.deletelink:focus, a.deletelink:hover {
|
||||
color: #993333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* OBJECT TOOLS */
|
||||
|
||||
.object-tools {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding-left: 0;
|
||||
float: right;
|
||||
position: relative;
|
||||
margin-top: -48px;
|
||||
}
|
||||
|
||||
.form-row .object-tools {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
float: none;
|
||||
height: 2em;
|
||||
padding-left: 3.5em;
|
||||
}
|
||||
|
||||
.object-tools li {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-left: 5px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.object-tools a {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.object-tools a:link, .object-tools a:visited {
|
||||
display: block;
|
||||
float: left;
|
||||
padding: 3px 12px;
|
||||
background: #999;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.object-tools a:focus, .object-tools a:hover {
|
||||
background-color: #417690;
|
||||
}
|
||||
|
||||
.object-tools a:focus{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.object-tools a.viewsitelink, .object-tools a.golink,.object-tools a.addlink {
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 7px center;
|
||||
padding-right: 26px;
|
||||
}
|
||||
|
||||
.object-tools a.viewsitelink, .object-tools a.golink {
|
||||
background-image: url(../img/tooltag-arrowright.svg);
|
||||
}
|
||||
|
||||
.object-tools a.addlink {
|
||||
background-image: url(../img/tooltag-add.svg);
|
||||
}
|
||||
|
||||
/* OBJECT HISTORY */
|
||||
|
||||
table#change-history {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table#change-history tbody th {
|
||||
width: 16em;
|
||||
}
|
||||
|
||||
/* PAGE STRUCTURE */
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 980px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.dashboard #content {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
#content-main {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
float: right;
|
||||
width: 260px;
|
||||
position: relative;
|
||||
margin-right: -300px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
clear: both;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* COLUMN TYPES */
|
||||
|
||||
.colMS {
|
||||
margin-right: 300px;
|
||||
}
|
||||
|
||||
.colSM {
|
||||
margin-left: 300px;
|
||||
}
|
||||
|
||||
.colSM #content-related {
|
||||
float: left;
|
||||
margin-right: 0;
|
||||
margin-left: -300px;
|
||||
}
|
||||
|
||||
.colSM #content-main {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.popup .colM {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
|
||||
#header {
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 40px;
|
||||
background: #417690;
|
||||
color: #ffc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#header a:link, #header a:visited {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#header a:focus , #header a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#branding {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#branding h1 {
|
||||
padding: 0;
|
||||
margin: 0 20px 0 0;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
color: #f5dd5d;
|
||||
}
|
||||
|
||||
#branding h1, #branding h1 a:link, #branding h1 a:visited {
|
||||
color: #f5dd5d;
|
||||
}
|
||||
|
||||
#branding h2 {
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
margin: -8px 0 8px 0;
|
||||
font-weight: normal;
|
||||
color: #ffc;
|
||||
}
|
||||
|
||||
#branding a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#user-tools {
|
||||
float: right;
|
||||
padding: 0;
|
||||
margin: 0 0 0 20px;
|
||||
font-weight: 300;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#user-tools a {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
#user-tools a:focus, #user-tools a:hover {
|
||||
text-decoration: none;
|
||||
border-bottom-color: #79aec8;
|
||||
color: #79aec8;
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
|
||||
#content-related {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
#content-related .module {
|
||||
background: none;
|
||||
}
|
||||
|
||||
#content-related h3 {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
padding: 0 16px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
#content-related h4 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#content-related p {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#content-related .actionlist {
|
||||
padding: 0;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#content-related .actionlist li {
|
||||
line-height: 1.2;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
#content-related .module h2 {
|
||||
background: none;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.delete-confirmation form input[type="submit"] {
|
||||
background: #ba2121;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.delete-confirmation form input[type="submit"]:active,
|
||||
.delete-confirmation form input[type="submit"]:focus,
|
||||
.delete-confirmation form input[type="submit"]:hover {
|
||||
background: #a41515;
|
||||
}
|
||||
|
||||
.delete-confirmation form .cancel-link {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
background: #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
color: #333;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.delete-confirmation form .cancel-link:active,
|
||||
.delete-confirmation form .cancel-link:focus,
|
||||
.delete-confirmation form .cancel-link:hover {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
/* POPUP */
|
||||
.popup #content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup #container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.popup #header {
|
||||
padding: 10px 20px;
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
/* CHANGELISTS */
|
||||
|
||||
#changelist {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list .hiddenfields { display:none; }
|
||||
|
||||
.change-list .filtered table {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.change-list .filtered .results, .change-list .filtered .paginator,
|
||||
.filtered #toolbar, .filtered div.xfull {
|
||||
margin-right: 280px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.change-list .filtered table tbody th {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#changelist-form .results {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#changelist .toplinks {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
#changelist .paginator {
|
||||
color: #666;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* CHANGELIST TABLES */
|
||||
|
||||
#changelist table thead th {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#changelist table thead th.action-checkbox-column {
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tbody td.action-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tfoot {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* TOOLBAR */
|
||||
|
||||
#changelist #toolbar {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
background: #f8f8f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#changelist #toolbar form input {
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#changelist #toolbar form #searchbar {
|
||||
height: 19px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#changelist #toolbar form #searchbar:focus {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
#changelist #toolbar form input[type="submit"] {
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 10px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
background: #fff;
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#changelist #toolbar form input[type="submit"]:focus,
|
||||
#changelist #toolbar form input[type="submit"]:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
#changelist #changelist-search img {
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* FILTER COLUMN */
|
||||
|
||||
#changelist-filter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
width: 240px;
|
||||
background: #f8f8f8;
|
||||
border-left: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#changelist-filter h2 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter h3 {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#changelist-filter ul {
|
||||
margin: 5px 0;
|
||||
padding: 0 15px 15px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
#changelist-filter ul:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter li {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#changelist-filter a {
|
||||
display: block;
|
||||
color: #999;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: 5px solid #eaeaea;
|
||||
padding-left: 10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected a {
|
||||
color: #5b80b2;
|
||||
}
|
||||
|
||||
#changelist-filter a:focus, #changelist-filter a:hover,
|
||||
#changelist-filter li.selected a:focus,
|
||||
#changelist-filter li.selected a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
/* DATE DRILLDOWN */
|
||||
|
||||
.change-list ul.toplinks {
|
||||
display: block;
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks li {
|
||||
padding: 3px 6px;
|
||||
font-weight: bold;
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a:focus,
|
||||
.change-list ul.toplinks .date-back a:hover {
|
||||
color: #036;
|
||||
}
|
||||
|
||||
/* PAGINATOR */
|
||||
|
||||
.paginator {
|
||||
font-size: 13px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.paginator a:link, .paginator a:visited {
|
||||
padding: 2px 6px;
|
||||
background: #79aec8;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.paginator a.showall {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #5b80b2;
|
||||
}
|
||||
|
||||
.paginator a.showall:focus, .paginator a.showall:hover {
|
||||
background: none;
|
||||
color: #036;
|
||||
}
|
||||
|
||||
.paginator .end {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.paginator .this-page {
|
||||
padding: 2px 6px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.paginator a:focus, .paginator a:hover {
|
||||
color: white;
|
||||
background: #036;
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
.filtered .actions {
|
||||
margin-right: 280px;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#changelist table input {
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
#changelist table tbody tr.selected {
|
||||
background-color: #FFFFCC;
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
line-height: 24px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#changelist .actions.selected {
|
||||
background: #fffccf;
|
||||
border-top: 1px solid #fffee8;
|
||||
border-bottom: 1px solid #edecd6;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.action-counter,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question {
|
||||
font-size: 13px;
|
||||
margin: 0 0.5em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#changelist .actions:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
vertical-align: top;
|
||||
height: 24px;
|
||||
background: none;
|
||||
color: #000;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 0 0 0 4px;
|
||||
margin: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#changelist .actions select:focus {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
font-size: 13px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
line-height: 1;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
||||
border-color: #999;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* DASHBOARD */
|
||||
|
||||
.dashboard .module table th {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .module table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
display: block;
|
||||
padding-right: .6em;
|
||||
}
|
||||
|
||||
/* RECENT ACTIONS MODULE */
|
||||
|
||||
.module ul.actionlist {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul.actionlist li {
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-o-text-overflow: ellipsis;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Bold-webfont.woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Regular-webfont.woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Light-webfont.woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue