1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Merge branch 'master' into rights

# Conflicts:
#	note_kfet/settings/base.py
This commit is contained in:
Yohann D'ANELLO 2020-03-07 10:42:51 +01:00
commit 0884270474
429 changed files with 86067 additions and 920 deletions

12
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Server config files
nginx_note.conf
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
dist dist
build build
@ -28,10 +31,13 @@ coverage
# PyCharm project settings # PyCharm project settings
.idea .idea
# Local data # VSCode project settings
settings_local.py .vscode
*.log
# Local data
secrets.py
*.log
media/
# Virtualenv # Virtualenv
env/ env/
venv/ venv/

View File

@ -2,20 +2,25 @@ image: python:3.6
stages: stages:
- test - test
- quality-assurance
before_script: before_script:
- pip install tox - pip install tox
python36: py36-django22:
image: python:3.6 image: python:3.6
stage: test stage: test
script: tox -e py36 script: tox -e py36-django22
python37: py37-django22:
image: python:3.7 image: python:3.7
stage: test stage: test
script: tox -e py37 script: tox -e py37-django22
linters: linters:
stage: test image: python:3.6
stage: quality-assurance
script: tox -e linters script: tox -e linters
# Be nice to new contributors, but please use `tox`
allow_failure: true

379
.pylintrc
View File

@ -1,379 +0,0 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS,.git
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=INFERENCE_FAILURE
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=yes
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set). This supports can work
# with qualified names.
ignored-classes=
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=20
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in an if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM python:3-buster
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 8000

154
README.md
View File

@ -10,8 +10,8 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
1. Paquets nécessaires 1. Paquets nécessaires
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi $ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-virtualenv git $ sudo apt install uwsgi-plugin-python3 python3-venv git acl
2. Clonage du dépot 2. Clonage du dépot
@ -19,60 +19,163 @@ On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout n
$ cd /var/www/ $ cd /var/www/
$ mkdir note_kfet $ mkdir note_kfet
$ sudo chown www-data:www-data note_kfet
$ sudo usermod -a -G www-data $USER
$ sudo chmod g+ws note_kfet
$ sudo setfacl -d -m "g::rwx" note_kfet
$ cd note_kfet $ cd note_kfet
$ git clone git@gitlab.crans.org:bde/nk20.git . $ git clone git@gitlab.crans.org:bde/nk20.git .
3. Environment Virtuel 3. Environment Virtuel
À la racine du projet: À la racine du projet:
$ virtualenv env $ python3 -m venv env
$ source /env/bin/activate $ source env/bin/activate
(env)$ pip install -r requirements.txt (env)$ pip3 install -r requirements.txt
(env)$ deactivate (env)$ deactivate
4. uwsgi et Nginx 4. uwsgi et Nginx
Un exemple de conf est disponible :
$ cp nginx_note.conf_example nginx_note.conf
***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é serveu :
$ sudo ln -s /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/
**Modifier la config nginx pour l'adapter à votre server!**
Si l'on a un emperor (plusieurs instance uwsgi): Si l'on a un emperor (plusieurs instance uwsgi):
$ sudo ln -s /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/ $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
Sinon: Sinon:
$ sudo ln -s /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/ $ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/
Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`.
5. Base de données 5. Base de données
Pour le moment c'est du sqllite, pas de config particulière. En prod on utilise postgresql.
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
(env)$ pip3 install psycopg2
La config de la base de donnée se fait comme suit:
a. On se connecte au shell de psql
$ sudo su - postgres
$ psql
b. On sécurise l'utilisateur postgres
postgres=# \password
Enter new password:
Conservez ce mot de passe de la meme manière que tous les autres.
c. On créer la basse de donnée, et l'utilisateur associé
postgres=# CREATE USER note WITH PASSWORD 'un_mot_de_passe_sur';
CREATE ROLE
postgres=# CREATE DATABASE note_db OWNER note;
CREATE DATABASE
## Développer en local Si tout va bien:
postgres=#\list
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
note_db | note | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 |
postgres | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 |
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)
Il est tout a fait possible de travailler en local, vive `./manage.py runserver` ! 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'
1. Cloner le dépot là ou vous voulez: 6. Variable d'environnement et Migrations
$ git@gitlab.crans.org:bde/nk20.git Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
2. Environnement Virtuel
$ virtualenv env
$ source /env/bin/activate $ source /env/bin/activate
(env)$ pip install -r requirements.txt (env)$ ./manage.py check # pas de bétise qui traine
3. Migrations:
(env)$ ./manage.py makemigrations (env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
4. Enjoy: 7. Enjoy
(env)$ ./manage.py runserver
## Installer avec Docker
Il est possible de travailler sur une instance Docker.
1. Cloner le dépôt là où vous voulez :
$ git clone git@gitlab.crans.org:bde/nk20.git
2. 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/
restart: always
labels:
- traefik.domain=ndd.exemple.com
- traefik.frontend.rule=Host:ndd.exemple.com
- traefik.port=8000
3. Enjoy :
$ docker-compose up -d nk20
## Installer un serveur de développement
Avec `./manage.py runserver` il est très rapide de mettre en place
un serveur de développement par exemple sur son ordinateur.
1. Cloner le dépôt là où vous voulez :
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
2. Créer un environnement Python isolé
pour ne pas interférer avec les versions de paquets systèmes :
$ python3 -m venv venv
$ source venv/bin/activate
(env)$ pip install -r requirements.txt
3. 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 :
(env)$ ./manage.py createsuperuser
5. Enjoy :
(env)$ ./manage.py runserver 0.0.0.0:8000
En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
de la note sur un téléphone !
## Cahier des Charges ## Cahier des Charges
@ -80,4 +183,5 @@ Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
## Documentation ## Documentation
La documentation est générée par django et son module admindocs. **Commenter votre code !* La documentation est générée par django et son module admindocs.
**Commenter votre code !**

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig' default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
@ -12,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin):
Admin customisation for Activity Admin customisation for Activity
""" """
list_display = ('name', 'activity_type', 'organizer') list_display = ('name', 'activity_type', 'organizer')
list_filter = ('activity_type',) list_filter = ('activity_type', )
search_fields = ['name', 'organizer__name'] search_fields = ['name', 'organizer__name']
# Organize activities by start date # Organize activities by start date

View File

View File

@ -0,0 +1,36 @@
# 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 ActivityType, Activity, Guest
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__'
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__'
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__'

13
apps/activity/api/urls.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet
def register_activity_urls(router, path):
"""
Configure router for Activity REST API.
"""
router.register(path + '/activity', ActivityViewSet)
router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet)

View File

@ -0,0 +1,37 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
from ..models import ActivityType, Activity, Guest
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
class ActivityTypeViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/type/
"""
queryset = ActivityType.objects.all()
serializer_class = ActivityTypeSerializer
class ActivityViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/activity/
"""
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
class GuestViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/guest/
"""
queryset = Guest.objects.all()
serializer_class = GuestSerializer

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
@ -9,7 +8,12 @@ from django.utils.translation import gettext_lazy as _
class ActivityType(models.Model): class ActivityType(models.Model):
""" """
Type of Activity, (e.g "Pot", "Soirée Club") and associated properties Type of Activity, (e.g "Pot", "Soirée Club") and associated properties.
Activity Type are used as a search field for Activity, and determine how
some rules about the activity:
- Can people be invited
- What is the entrance fee.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -32,7 +36,9 @@ class ActivityType(models.Model):
class Activity(models.Model): class Activity(models.Model):
""" """
An IRL event organized by a club for others. An IRL event organized by a club for other club.
By default the invited clubs should be the Club containing all the active accounts.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -73,7 +79,7 @@ class Activity(models.Model):
class Guest(models.Model): class Guest(models.Model):
""" """
People who are not current members of any clubs, and invited by someone who is a current member. People who are not current members of any clubs, and are invited by someone who is a current member.
""" """
activity = models.ForeignKey( activity = models.ForeignKey(
Activity, Activity,
@ -94,6 +100,8 @@ class Guest(models.Model):
entry_transaction = models.ForeignKey( entry_transaction = models.ForeignKey(
'note.Transaction', 'note.Transaction',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True,
null=True,
) )
class Meta: class Meta:

4
apps/api/__init__.py Normal file
View File

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

10
apps/api/apps.py Normal file
View File

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

51
apps/api/urls.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf.urls import url, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
from activity.api.urls import register_activity_urls
from member.api.urls import register_members_urls
from note.api.urls import register_note_urls
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 = (
'password',
'groups',
'user_permissions',
)
class UserViewSet(viewsets.ModelViewSet):
"""
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 = User.objects.all()
serializer_class = UserSerializer
# Routers provide an easy way of automatically determining the URL conf.
# Register each app API router and user viewset
router = routers.DefaultRouter()
router.register('user', UserViewSet)
register_members_urls(router, 'members')
register_activity_urls(router, 'activity')
register_note_urls(router, 'note')
app_name = 'api'
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

4
apps/logs/__init__.py Normal file
View File

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

14
apps/logs/apps.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class LogsConfig(AppConfig):
name = 'logs'
verbose_name = _('Logs')
def ready(self):
# noinspection PyUnresolvedReferences
import logs.signals

View File

71
apps/logs/models.py Normal file
View File

@ -0,0 +1,71 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
class Changelog(models.Model):
"""
Store each modification on 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,
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."))

119
apps/logs/signals.py Normal file
View File

@ -0,0 +1,119 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import inspect
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.db.models.signals import pre_save, post_save, post_delete
from django.dispatch import receiver
from .models import Changelog
def get_request_in_signal(sender):
req = None
for entry in reversed(inspect.stack()):
try:
req = entry[0].f_locals['request']
# Check if there is a user
# noinspection PyStatementEffect
req.user
break
except:
pass
if not req:
print("WARNING: Attempt to save " + str(sender) + " with no user")
return req
def get_user_and_ip(sender):
req = get_request_in_signal(sender)
try:
user = req.user
if 'HTTP_X_FORWARDED_FOR' in req.META:
ip = req.META.get('HTTP_X_FORWARDED_FOR')
else:
ip = req.META.get('REMOTE_ADDR')
except:
user = None
ip = None
return user, ip
EXCLUDED = [
'admin.logentry',
'authtoken.token',
'cas_server.user',
'cas_server.userattributes',
'contenttypes.contenttype',
'logs.changelog',
'migrations.migration',
'note.noteuser',
'note.noteclub',
'note.notespecial',
'sessions.session',
'reversion.revision',
'reversion.version',
]
@receiver(pre_save)
def pre_save_object(sender, instance, **kwargs):
qs = sender.objects.filter(pk=instance.pk).all()
if qs.exists():
instance._previous = qs.get()
else:
instance._previous = None
@receiver(post_save)
def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
previous = instance._previous
user, ip = get_user_and_ip(sender)
if user is not None and instance._meta.label_lower == "auth.user" and previous:
# Don't save last login modifications
if instance.last_login != previous.last_login:
return
previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None
instance_json = serializers.serialize('json', [instance, ])[1:-1]
if previous_json == instance_json:
# No 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()
@receiver(post_delete)
def delete_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
user, ip = get_user_and_ip(sender)
instance_json = serializers.serialize('json', [instance, ])[1:-1]
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()

8
apps/logs/urls.py Normal file
View File

@ -0,0 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
app_name = 'logs'
# TODO User interface
urlpatterns = [
]

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig' default_app_config = 'member.apps.MemberConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
@ -19,9 +18,9 @@ class ProfileInline(admin.StackedInline):
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,) inlines = (ProfileInline, )
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_select_related = ('profile',) list_select_related = ('profile', )
form = ProfileForm form = ProfileForm
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):

View File

View File

@ -0,0 +1,46 @@
# 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 Profile, Club, Role, Membership
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__'
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__'
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__'
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__'

14
apps/member/api/urls.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
def register_members_urls(router, path):
"""
Configure router for Member REST API.
"""
router.register(path + '/profile', ProfileViewSet)
router.register(path + '/club', ClubViewSet)
router.register(path + '/role', RoleViewSet)
router.register(path + '/membership', MembershipViewSet)

47
apps/member/api/views.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
from ..models import Profile, Club, Role, Membership
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
class ProfileViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
then render it on /api/members/profile/
"""
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
class ClubViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
then render it on /api/members/club/
"""
queryset = Club.objects.all()
serializer_class = ClubSerializer
class RoleViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
then render it on /api/members/role/
"""
queryset = Role.objects.all()
serializer_class = RoleSerializer
class MembershipViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
then render it on /api/members/membership/
"""
queryset = Membership.objects.all()
serializer_class = MembershipSerializer

View File

@ -1,11 +1,23 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import post_save
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .signals import save_user_profile
class MemberConfig(AppConfig): class MemberConfig(AppConfig):
name = 'member' name = 'member'
verbose_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,
)

33
apps/member/filters.py Normal file
View File

@ -0,0 +1,33 @@
# 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
class UserFilter(FilterSet):
class Meta:
model = User
fields = ['last_name', 'first_name', 'username', 'profile__section']
filter_overrides = {
CharField: {
'filter_class': CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains'
}
}
}
class UserFilterFormHelper(FormHelper):
form_method = 'GET'
layout = Layout(
'last_name',
'first_name',
'username',
'profile__section',
Submit('Submit', 'Apply Filter'),
)

View File

@ -0,0 +1,26 @@
[
{
"model": "member.club",
"pk": 1,
"fields": {
"name": "BDE",
"email": "tresorerie.bde@example.com",
"membership_fee": 5,
"membership_duration": "396 00:00:00",
"membership_start": "213 00:00:00",
"membership_end": "273 00:00:00"
}
},
{
"model": "member.club",
"pk": 2,
"fields": {
"name": "Kfet",
"email": "tresorerie.bde@example.com",
"membership_fee": 35,
"membership_duration": "396 00:00:00",
"membership_start": "213 00:00:00",
"membership_end": "273 00:00:00"
}
}
]

View File

@ -1,61 +1,88 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.forms import UserChangeForm, UserCreationForm from dal import autocomplete
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django import forms from django import forms
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap from crispy_forms.bootstrap import Div
from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field
from crispy_forms.layout import Layout from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm):
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"})
class Meta:
model = User
fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
""" """
Forms pour la création d'un profile utilisateur. A form for the extras field provided by the :model:`member.Profile` model.
""" """
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
exclude = ['user'] exclude = ['user']
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):
class Meta: class Meta:
model = Club model = Club
fields ='__all__' fields = '__all__'
class AddMembersForm(forms.Form): class AddMembersForm(forms.Form):
class Meta: class Meta:
fields = ('',) fields = ('', )
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership
fields = ('user','roles','date_start') fields = ('user', 'roles', 'date_start')
# 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 noms d'utilisateur valides
widgets = {
'user':
autocomplete.ModelSelect2(
url='member:user_autocomplete',
attrs={
'data-placeholder': 'Nom ...',
'data-minimum-input-length': 1,
},
),
}
MemberFormSet = forms.modelformset_factory(
Membership,
form=MembershipForm,
extra=2,
can_delete=True,
)
MemberFormSet = forms.modelformset_factory(Membership,
form=MembershipForm,
extra=2,
can_delete=True)
class FormSetHelper(FormHelper): class FormSetHelper(FormHelper):
def __init__(self,*args,**kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs) super().__init__(*args, **kwargs)
self.form_tag = False self.form_tag = False
self.form_method = 'POST' self.form_method = 'POST'
self.form_class='form-inline' self.form_class = 'form-inline'
# self.template = 'bootstrap/table_inline_formset.html' # self.template = 'bootstrap/table_inline_formset.html'
self.layout = Layout( self.layout = Layout(
Div( Div(
Div('user',css_class='col-sm-2'), Div('user', css_class='col-sm-2'),
Div('roles',css_class='col-sm-2'), Div('roles', css_class='col-sm-2'),
Div('date_start',css_class='col-sm-2'), Div('date_start', css_class='col-sm-2'),
css_class="row formset-row", css_class="row formset-row",
) ))
)

27
apps/member/hashers.py Normal file
View File

@ -0,0 +1,27 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib
from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare
class CustomNK15Hasher(PBKDF2PasswordHasher):
"""
Permet d'importer les mots de passe depuis la Note KFet 2015.
Si un hash de mot de passe est de la forme :
`custom_nk15$<NB>$<ENCODED>`
<NB> est un entier quelconque (symbolisant normalement un nombre d'itérations)
et <ENCODED> le hash du mot de passe dans la Note Kfet 2015,
alors ce hasher va vérifier le mot de passe.
N'ayant pas la priorité (cf note_kfet/settings/base.py), le mot de passe sera
converti automatiquement avec l'algorithme PBKDF2.
"""
algorithm = "custom_nk15"
def verify(self, password, encoded):
if '|' in encoded:
salt, db_hashed_pass = encoded.split('$')[2].split('|')
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
return super().verify(password, encoded)

View File

@ -1,13 +1,10 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -16,8 +13,9 @@ class Profile(models.Model):
""" """
An user profile An user profile
We do not want to patch the Django Contrib Auth User class We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information. so this model add an user profile with additional information.
""" """
user = models.OneToOneField( user = models.OneToOneField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -52,12 +50,14 @@ class Profile(models.Model):
verbose_name_plural = _('user profile') verbose_name_plural = _('user profile')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('user_detail',args=(self.pk,)) return reverse('user_detail', args=(self.pk, ))
class Club(models.Model): class Club(models.Model):
""" """
A student club A club is a group of people, whose membership is handle by their
:model:`member.Membership`, and gives access to right defined by a :model:`member.Role`.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -100,12 +100,15 @@ class Club(models.Model):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,)) return reverse_lazy('member:club_detail', args=(self.pk, ))
class Role(models.Model): class Role(models.Model):
""" """
Role that an user can have in a club Role that an :model:`auth.User` can have in a :model:`member.Club`
TODO: Integrate the right management, and create some standard Roles at the
creation of the club.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -117,22 +120,26 @@ class Role(models.Model):
verbose_name = _('role') verbose_name = _('role')
verbose_name_plural = _('roles') verbose_name_plural = _('roles')
def __str__(self):
return str(self.name)
class Membership(models.Model): class Membership(models.Model):
""" """
Register the membership of a user to a club, including roles and membership duration. Register the membership of a user to a club, including roles and membership duration.
""" """
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
club = models.ForeignKey( club = models.ForeignKey(
Club, Club,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
roles = models.ForeignKey( roles = models.ForeignKey(
Role, Role,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
date_start = models.DateField( date_start = models.DateField(
verbose_name=_('membership starts on'), verbose_name=_('membership starts on'),

View File

@ -1,6 +1,15 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
def save_user_profile(instance, created, raw, **_kwargs):
"""
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()

View File

@ -1,13 +1,34 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables import django_tables2 as tables
from django.contrib.auth.models import User
from .models import Club from .models import Club
class ClubTable(tables.Table): class ClubTable(tables.Table):
class Meta: class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Club model = Club
template_name = 'django_tables2/bootstrap.html' template_name = 'django_tables2/bootstrap4.html'
fields= ('id','name','email') fields = ('id', 'name', 'email')
row_attrs = {'class':'table-row', row_attrs = {
'data-href': lambda record: record.pk } 'class': 'table-row',
'data-href': lambda record: record.pk
}
class UserTable(tables.Table):
section = tables.Column(accessor='profile.section')
solde = tables.Column(accessor='note.balance')
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path
@ -10,10 +7,18 @@ from . import views
app_name = 'member' app_name = 'member'
urlpatterns = [ urlpatterns = [
path('signup/',views.UserCreateView.as_view(),name="signup"), path('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/',views.ClubListView.as_view(),name="club_list"), path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/<int:pk>/',views.ClubDetailView.as_view(),name="club_detail"), 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/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/create/',views.ClubCreateView.as_view(),name="club_create"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail") 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"),
] ]

View File

@ -1,88 +1,333 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView from django.views.generic import CreateView, DetailView, UpdateView, TemplateView,DeleteView
from django.http import HttpResponseRedirect from django.views.generic.edit import FormMixin
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User
from django.contrib import messages
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import HttpResponseRedirect
from django.db.models import Q from django.db.models import Q
from django.core.exceptions import ValidationError
from django.conf import settings
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from dal import autocomplete
from PIL import Image
import io
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable, AliasTable
from note.forms import AliasForm, ImageForm
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
from .forms import ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .tables import ClubTable from .tables import ClubTable, UserTable
from note.models.transactions import Transaction from .filters import UserFilter, UserFilterFormHelper
from note.tables import HistoryTable
class UserCreateView(CreateView): class UserCreateView(CreateView):
""" """
Une vue pour inscrire un utilisateur et lui créer un profile Une vue pour inscrire un utilisateur et lui créer un profile
""" """
form_class = ProfileForm
success_url = reverse_lazy('login')
template_name ='member/signup.html'
second_form = UserCreationForm
def get_context_data(self,**kwargs): form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'member/signup.html'
second_form = ProfileForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["user_form"] = self.second_form context["profile_form"] = self.second_form()
return context return context
def form_valid(self, form): def form_valid(self, form):
user_form = UserCreationForm(self.request.POST) profile_form = ProfileForm(self.request.POST)
if user_form.is_valid(): if form.is_valid() and profile_form.is_valid():
user = user_form.save() user = form.save()
user_profile = form.save(commit=False) # do not save to db profile = profile_form.save(commit=False)
user_profile.user = user profile.user = user
user_profile.save() profile.save()
return super().form_valid(form) return super().form_valid(form)
class UserDetailView(LoginRequiredMixin,DetailView): class UserUpdateView(LoginRequiredMixin, UpdateView):
model = Profile model = User
context_object_name = "profile" fields = ['first_name', 'last_name', 'username', 'email']
def get_context_data(slef,**kwargs): template_name = 'member/profile_update.html'
context_object_name = 'user_object'
profile_form = ProfileForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['profile'].user 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):
profile_form = ProfileForm(
data=self.request.POST,
instance=self.object.profile,
)
if form.is_valid() and profile_form.is_valid():
new_username = form.data['username']
alias = Alias.objects.filter(name=new_username)
# Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
if not alias.exists():
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
if similar.exists():
similar.delete()
user = form.save(commit=False)
profile = profile_form.save(commit=False)
profile.user = user
profile.save()
user.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
if kwargs:
return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']})
else:
return reverse_lazy('member:user_detail', args=(self.object.id, ))
class UserDetailView(LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs...
"""
model = User
context_object_name = "user_object"
template_name = "member/profile_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = context['user_object']
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)) Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))
context['history_list'] = HistoryTable(history_list) context['history_list'] = HistoryTable(history_list)
club_list = \ club_list = \
Membership.objects.all().filter(user=user).only("club") Membership.objects.all().filter(user=user).only("club")
context['club_list'] = ClubTable(club_list) context['club_list'] = ClubTable(club_list)
context['title'] = _("Account #%(id)s: %(username)s") % {
'id': user.pk,
'username': user.username,
}
return context return context
class ClubCreateView(LoginRequiredMixin,CreateView): class UserListView(LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
"""
model = User
table_class = UserTable
template_name = 'member/user_list.html'
filter_class = UserFilter
formhelper_class = UserFilterFormHelper
def get_queryset(self, **kwargs):
qs = super().get_queryset()
self.filter = self.filter_class(self.request.GET, queryset=qs)
self.filter.form.helper = self.formhelper_class()
return self.filter.qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
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):
print(self.request)
return reverse_lazy('member:user_alias',kwargs={'pk':self.object.note.user.pk})
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
model = User
template_name = 'member/profile_picture_update.html'
context_object_name = 'user_object'
form_class = ImageForm
def get_context_data(self,*args,**kwargs):
context = super().get_context_data(*args,**kwargs)
context['form'] = self.form_class(self.request.POST,self.request.FILES)
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]
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,ext)
image_field.file = image_file
# renaming
filename = "{}_pic.{}".format(self.object.note.pk, ext)
image_field.name = filename
self.object.note.display_image = image_field
self.object.note.save()
return super().form_valid(form)
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
"""
Affiche le jeton d'authentification, et permet de le regénérer
"""
model = Token
template_name = "member/manage_auth_tokens.html"
def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(
user=request.user).exists():
Token.objects.get(user=self.request.user).delete()
return redirect(reverse_lazy('member:auth_token') + "?show",
permanent=True)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['token'] = Token.objects.get_or_create(
user=self.request.user)[0]
return context
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.
Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos.
"""
# Un utilisateur non connecté n'a accès à aucune information
if not self.request.user.is_authenticated:
return User.objects.none()
qs = User.objects.all()
if self.q:
qs = qs.filter(username__regex=self.q)
return qs
# ******************************* #
# CLUB #
# ******************************* #
class ClubCreateView(LoginRequiredMixin, CreateView):
""" """
Create Club Create Club
""" """
model = Club model = Club
form_class = ClubForm form_class = ClubForm
def form_valid(self,form): def form_valid(self, form):
return super().form_valid(form) return super().form_valid(form)
class ClubListView(LoginRequiredMixin,SingleTableView):
class ClubListView(LoginRequiredMixin, SingleTableView):
""" """
List existing tables List existing Clubs
""" """
model = Club model = Club
table_class = ClubTable table_class = ClubTable
class ClubDetailView(LoginRequiredMixin,DetailView):
model = Club
context_object_name="club"
def get_context_data(self,**kwargs): class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = context["club"] club = context["club"]
club_transactions = \ club_transactions = \
@ -93,24 +338,31 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
# TODO: consider only valid Membership # TODO: consider only valid Membership
context['member_list'] = club_member context['member_list'] = club_member
return context return context
class ClubAddMemberView(LoginRequiredMixin,CreateView):
class ClubAddMemberView(LoginRequiredMixin, CreateView):
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipForm
template_name = 'member/add_members.html' template_name = 'member/add_members.html'
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet() context['formset'] = MemberFormSet()
context['helper'] = FormSetHelper() context['helper'] = FormSetHelper()
return context
def post(self,request,*args,**kwargs):
formset = MembershipFormset(request.POST)
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self,formset): context['no_cache'] = True
return context
def post(self, request, *args, **kwargs):
return
# TODO: Implement POST
# formset = MembershipFormset(request.POST)
# if formset.is_valid():
# return self.form_valid(formset)
# else:
# return self.form_invalid(formset)
def form_valid(self, formset):
formset.save() formset.save()
return super().form_valid(formset) return super().form_valid(formset)

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig' default_app_config = 'note.apps.NoteConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
@ -8,7 +7,8 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TransactionTemplate from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
TemplateTransaction, MembershipTransaction
class AliasInlines(admin.TabularInline): class AliasInlines(admin.TabularInline):
@ -25,7 +25,10 @@ class NoteAdmin(PolymorphicParentModelAdmin):
Parent regrouping all note types as children Parent regrouping all note types as children
""" """
child_models = (NoteClub, NoteSpecial, NoteUser) child_models = (NoteClub, NoteSpecial, NoteUser)
list_filter = (PolymorphicChildModelFilter, 'is_active',) list_filter = (
PolymorphicChildModelFilter,
'is_active',
)
# Use a polymorphic list # Use a polymorphic list
list_display = ('pretty', 'balance', 'is_active') list_display = ('pretty', 'balance', 'is_active')
@ -44,11 +47,12 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
""" """
Child for a club note, see NoteAdmin Child for a club note, see NoteAdmin
""" """
inlines = (AliasInlines,) inlines = (AliasInlines, )
# We can't change club after creation or the balance # We can't change club after creation or the balance
readonly_fields = ('club', 'balance') readonly_fields = ('club', 'balance')
search_fields = ('club',) search_fields = ('club', )
def has_add_permission(self, request): def has_add_permission(self, request):
""" """
A club note should not be manually added A club note should not be manually added
@ -67,7 +71,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin):
""" """
Child for a special note, see NoteAdmin Child for a special note, see NoteAdmin
""" """
readonly_fields = ('balance',) readonly_fields = ('balance', )
@admin.register(NoteUser) @admin.register(NoteUser)
@ -75,7 +79,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
""" """
Child for an user note, see NoteAdmin Child for an user note, see NoteAdmin
""" """
inlines = (AliasInlines,) inlines = (AliasInlines, )
# We can't change user after creation or the balance # We can't change user after creation or the balance
readonly_fields = ('user', 'balance') readonly_fields = ('user', 'balance')
@ -94,14 +98,18 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
@admin.register(Transaction) @admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(PolymorphicParentModelAdmin):
""" """
Admin customisation for Transaction Admin customisation for Transaction
""" """
child_models = (TemplateTransaction, MembershipTransaction)
list_display = ('created_at', 'poly_source', 'poly_destination', list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'transaction_type', 'valid') 'quantity', 'amount', 'valid')
list_filter = ('transaction_type', 'valid') list_filter = ('valid',)
autocomplete_fields = ('source', 'destination',) autocomplete_fields = (
'source',
'destination',
)
def poly_source(self, obj): def poly_source(self, obj):
""" """
@ -126,7 +134,7 @@ class TransactionAdmin(admin.ModelAdmin):
""" """
if obj: # user is editing an existing object if obj: # user is editing an existing object
return 'created_at', 'source', 'destination', 'quantity',\ return 'created_at', 'source', 'destination', 'quantity',\
'amount', 'transaction_type' 'amount'
return [] return []
@ -135,9 +143,9 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
""" """
Admin customisation for TransactionTemplate Admin customisation for TransactionTemplate
""" """
list_display = ('name', 'poly_destination', 'amount', 'template_type') list_display = ('name', 'poly_destination', 'amount', 'category', 'display', )
list_filter = ('template_type',) list_filter = ('category', 'display')
autocomplete_fields = ('destination',) autocomplete_fields = ('destination', )
def poly_destination(self, obj): def poly_destination(self, obj):
""" """
@ -146,3 +154,12 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
return str(obj.destination) return str(obj.destination)
poly_destination.short_description = _('destination') poly_destination.short_description = _('destination')
@admin.register(TemplateCategory)
class TemplateCategoryAdmin(admin.ModelAdmin):
"""
Admin customisation for TransactionTemplate
"""
list_display = ('name', )
list_filter = ('name', )

View File

View File

@ -0,0 +1,103 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
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
class NoteSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Notes.
The djangorestframework plugin will analyse the model `Note` and parse all fields in the API.
"""
class Meta:
model = Note
fields = '__all__'
extra_kwargs = {
'url': {
'view_name': 'project-detail',
'lookup_field': 'pk'
},
}
class NoteClubSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Club's notes.
The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
"""
class Meta:
model = NoteClub
fields = '__all__'
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.
"""
class Meta:
model = NoteSpecial
fields = '__all__'
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.
"""
class Meta:
model = NoteUser
fields = '__all__'
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__'
class NotePolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Note: NoteSerializer,
NoteUser: NoteUserSerializer,
NoteClub: NoteClubSerializer,
NoteSpecial: NoteSpecialSerializer
}
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__'
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 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__'

17
apps/note/api/urls.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, \
TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
def register_note_urls(router, path):
"""
Configure router for Note REST API.
"""
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet)
router.register(path + '/transaction/template', TransactionTemplateViewSet)
router.register(path + '/transaction/membership', MembershipTransactionViewSet)

161
apps/note/api/views.py Normal file
View File

@ -0,0 +1,161 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db.models import Q
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
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):
"""
REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
then render it on /api/note/note/
"""
queryset = Note.objects.all()
serializer_class = NotePolymorphicSerializer
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested notes
"""
queryset = Note.objects.all()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
Q(alias__name__regex=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
class AliasViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/aliases/
"""
queryset = Alias.objects.all()
serializer_class = AliasSerializer
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested aliases
"""
queryset = Alias.objects.all()
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()
return queryset
class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/template/
"""
queryset = TransactionTemplate.objects.all()
serializer_class = TransactionTemplateSerializer
class TransactionViewSet(viewsets.ModelViewSet):
"""
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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig
@ -20,9 +19,9 @@ class NoteConfig(AppConfig):
""" """
post_save.connect( post_save.connect(
signals.save_user_note, signals.save_user_note,
sender=settings.AUTH_USER_MODEL sender=settings.AUTH_USER_MODEL,
) )
post_save.connect( post_save.connect(
signals.save_club_note, signals.save_club_note,
sender='member.Club' sender='member.Club',
) )

View File

@ -0,0 +1,220 @@
[
{
"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"
}
}
]

View File

@ -1,9 +1,139 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django import forms from django import forms
from .models import TransactionTemplate from django.conf import settings
from django.utils.translation import gettext_lazy as _
import os
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout, HTML
from .models import Transaction, TransactionTemplate, TemplateTransaction
from .models import Note, Alias
class AliasForm(forms.ModelForm):
class Meta:
model = Alias
fields = ("name",)
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
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): class TransactionTemplateForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionTemplate model = TransactionTemplate
fields ='__all__' fields = '__all__'
# Le champ de destination 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 valides
# Pour force le type d'une note, il faut rajouter le paramètre :
# 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,
},
),
}

View File

@ -1,14 +1,14 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \ from .transactions import MembershipTransaction, Transaction, \
TransactionTemplate TemplateCategory, TransactionTemplate, TemplateTransaction
__all__ = [ __all__ = [
# Notes # Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions # Transactions
'MembershipTransaction', 'Transaction', 'TransactionTemplate', 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'TemplateTransaction',
] ]

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata import unicodedata
@ -10,7 +9,6 @@ from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
""" """
Defines each note types Defines each note types
""" """
@ -18,25 +16,37 @@ Defines each note types
class Note(PolymorphicModel): class Note(PolymorphicModel):
""" """
An model, use to add transactions capabilities Gives transactions capabilities. Note is a Polymorphic Model, use as based
for the models :model:`note.NoteUser` and :model:`note.NoteClub`.
A Note principaly store the actual balance of someone/some club.
A Note can be searched find throught an :model:`note.Alias`
""" """
balance = models.IntegerField( balance = models.IntegerField(
verbose_name=_('account balance'), verbose_name=_('account balance'),
help_text=_('in centimes, money credited for this instance'), help_text=_('in centimes, money credited for this instance'),
default=0, default=0,
) )
last_negative= models.DateTimeField(
verbose_name=_('last negative date'),
help_text=_('last time the balance was negative'),
null=True,
blank=True,
)
is_active = models.BooleanField( is_active = models.BooleanField(
_('active'), _('active'),
default=True, default=True,
help_text=_( help_text=_(
'Designates whether this note should be treated as active. ' 'Designates whether this note should be treated as active. '
'Unselect this instead of deleting notes.' 'Unselect this instead of deleting notes.'),
),
) )
display_image = models.ImageField( display_image = models.ImageField(
verbose_name=_('display image'), verbose_name=_('display image'),
max_length=255, max_length=255,
blank=True, blank=False,
null=False,
upload_to='pic/',
default='pic/default.png'
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
verbose_name=_('created at'), verbose_name=_('created at'),
@ -63,7 +73,8 @@ class Note(PolymorphicModel):
if aliases.exists(): if aliases.exists():
# Alias exists, so check if it is linked to this note # Alias exists, so check if it is linked to this note
if aliases.first().note != self: if aliases.first().note != self:
raise ValidationError(_('This alias is already taken.')) raise ValidationError(_('This alias is already taken.'),
code="same_alias")
# Save note # Save note
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -81,11 +92,13 @@ class Note(PolymorphicModel):
""" """
Verify alias (simulate save) Verify alias (simulate save)
""" """
aliases = Alias.objects.filter(name=str(self)) aliases = Alias.objects.filter(
normalized_name=Alias.normalize(str(self)))
if aliases.exists(): if aliases.exists():
# Alias exists, so check if it is linked to this note # Alias exists, so check if it is linked to this note
if aliases.first().note != self: if aliases.first().note != self:
raise ValidationError(_('This alias is already taken.')) raise ValidationError(_('This alias is already taken.'),
code="same_alias",)
else: else:
# Alias does not exist yet, so check if it can exist # Alias does not exist yet, so check if it can exist
a = Alias(name=str(self)) a = Alias(name=str(self))
@ -94,7 +107,7 @@ class Note(PolymorphicModel):
class NoteUser(Note): class NoteUser(Note):
""" """
A Note associated to an User A :model:`note.Note` associated to an unique :model:`auth.User`.
""" """
user = models.OneToOneField( user = models.OneToOneField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
@ -116,7 +129,7 @@ class NoteUser(Note):
class NoteClub(Note): class NoteClub(Note):
""" """
A Note associated to a Club A :model:`note.Note` associated to an unique :model:`member.Club`
""" """
club = models.OneToOneField( club = models.OneToOneField(
'member.Club', 'member.Club',
@ -133,17 +146,18 @@ class NoteClub(Note):
return str(self.club) return str(self.club)
def pretty(self): def pretty(self):
return _("Note for %(club)s club") % {'club': str(self.club)} return _("Note of %(club)s club") % {'club': str(self.club)}
class NoteSpecial(Note): class NoteSpecial(Note):
""" """
A Note for special account, where real money enter or leave the system A :model:`note.Note` for special accounts, where real money enter or leave the system
- bank check - bank check
- credit card - credit card
- bank transfer - bank transfer
- cash - cash
- refund - refund
This Type of Note is not associated to a :model:`auth.User` or :model:`member.Club` .
""" """
special_type = models.CharField( special_type = models.CharField(
verbose_name=_('type'), verbose_name=_('type'),
@ -161,7 +175,13 @@ class NoteSpecial(Note):
class Alias(models.Model): class Alias(models.Model):
""" """
An alias labels a Note instance, only for user and clubs points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
Alias are unique, but a :model:`note.NoteUser` or :model:`note.NoteClub` can
have multiples aliases.
Aliases name are also normalized, two differents :model:`note.Note` can not
have the same normalized alias, to avoid confusion when referring orally to
it.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -170,15 +190,15 @@ class Alias(models.Model):
validators=[ validators=[
RegexValidator( RegexValidator(
regex=settings.ALIAS_VALIDATOR_REGEX, regex=settings.ALIAS_VALIDATOR_REGEX,
message=_('Invalid alias') message=_('Invalid alias'),
) )
] if settings.ALIAS_VALIDATOR_REGEX else [] ] if settings.ALIAS_VALIDATOR_REGEX else [],
) )
normalized_name = models.CharField( normalized_name = models.CharField(
max_length=255, max_length=255,
unique=True, unique=True,
default='', default='',
editable=False editable=False,
) )
note = models.ForeignKey( note = models.ForeignKey(
Note, Note,
@ -198,27 +218,27 @@ class Alias(models.Model):
Normalizes a string: removes most diacritics and does casefolding Normalizes a string: removes most diacritics and does casefolding
""" """
return ''.join( return ''.join(
char char for char in unicodedata.normalize('NFKD', string.casefold())
for char in unicodedata.normalize('NFKD', string.casefold())
if all(not unicodedata.category(char).startswith(cat) if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'}) for cat in {'M', 'P', 'Z', 'C'})).casefold()
).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): def clean(self):
normalized_name = Alias.normalize(self.name) normalized_name = Alias.normalize(self.name)
if len(normalized_name) >= 255: if len(normalized_name) >= 255:
raise ValidationError(_('Alias too long.')) raise ValidationError(_('Alias is too long.'),
code='alias_too_long')
try: try:
if self != Alias.objects.get(normalized_name=normalized_name): sim_alias = Alias.objects.get(normalized_name=normalized_name)
raise ValidationError(_('An alias with a similar name ' if self != sim_alias:
'already exists.')) raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)),
code="same_alias"
)
except Alias.DoesNotExist: except Alias.DoesNotExist:
pass pass
self.normalized_name = normalized_name
def delete(self, using=None, keep_parents=False):
if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."),
code="cant_delete_main_alias")
return super().delete(using, keep_parents)

View File

@ -1,24 +1,50 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse from django.urls import reverse
from polymorphic.models import PolymorphicModel
from .notes import Note,NoteClub from .notes import Note, NoteClub
""" """
Defines transactions Defines transactions
""" """
class TemplateCategory(models.Model):
"""
Defined a recurrent transaction category
Example: food, softs, ...
"""
name = models.CharField(
verbose_name=_("name"),
max_length=31,
unique=True,
)
class Meta:
verbose_name = _("transaction category")
verbose_name_plural = _("transaction categories")
def __str__(self):
return str(self.name)
class TransactionTemplate(models.Model): class TransactionTemplate(models.Model):
"""
Defined a recurrent transaction
associated to selling something (a burger, a beer, ...)
"""
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=255, max_length=255,
unique=True, unique=True,
error_messages={'unique':_("A template with this name already exist")},
) )
destination = models.ForeignKey( destination = models.ForeignKey(
NoteClub, NoteClub,
@ -30,9 +56,18 @@ class TransactionTemplate(models.Model):
verbose_name=_('amount'), verbose_name=_('amount'),
help_text=_('in centimes'), help_text=_('in centimes'),
) )
template_type = models.CharField( category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
verbose_name=_('type'), verbose_name=_('type'),
max_length=31 max_length=31,
)
display = models.BooleanField(
default = True,
)
description = models.CharField(
verbose_name=_('description'),
max_length=255,
) )
class Meta: class Meta:
@ -40,10 +75,17 @@ class TransactionTemplate(models.Model):
verbose_name_plural = _("transaction templates") verbose_name_plural = _("transaction templates")
def get_absolute_url(self): def get_absolute_url(self):
return reverse('note:template_update',args=(self.pk,)) return reverse('note:template_update', args=(self.pk, ))
class Transaction(models.Model): class Transaction(PolymorphicModel):
"""
General transaction between two :model:`note.Note`
amount is store in centimes of currency, making it a positive integer
value. (from someone to someone else)
"""
source = models.ForeignKey( source = models.ForeignKey(
Note, Note,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -64,13 +106,7 @@ class Transaction(models.Model):
verbose_name=_('quantity'), verbose_name=_('quantity'),
default=1, default=1,
) )
amount = models.PositiveIntegerField( amount = models.PositiveIntegerField(verbose_name=_('amount'), )
verbose_name=_('amount'),
)
transaction_type = models.CharField(
verbose_name=_('type'),
max_length=31,
)
reason = models.CharField( reason = models.CharField(
verbose_name=_('reason'), verbose_name=_('reason'),
max_length=255, max_length=255,
@ -88,6 +124,11 @@ class Transaction(models.Model):
""" """
When saving, also transfer money between two notes When saving, also transfer money between two notes
""" """
if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered
return
created = self.pk is None created = self.pk is None
to_transfer = self.amount * self.quantity to_transfer = self.amount * self.quantity
if not created: if not created:
@ -108,10 +149,31 @@ class Transaction(models.Model):
@property @property
def total(self): def total(self):
return self.amount*self.quantity return self.amount * self.quantity
class TemplateTransaction(Transaction):
"""
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
"""
template = models.ForeignKey(
TransactionTemplate,
null=True,
on_delete=models.SET_NULL,
)
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
)
class MembershipTransaction(Transaction): class MembershipTransaction(Transaction):
"""
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
"""
membership = models.OneToOneField( membership = models.OneToOneField(
'member.Membership', 'member.Membership',
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -1,22 +1,29 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
def save_user_note(instance, created, **_kwargs): def save_user_note(instance, created, raw, **_kwargs):
""" """
Hook to create and save a note when an user is updated Hook to create and save a note when an user is updated
""" """
if raw:
# When provisionning data, do not try to autocreate
return
if created: if created:
from .models import NoteUser from .models import NoteUser
NoteUser.objects.create(user=instance) NoteUser.objects.create(user=instance)
instance.note.save() instance.note.save()
def save_club_note(instance, created, **_kwargs): def save_club_note(instance, created, raw, **_kwargs):
""" """
Hook to create and save a note when a club is updated Hook to create and save a note when a club is updated
""" """
if raw:
# When provisionning data, do not try to autocreate
return
if created: if created:
from .models import NoteClub from .models import NoteClub
NoteClub.objects.create(club=instance) NoteClub.objects.create(club=instance)

View File

@ -1,20 +1,45 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
import django_tables2 as tables # SPDX-License-Identifier: GPL-3.0-or-later
from .models.transactions import Transaction
import django_tables2 as tables
from django.db.models import F
from django_tables2.utils import A
from .models.transactions import Transaction
from .models.notes import Alias
class HistoryTable(tables.Table): class HistoryTable(tables.Table):
class Meta: class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} attrs = {
'class':
'table table-condensed table-striped table-hover'
}
model = Transaction model = Transaction
template_name = 'django_tables2/bootstrap.html' template_name = 'django_tables2/bootstrap4.html'
sequence = ('...','total','valid') sequence = ('...', 'total', 'valid')
total = tables.Column() #will use Transaction.total() !! total = tables.Column() # will use Transaction.total() !!
def order_total(self, QuerySet, is_descending): def order_total(self, queryset, is_descending):
# needed for rendering # needed for rendering
QuerySet = QuerySet.annotate( queryset = queryset.annotate(total=F('amount') * F('quantity')) \
total=F('amount') * F('quantity') .order_by(('-' if is_descending else '') + 'total')
).order_by(('-' if is_descending else '') + 'total') return (queryset, True)
return (QuerySet, True)
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.LinkColumn('member:user_alias_delete',
args=[A('pk')],
attrs={
'td': {'class':'col-sm-2'},
'a': {'class': 'btn btn-danger'} },
text='delete',accessor='pk')

View File

@ -1,11 +1,21 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template
def pretty_money(value): def pretty_money(value):
if value%100 == 0: if value % 100 == 0:
return str(value//100) + '' return "{:s}{:d}".format(
"- " if value < 0 else "",
abs(value) // 100,
)
else: else:
return str(value//100) + '' + str(value%100) return "{:s}{:d}{:02d}".format(
"- " if value < 0 else "",
abs(value) // 100,
abs(value) % 100,
)
register = template.Library() register = template.Library()

View File

@ -1,15 +1,19 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path
from . import views from . import views
from .models import Note
app_name = 'note' app_name = 'note'
urlpatterns = [ urlpatterns = [
path('transfer/', views.TransactionCreate.as_view(), name='transfer'), path('transfer/', views.TransactionCreate.as_view(), name='transfer'),
path('buttons/create/',views.TransactionTemplateCreateView.as_view(),name='template_create'), path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'),
path('buttons/update/<int:pk>/',views.TransactionTemplateUpdateView.as_view(),name='template_update'), path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list') path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
path('consos/', views.ConsoView.as_view(), name='consos'),
# API for the note autocompleter
path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'),
] ]

View File

@ -1,13 +1,16 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView, UpdateView from django.views.generic import CreateView, ListView, UpdateView
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
from .models import Transaction,TransactionTemplate
from .forms import TransactionTemplateForm
class TransactionCreate(LoginRequiredMixin, CreateView): class TransactionCreate(LoginRequiredMixin, CreateView):
""" """
@ -16,7 +19,7 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
TODO: If user have sufficient rights, they can transfer from an other note TODO: If user have sufficient rights, they can transfer from an other note
""" """
model = Transaction model = Transaction
fields = ('destination', 'amount', 'reason') form_class = TransactionForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
@ -25,24 +28,123 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money from your account ' context['title'] = _('Transfer money from your account '
'to one or others') 'to one or others')
context['no_cache'] = True
return context return context
class TransactionTemplateCreateView(LoginRequiredMixin,CreateView): def get_form(self, form_class=None):
"""
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
"""
form = super().get_form(form_class)
if False: # TODO: fix it with "if %user has no right to transfer funds"
del form.fields['source']
form.user = self.request.user
return form
def get_success_url(self):
return reverse('note:transfer')
class NoteAutocomplete(autocomplete.Select2QuerySetView):
"""
Auto complete note by aliases
"""
def get_queryset(self):
"""
Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion.
Cette fonction récupère la requête, et renvoie la liste filtrée des aliases.
"""
# Un utilisateur non connecté n'a accès à aucune information
if not self.request.user.is_authenticated:
return Alias.objects.none()
qs = Alias.objects.all()
# 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)))\
.order_by('normalized_name').distinct()
# Filtrage par type de note (user, club, special)
note_type = self.forwarded.get("note_type", None)
if note_type:
types = str(note_type).lower()
if "user" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteuser")
elif "club" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteclub")
elif "special" in types:
qs = qs.filter(note__polymorphic_ctype__model="notespecial")
else:
qs = qs.none()
return qs
def get_result_label(self, result):
# Gère l'affichage de l'alias dans la recherche
res = result.name
note_name = str(result.note)
if res != note_name:
res += " (aka. " + note_name + ")"
return res
def get_result_value(self, result):
# Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias
return str(result.note.pk)
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
""" """
Create TransactionTemplate Create TransactionTemplate
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class TransactionTemplateListView(LoginRequiredMixin,ListView):
class TransactionTemplateListView(LoginRequiredMixin, ListView):
""" """
List TransactionsTemplates List TransactionsTemplates
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class TransactionTemplateUpdateView(LoginRequiredMixin,UpdateView):
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
""" """
""" """
model = TransactionTemplate model = TransactionTemplate
form_class=TransactionTemplateForm form_class = TransactionTemplateForm
class ConsoView(LoginRequiredMixin, CreateView):
"""
Consume
"""
model = TemplateTransaction
template_name = "note/conso_form.html"
form_class = ConsoForm
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")
# 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')

13
entrypoint.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
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

View File

@ -0,0 +1,581 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-27 17:39+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/activity/apps.py:10 apps/activity/models.py:76
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
msgid "name"
msgstr ""
#: apps/activity/models.py:23
msgid "can invite"
msgstr ""
#: apps/activity/models.py:26
msgid "guest entry fee"
msgstr ""
#: apps/activity/models.py:30
msgid "activity type"
msgstr ""
#: apps/activity/models.py:31
msgid "activity types"
msgstr ""
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
msgid "description"
msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:160
#: apps/note/models/transactions.py:62
msgid "type"
msgstr ""
#: apps/activity/models.py:60
msgid "organizer"
msgstr ""
#: apps/activity/models.py:66
msgid "attendees club"
msgstr ""
#: apps/activity/models.py:69
msgid "start date"
msgstr ""
#: apps/activity/models.py:72
msgid "end date"
msgstr ""
#: apps/activity/models.py:77
msgid "activities"
msgstr ""
#: apps/activity/models.py:108
msgid "guest"
msgstr ""
#: apps/activity/models.py:109
msgid "guests"
msgstr ""
#: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr ""
#: apps/logs/models.py:27
msgid "model"
msgstr ""
#: apps/logs/models.py:34
msgid "identifier"
msgstr ""
#: apps/logs/models.py:39
msgid "previous data"
msgstr ""
#: apps/logs/models.py:44
msgid "new data"
msgstr ""
#: apps/logs/models.py:51
msgid "action"
msgstr ""
#: apps/logs/models.py:59
msgid "timestamp"
msgstr ""
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr ""
#: apps/member/apps.py:10
msgid "member"
msgstr ""
#: apps/member/models.py:23
msgid "phone number"
msgstr ""
#: apps/member/models.py:29 templates/member/profile_detail.html:24
msgid "section"
msgstr ""
#: apps/member/models.py:30
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr ""
#: apps/member/models.py:36 templates/member/profile_detail.html:27
msgid "address"
msgstr ""
#: apps/member/models.py:42
msgid "paid"
msgstr ""
#: apps/member/models.py:47 apps/member/models.py:48
msgid "user profile"
msgstr ""
#: apps/member/models.py:65
msgid "email"
msgstr ""
#: apps/member/models.py:70
msgid "membership fee"
msgstr ""
#: apps/member/models.py:74
msgid "membership duration"
msgstr ""
#: apps/member/models.py:75
msgid "The longest time a membership can last (NULL = infinite)."
msgstr ""
#: apps/member/models.py:80
msgid "membership start"
msgstr ""
#: apps/member/models.py:81
msgid "How long after January 1st the members can renew their membership."
msgstr ""
#: apps/member/models.py:86
msgid "membership end"
msgstr ""
#: apps/member/models.py:87
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
msgid "club"
msgstr ""
#: apps/member/models.py:94
msgid "clubs"
msgstr ""
#: apps/member/models.py:117
msgid "role"
msgstr ""
#: apps/member/models.py:118
msgid "roles"
msgstr ""
#: apps/member/models.py:142
msgid "membership starts on"
msgstr ""
#: apps/member/models.py:145
msgid "membership ends on"
msgstr ""
#: apps/member/models.py:149
msgid "fee"
msgstr ""
#: apps/member/models.py:153
msgid "membership"
msgstr ""
#: apps/member/models.py:154
msgid "memberships"
msgstr ""
#: apps/member/views.py:63 templates/member/profile_detail.html:42
msgid "Update Profile"
msgstr ""
#: apps/member/views.py:79
msgid "An alias with a similar name already exists."
msgstr ""
#: apps/member/views.py:130
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
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
msgid "destination"
msgstr ""
#: apps/note/apps.py:14 apps/note/models/notes.py:54
msgid "note"
msgstr ""
#: apps/note/forms.py:49
msgid "Source and destination must be different."
msgstr ""
#: apps/note/models/notes.py:26
msgid "account balance"
msgstr ""
#: apps/note/models/notes.py:27
msgid "in centimes, money credited for this instance"
msgstr ""
#: apps/note/models/notes.py:31
msgid "last negative date"
msgstr ""
#: apps/note/models/notes.py:32
msgid "last time the balance was negative"
msgstr ""
#: apps/note/models/notes.py:37
msgid "active"
msgstr ""
#: apps/note/models/notes.py:40
msgid ""
"Designates whether this note should be treated as active. Unselect this "
"instead of deleting notes."
msgstr ""
#: apps/note/models/notes.py:44
msgid "display image"
msgstr ""
#: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
msgid "created at"
msgstr ""
#: apps/note/models/notes.py:55
msgid "notes"
msgstr ""
#: apps/note/models/notes.py:63
msgid "Note"
msgstr ""
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97
msgid "This alias is already taken."
msgstr ""
#: apps/note/models/notes.py:113
msgid "user"
msgstr ""
#: apps/note/models/notes.py:117
msgid "one's note"
msgstr ""
#: apps/note/models/notes.py:118
msgid "users note"
msgstr ""
#: apps/note/models/notes.py:124
#, python-format
msgid "%(user)s's note"
msgstr ""
#: apps/note/models/notes.py:139
msgid "club note"
msgstr ""
#: apps/note/models/notes.py:140
msgid "clubs notes"
msgstr ""
#: apps/note/models/notes.py:146
#, python-format
msgid "Note of %(club)s club"
msgstr ""
#: apps/note/models/notes.py:166
msgid "special note"
msgstr ""
#: apps/note/models/notes.py:167
msgid "special notes"
msgstr ""
#: apps/note/models/notes.py:190
msgid "Invalid alias"
msgstr ""
#: apps/note/models/notes.py:206
msgid "alias"
msgstr ""
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
msgid "aliases"
msgstr ""
#: apps/note/models/notes.py:233
msgid "Alias is too long."
msgstr ""
#: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists:"
msgstr ""
#: apps/note/models/notes.py:246
msgid "You can't delete your main alias."
msgstr ""
#: apps/note/models/transactions.py:30
msgid "transaction category"
msgstr ""
#: apps/note/models/transactions.py:31
msgid "transaction categories"
msgstr ""
#: apps/note/models/transactions.py:47
msgid "A template with this name already exist"
msgstr ""
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
msgid "amount"
msgstr ""
#: apps/note/models/transactions.py:57
msgid "in centimes"
msgstr ""
#: apps/note/models/transactions.py:74
msgid "transaction template"
msgstr ""
#: apps/note/models/transactions.py:75
msgid "transaction templates"
msgstr ""
#: apps/note/models/transactions.py:106
msgid "quantity"
msgstr ""
#: apps/note/models/transactions.py:111
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:115
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:120
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:121
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:184
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:185
msgid "membership transactions"
msgstr ""
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
msgstr ""
#: apps/note/views.py:138
msgid "Consommations"
msgstr ""
#: note_kfet/settings/base.py:155
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:156
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:157
msgid "French"
msgstr ""
#: templates/base.html:13
msgid "The ENS Paris-Saclay BDE note."
msgstr ""
#: templates/member/club_detail.html:10
msgid "Membership starts on"
msgstr ""
#: templates/member/club_detail.html:12
msgid "Membership ends on"
msgstr ""
#: templates/member/club_detail.html:14
msgid "Membership duration"
msgstr ""
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
msgid "balance"
msgstr ""
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr ""
#: templates/member/manage_auth_tokens.html:23
msgid "Created"
msgstr ""
#: templates/member/manage_auth_tokens.html:31
msgid "Regenerate token"
msgstr ""
#: templates/member/profile_detail.html:11
msgid "first name"
msgstr ""
#: templates/member/profile_detail.html:14
msgid "username"
msgstr ""
#: templates/member/profile_detail.html:17
msgid "password"
msgstr ""
#: templates/member/profile_detail.html:20
msgid "Change password"
msgstr ""
#: templates/member/profile_detail.html:38
msgid "Manage auth token"
msgstr ""
#: templates/member/profile_detail.html:54
msgid "View my memberships"
msgstr ""
#: templates/member/profile_update.html:13
msgid "Save Changes"
msgstr ""
#: templates/member/signup.html:14
msgid "Sign Up"
msgstr ""
#: templates/note/transaction_form.html:35
msgid "Transfer"
msgstr ""
#: templates/registration/logged_out.html:8
msgid "Thanks for spending some quality time with the Web site today."
msgstr ""
#: templates/registration/logged_out.html:9
msgid "Log in again"
msgstr ""
#: templates/registration/login.html:7 templates/registration/login.html:8
#: templates/registration/login.html:22
#: templates/registration/password_reset_complete.html:10
msgid "Log in"
msgstr ""
#: templates/registration/login.html:13
#, python-format
msgid ""
"You are authenticated as %(username)s, but are not authorized to access this "
"page. Would you like to login to a different account?"
msgstr ""
#: templates/registration/login.html:23
msgid "Forgotten your password or username?"
msgstr ""
#: templates/registration/password_change_done.html:8
msgid "Your password was changed."
msgstr ""
#: templates/registration/password_change_form.html:9
msgid ""
"Please enter your old password, for security's sake, and then enter your new "
"password twice so we can verify you typed it in correctly."
msgstr ""
#: templates/registration/password_change_form.html:11
#: templates/registration/password_reset_confirm.html:12
msgid "Change my password"
msgstr ""
#: templates/registration/password_reset_complete.html:8
msgid "Your password has been set. You may go ahead and log in now."
msgstr ""
#: templates/registration/password_reset_confirm.html:9
msgid ""
"Please enter your new password twice so we can verify you typed it in "
"correctly."
msgstr ""
#: templates/registration/password_reset_confirm.html:15
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
msgstr ""
#: templates/registration/password_reset_done.html:8
msgid ""
"We've emailed you instructions for setting your password, if an account "
"exists with the email you entered. You should receive them shortly."
msgstr ""
#: templates/registration/password_reset_done.html:9
msgid ""
"If you don't receive an email, please make sure you've entered the address "
"you registered with, and check your spam folder."
msgstr ""
#: templates/registration/password_reset_form.html:8
msgid ""
"Forgotten your password? Enter your email address below, and we'll email "
"instructions for setting a new one."
msgstr ""
#: templates/registration/password_reset_form.html:11
msgid "Reset my password"
msgstr ""

View File

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-14 15:14+0200\n" "POT-Creation-Date: 2020-02-27 17:39+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -13,356 +13,500 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: apps/activity/apps.py:11 apps/activity/models.py:70 #: apps/activity/apps.py:10 apps/activity/models.py:76
msgid "activity" msgid "activity"
msgstr "activité" msgstr "activité"
#: apps/activity/models.py:15 apps/activity/models.py:38 #: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:59 apps/member/models.py:107 #: apps/member/models.py:60 apps/member/models.py:111
#: apps/note/models/notes.py:167 apps/note/models/transactions.py:19 #: apps/note/models/notes.py:184 apps/note/models/transactions.py:24
#: templates/member/profile_detail.html:10 #: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: apps/activity/models.py:19 #: apps/activity/models.py:23
msgid "can invite" msgid "can invite"
msgstr "peut inviter" msgstr "peut inviter"
#: apps/activity/models.py:22 #: apps/activity/models.py:26
msgid "guest entry fee" msgid "guest entry fee"
msgstr "cotisation de l'entrée invité" msgstr "cotisation de l'entrée invité"
#: apps/activity/models.py:26 #: apps/activity/models.py:30
msgid "activity type" msgid "activity type"
msgstr "type d'activité" msgstr "type d'activité"
#: apps/activity/models.py:27 #: apps/activity/models.py:31
msgid "activity types" msgid "activity types"
msgstr "types d'activité" msgstr "types d'activité"
#: apps/activity/models.py:42 #: apps/activity/models.py:48 apps/note/models/transactions.py:69
msgid "description" msgid "description"
msgstr "description" msgstr "description"
#: apps/activity/models.py:48 apps/note/models/notes.py:149 #: apps/activity/models.py:54 apps/note/models/notes.py:160
#: apps/note/models/transactions.py:34 apps/note/models/transactions.py:71 #: apps/note/models/transactions.py:62
msgid "type" msgid "type"
msgstr "type" msgstr "type"
#: apps/activity/models.py:54 #: apps/activity/models.py:60
msgid "organizer" msgid "organizer"
msgstr "organisateur" msgstr "organisateur"
#: apps/activity/models.py:60 #: apps/activity/models.py:66
msgid "attendees club" msgid "attendees club"
msgstr "" msgstr ""
#: apps/activity/models.py:63 #: apps/activity/models.py:69
msgid "start date" msgid "start date"
msgstr "date de début" msgstr "date de début"
#: apps/activity/models.py:66 #: apps/activity/models.py:72
msgid "end date" msgid "end date"
msgstr "date de fin" msgstr "date de fin"
#: apps/activity/models.py:71 #: apps/activity/models.py:77
msgid "activities" msgid "activities"
msgstr "activités" msgstr "activités"
#: apps/activity/models.py:100 #: apps/activity/models.py:108
msgid "guest" msgid "guest"
msgstr "invité" msgstr "invité"
#: apps/activity/models.py:101 #: apps/activity/models.py:109
msgid "guests" msgid "guests"
msgstr "invités" msgstr "invités"
#: apps/member/apps.py:11 #: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr "utilisateur"
#: apps/logs/models.py:27
msgid "model"
msgstr "Modèle"
#: apps/logs/models.py:34
msgid "identifier"
msgstr "Identifiant"
#: apps/logs/models.py:39
msgid "previous data"
msgstr "Données précédentes"
#: apps/logs/models.py:44
#, fuzzy
#| msgid "end date"
msgid "new data"
msgstr "Nouvelles données"
#: apps/logs/models.py:51
#, fuzzy
#| msgid "section"
msgid "action"
msgstr "Action"
#: apps/logs/models.py:59
msgid "timestamp"
msgstr "Date"
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr "Les logs ne peuvent pas être détruits."
#: apps/member/apps.py:10
msgid "member" msgid "member"
msgstr "adhérent" msgstr "adhérent"
#: apps/member/models.py:24 #: apps/member/models.py:23
msgid "phone number" msgid "phone number"
msgstr "numéro de téléphone" msgstr "numéro de téléphone"
#: apps/member/models.py:30 templates/member/profile_detail.html:18 #: apps/member/models.py:29 templates/member/profile_detail.html:24
msgid "section" msgid "section"
msgstr "section" msgstr "section"
#: apps/member/models.py:31 #: apps/member/models.py:30
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
#: apps/member/models.py:37 templates/member/profile_detail.html:20 #: apps/member/models.py:36 templates/member/profile_detail.html:27
msgid "address" msgid "address"
msgstr "adresse" msgstr "adresse"
#: apps/member/models.py:43 #: apps/member/models.py:42
msgid "paid" msgid "paid"
msgstr "payé" msgstr "payé"
#: apps/member/models.py:48 apps/member/models.py:49 #: apps/member/models.py:47 apps/member/models.py:48
msgid "user profile" msgid "user profile"
msgstr "profil utilisateur" msgstr "profil utilisateur"
#: apps/member/models.py:64 #: apps/member/models.py:65
msgid "email" msgid "email"
msgstr "courriel" msgstr "courriel"
#: apps/member/models.py:69 #: apps/member/models.py:70
msgid "membership fee" msgid "membership fee"
msgstr "cotisation pour adhérer" msgstr "cotisation pour adhérer"
#: apps/member/models.py:73 #: apps/member/models.py:74
msgid "membership duration" msgid "membership duration"
msgstr "durée de l'adhésion" msgstr "durée de l'adhésion"
#: apps/member/models.py:74 #: apps/member/models.py:75
msgid "The longest time a membership can last (NULL = infinite)." msgid "The longest time a membership can last (NULL = infinite)."
msgstr "La durée maximale d'une adhésion (NULL = infinie)." msgstr "La durée maximale d'une adhésion (NULL = infinie)."
#: apps/member/models.py:79 #: apps/member/models.py:80
msgid "membership start" msgid "membership start"
msgstr "début de l'adhésion" msgstr "début de l'adhésion"
#: apps/member/models.py:80 #: apps/member/models.py:81
msgid "How long after January 1st the members can renew their membership." msgid "How long after January 1st the members can renew their membership."
msgstr "" msgstr ""
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
"adhésion."
#: apps/member/models.py:85 #: apps/member/models.py:86
msgid "membership end" msgid "membership end"
msgstr "fin de l'adhésion" msgstr "fin de l'adhésion"
#: apps/member/models.py:86 #: apps/member/models.py:87
msgid "" msgid ""
"How long the membership can last after January 1st of the next year after " "How long the membership can last after January 1st of the next year after "
"members can renew their membership." "members can renew their membership."
msgstr "" msgstr ""
"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:92 apps/note/models/notes.py:125 #: apps/member/models.py:93 apps/note/models/notes.py:135
msgid "club" msgid "club"
msgstr "club" msgstr "club"
#: apps/member/models.py:93 #: apps/member/models.py:94
msgid "clubs" msgid "clubs"
msgstr "clubs" msgstr "clubs"
#: apps/member/models.py:113 #: apps/member/models.py:117
msgid "role" msgid "role"
msgstr "rôle" msgstr "rôle"
#: apps/member/models.py:114 #: apps/member/models.py:118
msgid "roles" msgid "roles"
msgstr "rôles" msgstr "rôles"
#: apps/member/models.py:134 #: apps/member/models.py:142
msgid "membership starts on" msgid "membership starts on"
msgstr "l'adhésion commence le" msgstr "l'adhésion commence le"
#: apps/member/models.py:137 #: apps/member/models.py:145
msgid "membership ends on" msgid "membership ends on"
msgstr "l'adhésion finie le" msgstr "l'adhésion finie le"
#: apps/member/models.py:141 #: apps/member/models.py:149
msgid "fee" msgid "fee"
msgstr "cotisation" msgstr "cotisation"
#: apps/member/models.py:145 #: apps/member/models.py:153
msgid "membership" msgid "membership"
msgstr "adhésion" msgstr "adhésion"
#: apps/member/models.py:146 #: apps/member/models.py:154
msgid "memberships" msgid "memberships"
msgstr "adhésions" msgstr "adhésions"
#: apps/note/admin.py:112 apps/note/models/transactions.py:51 #: apps/member/views.py:63 templates/member/profile_detail.html:42
msgid "Update Profile"
msgstr "Modifier le profil"
#: apps/member/views.py:79
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/views.py:130
#, 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
msgid "source" msgid "source"
msgstr "source" msgstr "source"
#: apps/note/admin.py:120 apps/note/admin.py:148 #: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:27 apps/note/models/transactions.py:57 #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
msgid "destination" msgid "destination"
msgstr "destination" msgstr "destination"
#: apps/note/apps.py:15 apps/note/models/notes.py:47 #: apps/note/apps.py:14 apps/note/models/notes.py:54
msgid "note" msgid "note"
msgstr "note" msgstr "note"
#: apps/note/models/notes.py:24 #: apps/note/forms.py:49
msgid "Source and destination must be different."
msgstr "La source et la destination doivent être différentes."
#: apps/note/models/notes.py:26
msgid "account balance" msgid "account balance"
msgstr "solde du compte" msgstr "solde du compte"
#: apps/note/models/notes.py:25 #: apps/note/models/notes.py:27
msgid "in centimes, money credited for this instance" msgid "in centimes, money credited for this instance"
msgstr "en centimes, argent crédité pour cette instance" msgstr "en centimes, argent crédité pour cette instance"
#: apps/note/models/notes.py:29 #: apps/note/models/notes.py:31
msgid "last negative date"
msgstr "dernier date de négatif"
#: apps/note/models/notes.py:32
msgid "last time the balance was negative"
msgstr "dernier instant où la note était en négatif"
#: apps/note/models/notes.py:37
msgid "active" msgid "active"
msgstr "actif" msgstr "actif"
#: apps/note/models/notes.py:32 #: apps/note/models/notes.py:40
msgid "" msgid ""
"Designates whether this note should be treated as active. Unselect this " "Designates whether this note should be treated as active. Unselect this "
"instead of deleting notes." "instead of deleting notes."
msgstr "" msgstr ""
"Indique si la note est active. Désactiver cela plutôt que supprimer la note." "Indique si la note est active. Désactiver cela plutôt que supprimer la note."
#: apps/note/models/notes.py:37 #: apps/note/models/notes.py:44
msgid "display image" msgid "display image"
msgstr "image affichée" msgstr "image affichée"
#: apps/note/models/notes.py:42 apps/note/models/transactions.py:60 #: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
msgid "created at" msgid "created at"
msgstr "créée le" msgstr "créée le"
#: apps/note/models/notes.py:48 #: apps/note/models/notes.py:55
msgid "notes" msgid "notes"
msgstr "notes" msgstr "notes"
#: apps/note/models/notes.py:56 #: apps/note/models/notes.py:63
msgid "Note" msgid "Note"
msgstr "Note" msgstr "Note"
#: apps/note/models/notes.py:66 apps/note/models/notes.py:88 #: apps/note/models/notes.py:73 apps/note/models/notes.py:97
msgid "This alias is already taken." msgid "This alias is already taken."
msgstr "Cet alias est déjà pris." msgstr "Cet alias est déjà pris."
#: apps/note/models/notes.py:103 #: apps/note/models/notes.py:113
msgid "user" msgid "user"
msgstr "utilisateur" msgstr "utilisateur"
#: apps/note/models/notes.py:107 #: apps/note/models/notes.py:117
msgid "one's note" msgid "one's note"
msgstr "note d'un utilisateur" msgstr "note d'un utilisateur"
#: apps/note/models/notes.py:108 #: apps/note/models/notes.py:118
msgid "users note" msgid "users note"
msgstr "notes des utilisateurs" msgstr "notes des utilisateurs"
#: apps/note/models/notes.py:114 #: apps/note/models/notes.py:124
#, python-format #, python-format
msgid "%(user)s's note" msgid "%(user)s's note"
msgstr "Note de %(user)s" msgstr "Note de %(user)s"
#: apps/note/models/notes.py:129 #: apps/note/models/notes.py:139
msgid "club note" msgid "club note"
msgstr "note d'un club" msgstr "note d'un club"
#: apps/note/models/notes.py:130 #: apps/note/models/notes.py:140
msgid "clubs notes" msgid "clubs notes"
msgstr "notes des clubs" msgstr "notes des clubs"
#: apps/note/models/notes.py:136 #: apps/note/models/notes.py:146
#, python-format #, python-format
msgid "Note for %(club)s club" msgid "Note of %(club)s club"
msgstr "Note du club %(club)s" msgstr "Note du club %(club)s"
#: apps/note/models/notes.py:155 #: apps/note/models/notes.py:166
msgid "special note" msgid "special note"
msgstr "note spéciale" msgstr "note spéciale"
#: apps/note/models/notes.py:156 #: apps/note/models/notes.py:167
msgid "special notes" msgid "special notes"
msgstr "notes spéciales" msgstr "notes spéciales"
#: apps/note/models/notes.py:173 #: apps/note/models/notes.py:190
msgid "Invalid alias" msgid "Invalid alias"
msgstr "Alias invalide" msgstr "Alias invalide"
#: apps/note/models/notes.py:189 #: apps/note/models/notes.py:206
msgid "alias" msgid "alias"
msgstr "alias" msgstr "alias"
#: apps/note/models/notes.py:190 #: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
msgid "aliases" msgid "aliases"
msgstr "alias" msgstr "alias"
#: apps/note/models/notes.py:218 #: apps/note/models/notes.py:233
msgid "Alias too long." msgid "Alias is too long."
msgstr "L'alias est trop long." msgstr "L'alias est trop long."
#: apps/note/models/notes.py:221 #: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists." msgid "An alias with a similar name already exists:"
msgstr "Un alias avec un nom similaire existe déjà." msgstr "Un alias avec un nom similaire existe déjà."
#: apps/note/models/transactions.py:30 apps/note/models/transactions.py:68 #: apps/note/models/notes.py:246
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
#: apps/note/models/transactions.py:30
msgid "transaction category"
msgstr "catégorie de transaction"
#: apps/note/models/transactions.py:31
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
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
#: apps/note/models/transactions.py:31 #: apps/note/models/transactions.py:57
msgid "in centimes" msgid "in centimes"
msgstr "en centimes" msgstr "en centimes"
#: apps/note/models/transactions.py:39 #: apps/note/models/transactions.py:74
msgid "transaction template" msgid "transaction template"
msgstr "modèle de transaction" msgstr "modèle de transaction"
#: apps/note/models/transactions.py:40 #: apps/note/models/transactions.py:75
msgid "transaction templates" msgid "transaction templates"
msgstr "modèles de transaction" msgstr "modèles de transaction"
#: apps/note/models/transactions.py:64 #: apps/note/models/transactions.py:106
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: apps/note/models/transactions.py:75 #: apps/note/models/transactions.py:111
msgid "reason" msgid "reason"
msgstr "raison" msgstr "raison"
#: apps/note/models/transactions.py:79 #: apps/note/models/transactions.py:115
msgid "valid" msgid "valid"
msgstr "valide" msgstr "valide"
#: apps/note/models/transactions.py:84 #: apps/note/models/transactions.py:120
msgid "transaction" msgid "transaction"
msgstr "transaction" msgstr "transaction"
#: apps/note/models/transactions.py:85 #: apps/note/models/transactions.py:121
msgid "transactions" msgid "transactions"
msgstr "transactions" msgstr "transactions"
#: apps/note/models/transactions.py:118 #: apps/note/models/transactions.py:184
msgid "membership transaction" msgid "membership transaction"
msgstr "transaction d'adhésion" msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:119 #: apps/note/models/transactions.py:185
msgid "membership transactions" msgid "membership transactions"
msgstr "transactions d'adhésion" msgstr "transactions d'adhésion"
#: apps/note/views.py:26 #: apps/note/views.py:29
msgid "Transfer money from your account to one or others" msgid "Transfer money from your account to one or others"
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres" msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
#: note_kfet/settings.py:140 #: apps/note/views.py:138
msgid "Consommations"
msgstr "transactions"
#: note_kfet/settings/base.py:155
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:156
msgid "English" msgid "English"
msgstr "" msgstr ""
#: note_kfet/settings.py:141 #: note_kfet/settings/base.py:157
msgid "French" msgid "French"
msgstr "" msgstr ""
#: templates/base.html:14 #: templates/base.html:13
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "" msgstr "La note du BDE de l'ENS Paris-Saclay."
#: templates/member/profile_detail.html:12 #: templates/member/club_detail.html:10
msgid "Membership starts on"
msgstr "L'adhésion commence le"
#: templates/member/club_detail.html:12
msgid "Membership ends on"
msgstr "L'adhésion finie le"
#: templates/member/club_detail.html:14
msgid "Membership duration"
msgstr "Durée de l'adhésion"
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
msgid "balance"
msgstr "solde du compte"
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr "Jeton"
#: templates/member/manage_auth_tokens.html:23
msgid "Created"
msgstr "Créé le"
#: templates/member/manage_auth_tokens.html:31
msgid "Regenerate token"
msgstr "Regénérer le jeton"
#: templates/member/profile_detail.html:11
msgid "first name" msgid "first name"
msgstr "" msgstr ""
#: templates/member/profile_detail.html:14 #: templates/member/profile_detail.html:14
#, fuzzy
#| msgid "name"
msgid "username" msgid "username"
msgstr "nom" msgstr ""
#: templates/member/profile_detail.html:22 #: templates/member/profile_detail.html:17
#, fuzzy #, fuzzy
#| msgid "account balance" #| msgid "Change password"
msgid "balance" msgid "password"
msgstr "solde du compte" msgstr ""
#: templates/member/profile_detail.html:26 #: templates/member/profile_detail.html:20
msgid "Change password" msgid "Change password"
msgstr "Changer le mot de passe"
#: templates/member/profile_detail.html:38
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:54
msgid "View my memberships"
msgstr "Voir mes adhésions"
#: templates/member/profile_update.html:13
msgid "Save Changes"
msgstr "Sauvegarder les changements"
#: templates/member/signup.html:14
msgid "Sign Up"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:35 #: templates/note/transaction_form.html:35

BIN
media/pic/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,19 @@
[
{
"model": "sites.site",
"pk": 1,
"fields": {
"domain": "localhost",
"name": "La Note Kfet \ud83c\udf7b"
}
},
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 1,
"pattern": ".*",
"name": "REPLACEME"
}
}
]

38
note_kfet/middlewares.py Normal file
View File

@ -0,0 +1,38 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.http import HttpResponseRedirect
from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
class TurbolinksMiddleware(object):
"""
Send the `Turbolinks-Location` header in response to a visit that was redirected,
and Turbolinks will replace the browser's topmost history entry.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
is_turbolinks = request.META.get('HTTP_TURBOLINKS_REFERRER')
is_response_redirect = response.has_header('Location')
if is_turbolinks:
if is_response_redirect:
location = response['Location']
prev_location = request.session.pop('_turbolinks_redirect_to', None)
if prev_location is not None:
# relative subsequent redirect
if location.startswith('.'):
location = prev_location.split('?')[0] + location
request.session['_turbolinks_redirect_to'] = location
else:
if request.session.get('_turbolinks_redirect_to'):
location = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = location
return response

View File

@ -0,0 +1,46 @@
import os
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
directory.
"""
try:
with open('.env') as f:
content = f.read()
except IOError:
content = ''
for line in content.splitlines():
m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line)
if m1:
key, val = m1.group(1), m1.group(2)
m2 = re.match(r"\A'(.*)'\Z", val)
if m2:
val = m2.group(1)
m3 = re.match(r'\A"(.*)"\Z', val)
if m3:
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:
from .secrets import *
except ImportError:
pass
# env variables set at the of in /env/bin/activate
# don't forget to unset in deactivate !

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os
@ -8,8 +7,8 @@ import sys
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
PROJECT_DIR = os.path.dirname(os.path.realpath(__file__)) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
APPS_DIR = os.path.realpath(os.path.join(BASE_DIR, "apps")) APPS_DIR = os.path.realpath(os.path.join(BASE_DIR, "apps"))
sys.path.append(APPS_DIR) sys.path.append(APPS_DIR)
@ -51,13 +50,25 @@ INSTALLED_APPS = [
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# API
'rest_framework',
'rest_framework.authtoken',
# Autocomplete
'dal',
'dal_select2',
# CAS
'cas_server',
'cas',
# Note apps # Note apps
'activity', 'activity',
'member', 'member',
'note', 'note',
'permission' 'permission',
'api',
'logs',
] ]
LOGIN_REDIRECT_URL = '/note/transfer/'
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -70,6 +81,8 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware',
'cas.middleware.CASMiddleware',
] ]
ROOT_URLCONF = 'note_kfet.urls' ROOT_URLCONF = 'note_kfet.urls'
@ -86,6 +99,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request', 'django.template.context_processors.request',
# 'django.template.context_processors.media',
], ],
}, },
}, },
@ -93,16 +107,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'note_kfet.wsgi.application' WSGI_APPLICATION = 'note_kfet.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@ -121,13 +125,32 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Use our custom hasher in order to import NK15 passwords
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'member.hashers.CustomNK15Hasher',
]
# Django Guardian object permissions # Django Guardian object permissions
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # this is default 'django.contrib.auth.backends.ModelBackend', # this is default
'guardian.backends.ObjectPermissionBackend', 'guardian.backends.ObjectPermissionBackend',
'cas.backends.CASBackend',
) )
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'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type' GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
@ -138,6 +161,7 @@ GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_c
LANGUAGE_CODE = 'en' LANGUAGE_CODE = 'en'
LANGUAGES = [ LANGUAGES = [
('de', _('German')),
('en', _('English')), ('en', _('English')),
('fr', _('French')), ('fr', _('French')),
] ]
@ -152,6 +176,8 @@ USE_TZ = True
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
FIXTURE_DIRS = [os.path.join(BASE_DIR, "note_kfet/fixtures")]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/ # https://docs.djangoproject.com/en/2.2/howto/static-files/
@ -159,10 +185,10 @@ LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
# Don't put anything in this directory yourself; store your static files # Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS. # in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/" # Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.realpath(__file__) STATIC_ROOT = os.path.join(BASE_DIR,"static/")
STATICFILES_DIRS = [ # STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')] # os.path.join(BASE_DIR, 'static')]
STATICFILES_DIRS = []
CRISPY_TEMPLATE_PACK = 'bootstrap4' CRISPY_TEMPLATE_PACK = 'bootstrap4'
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html' DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html'
# URL prefix for static files. # URL prefix for static files.
@ -171,7 +197,15 @@ STATIC_URL = '/static/'
ALIAS_VALIDATOR_REGEX = r'' ALIAS_VALIDATOR_REGEX = r''
try: MEDIA_ROOT=os.path.join(BASE_DIR,"media")
from .settings_local import * MEDIA_URL='/media/'
except ImportError:
pass # Profile Picture Settings
PIC_WIDTH = 200
PIC_RATIO = 1
# CAS Settings
CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"

View File

@ -0,0 +1,54 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
########################
# Development Settings #
########################
# For local dev on your machine:
# - Enabled by default
# - use sqlite as a db engine , Debug is True.
# - standalone mail server
# - and more ...
# 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'),
}
}
# Break it, fix it!
DEBUG = True
# Mandatory !
ALLOWED_HOSTS = ['*']
# Emails
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_USE_SSL = False
# EMAIL_HOST = 'smtp.example.org'
# EMAIL_PORT = 25
# EMAIL_HOST_USER = 'change_me'
# EMAIL_HOST_PASSWORD = 'change_me'
SERVER_EMAIL = 'no-reply@example.org'
# Security settings
SECURE_CONTENT_TYPE_NOSNIFF = False
SECURE_BROWSER_XSS_FILTER = False
SESSION_COOKIE_SECURE = False
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 = "https://note.comby.xyz/cas/"

View File

@ -0,0 +1,52 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
########################
# Production Settings #
########################
# For local dev on your machine:
# - Enabled by setting env variable DJANGO_APP_STAGE = 'prod'
# - use Postgresql as db engine
# - Debug should be false.
# - should have a dedicated mail server
# - and more ...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'note_db',
'USER': 'note',
'PASSWORD': 'update_in_env_variable',
'HOST': '127.0.0.1',
'PORT': '',
}
}
# Break it, fix it!
DEBUG = True
# Mandatory !
ALLOWED_HOSTS = ['127.0.0.1','note.comby.xyz']
# Emails
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_USE_SSL = False
# EMAIL_HOST = 'smtp.example.org'
# EMAIL_PORT = 25
# EMAIL_HOST_USER = 'change_me'
# EMAIL_HOST_PASSWORD = 'change_me'
SERVER_EMAIL = 'no-reply@example.org'
# Security settings
SECURE_CONTENT_TYPE_NOSNIFF = False
SECURE_BROWSER_XSS_FILTER = False
SESSION_COOKIE_SECURE = False
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://note.crans.org/cas/"

View File

@ -1,22 +0,0 @@
# Obligatoire, liste des host autorisés
ALLOWED_HOSTS = ['127.0.0.1']
# Emails
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_USE_SSL = False
# EMAIL_HOST = 'smtp.example.org'
# EMAIL_PORT = 25
# EMAIL_HOST_USER = 'change_me'
# EMAIL_HOST_PASSWORD = 'change_me'
SERVER_EMAIL = 'no-reply@example.org'
# Security settings
SECURE_CONTENT_TYPE_NOSNIFF = False
SECURE_BROWSER_XSS_FILTER = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False
X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_AGE = 60 * 60 * 3

View File

@ -1,10 +1,13 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.conf.urls.static import static
from django.conf import settings
from cas import views as cas_views
urlpatterns = [ urlpatterns = [
# Dev so redirect to something random # Dev so redirect to something random
@ -13,10 +16,25 @@ urlpatterns = [
# Include project routers # Include project routers
path('note/', include('note.urls')), path('note/', include('note.urls')),
# Include CAS Client routers
path('accounts/login/', cas_views.login, name='login'),
path('accounts/logout/', cas_views.logout, name='logout'),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
path('accounts/', include('member.urls')), path('accounts/', include('member.urls')),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# Include CAS Server routers
path('cas/', include('cas_server.urls', namespace="cas_server")),
# Include Django REST API
path('api/', include('api.urls')),
path('logs/', include('logs.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)

View File

@ -1,4 +1,3 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2016-2019 by BDE # Copyright (C) 2016-2019 by BDE
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,15 +1,22 @@
certifi==2019.6.16 certifi==2019.6.16
chardet==3.0.4 chardet==3.0.4
defusedxml==0.6.0 defusedxml==0.6.0
Django==2.2.3 Django~=2.2
django-allauth==0.39.1 django-allauth==0.39.1
django-autocomplete-light==3.5.1
django-cas-client==1.5.3
django-cas-server==1.1.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-extensions==2.1.9 django-extensions==2.1.9
django-guardian==1.4.9 django-filter==2.2.0
django-guardian==2.1.0
django-polymorphic==2.0.3 django-polymorphic==2.0.3
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8
django-reversion==3.0.3 django-reversion==3.0.3
django-tables2==2.1.0 django-tables2==2.1.0
docutils==0.14 docutils==0.14
psycopg2==2.8.4
idna==2.8 idna==2.8
oauthlib==3.1.0 oauthlib==3.1.0
Pillow==6.1.0 Pillow==6.1.0

View File

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

987
static/admin/css/base.css Normal file
View File

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

View File

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

View File

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

View File

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

532
static/admin/css/forms.css Normal file
View File

@ -0,0 +1,532 @@
@import url('widgets.css');
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 10px;
font-size: 13px;
border-bottom: 1px solid #eee;
}
.form-row img, .form-row input {
vertical-align: middle;
}
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
}
form .form-row p {
padding-left: 0;
}
.hidden {
display: none;
}
/* FORM LABELS */
label {
font-weight: normal;
color: #666;
font-size: 13px;
}
.required label, label.required {
font-weight: bold;
color: #333;
}
/* RADIO BUTTONS */
form ul.radiolist li {
list-style-type: none;
}
form ul.radiolist label {
float: none;
display: inline;
}
form ul.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 4px 10px 0 0;
float: left;
width: 160px;
word-wrap: break-word;
line-height: 1;
}
.aligned label:not(.vCheckboxLabel):after {
content: '';
display: inline-block;
vertical-align: middle;
height: 26px;
}
.aligned label + p, .aligned label + div.help, .aligned label + div.readonly {
padding: 6px 0;
margin-top: 0;
margin-bottom: 0;
margin-left: 170px;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.aligned .form-row input {
margin-bottom: 0;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul.radiolist {
display: inline-block;
margin: 0;
padding: 0;
}
form .aligned p.help,
form .aligned div.help {
clear: left;
margin-top: 0;
margin-left: 160px;
padding-left: 10px;
}
form .aligned label + p.help,
form .aligned label + div.help {
margin-left: 0;
padding-left: 0;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned input + p.help,
form .aligned textarea + p.help,
form .aligned select + p.help,
form .aligned input + div.help,
form .aligned textarea + div.help,
form .aligned select + div.help {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul li {
list-style: none;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
.aligned .vCheckboxLabel {
float: none;
width: auto;
display: inline-block;
vertical-align: -3px;
padding: 0 0 5px 5px;
}
.aligned .vCheckboxLabel + p.help,
.aligned .vCheckboxLabel + div.help {
margin-top: -4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
.checkbox-row p.help,
.checkbox-row div.help {
margin-left: 0;
padding-left: 0;
}
fieldset .fieldBox {
float: left;
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 200px;
}
form .wide p,
form .wide input + p.help,
form .wide input + div.help {
margin-left: 200px;
}
form .wide p.help,
form .wide div.help {
padding-left: 38px;
}
form div.help ul {
padding-left: 0;
margin-left: 0;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSED FIELDSETS */
fieldset.collapsed * {
display: none;
}
fieldset.collapsed h2, fieldset.collapsed {
display: block;
}
fieldset.collapsed {
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
fieldset.collapsed h2 {
background: #f8f8f8;
color: #666;
}
fieldset .collapse-toggle {
color: #fff;
}
fieldset.collapsed .collapse-toggle {
background: transparent;
display: inline;
color: #447e9b;
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
}
/* SUBMIT ROW */
.submit-row {
padding: 12px 14px;
margin: 0 0 20px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 4px;
text-align: right;
overflow: hidden;
}
body.popup .submit-row {
overflow: auto;
}
.submit-row input {
height: 35px;
line-height: 15px;
margin: 0 0 0 5px;
}
.submit-row input.default {
margin: 0 0 0 8px;
text-transform: uppercase;
}
.submit-row p {
margin: 0.3em;
}
.submit-row p.deletelink-box {
float: left;
margin: 0;
}
.submit-row a.deletelink {
display: block;
background: #ba2121;
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
color: #fff;
}
.submit-row a.closelink {
display: inline-block;
background: #bbbbbb;
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
margin: 0 0 0 5px;
color: #fff;
}
.submit-row a.deletelink:focus,
.submit-row a.deletelink:hover,
.submit-row a.deletelink:active {
background: #a41515;
}
.submit-row a.closelink:focus,
.submit-row a.closelink:hover,
.submit-row a.closelink:active {
background: #aaaaaa;
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
margin-bottom: 4px;
}
.vDateField {
min-width: 6.85em;
}
.vTimeField {
min-width: 4.7em;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vTextField, .vUUIDField {
width: 20em;
}
.vIntegerField {
width: 5em;
}
.vBigIntegerField {
width: 10em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
/* INLINES */
.inline-group {
padding: 0;
margin: 0 0 30px;
}
.inline-group thead th {
padding: 8px 10px;
}
.inline-group .aligned label {
width: 160px;
}
.inline-related {
position: relative;
}
.inline-related h3 {
margin: 0;
color: #666;
padding: 5px;
font-size: 13px;
background: #f8f8f8;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 11px;
}
.inline-related fieldset {
margin: 0;
background: #fff;
border: none;
width: 100%;
}
.inline-related fieldset.module h3 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
text-align: left;
font-weight: bold;
background: #bcd;
color: #fff;
}
.inline-group .tabular fieldset.module {
border: none;
}
.inline-related.tabular fieldset.module table {
width: 100%;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 9px;
overflow: hidden;
font-size: 9px;
font-weight: bold;
color: #666;
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: #666;
background: #f8f8f8;
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.inline-group .tabular tr.add-row td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 12px;
}
.empty-form {
display: none;
}
/* RELATED FIELD ADD ONE / LOOKUP */
.add-another, .related-lookup {
margin-left: 5px;
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-size: 14px;
}
.add-another {
width: 16px;
height: 16px;
background-image: url(../img/icon-addlink.svg);
}
.related-lookup {
width: 16px;
height: 16px;
background-image: url(../img/search.svg);
}
form .related-widget-wrapper ul {
display: inline-block;
margin-left: 0;
padding-left: 0;
}
.clearable-file-input input {
margin-top: 0;
}

View File

@ -0,0 +1,79 @@
/* LOGIN FORM */
body.login {
background: #f8f8f8;
}
.login #header {
height: auto;
padding: 15px 16px;
justify-content: center;
}
.login #header h1 {
font-size: 18px;
}
.login #header h1 a {
color: #fff;
}
.login #content {
padding: 20px 20px 0;
}
.login #container {
background: #fff;
border: 1px solid #eaeaea;
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
}
.login #content-main {
width: 100%;
}
.login .form-row {
padding: 4px 0;
float: left;
width: 100%;
border-bottom: none;
}
.login .form-row label {
padding-right: 0.5em;
line-height: 2em;
font-size: 1em;
clear: both;
color: #333;
}
.login .form-row #id_username, .login .form-row #id_password {
clear: both;
padding: 8px;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.login span.help {
font-size: 10px;
display: block;
}
.login .submit-row {
clear: both;
padding: 1em 0 0 9.4em;
margin: 0;
border: none;
background: none;
text-align: left;
}
.login .password-reset-link {
text-align: center;
}

View File

@ -0,0 +1,992 @@
/* Tablets */
input[type="submit"], button {
-webkit-appearance: none;
appearance: none;
}
@media (max-width: 1024px) {
/* Basic */
html {
-webkit-text-size-adjust: 100%;
}
td, th {
padding: 10px;
font-size: 14px;
}
.small {
font-size: 12px;
}
/* Layout */
#container {
min-width: 0;
}
#content {
padding: 20px 30px 30px;
}
div.breadcrumbs {
padding: 10px 30px;
}
/* Header */
#header {
flex-direction: column;
padding: 15px 30px;
justify-content: flex-start;
}
#branding h1 {
margin: 0 0 8px;
font-size: 20px;
line-height: 1.2;
}
#user-tools {
margin: 0;
font-weight: 400;
line-height: 1.85;
text-align: left;
}
#user-tools a {
display: inline-block;
line-height: 1.4;
}
/* Dashboard */
.dashboard #content {
width: auto;
}
#content-related {
margin-right: -290px;
}
.colSM #content-related {
margin-left: -290px;
}
.colMS {
margin-right: 290px;
}
.colSM {
margin-left: 290px;
}
.dashboard .module table td a {
padding-right: 0;
}
td .changelink, td .addlink {
font-size: 13px;
}
/* Changelist */
#changelist #toolbar {
border: none;
padding: 15px;
}
#changelist-search > div {
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
max-width: 480px;
}
#changelist-search label {
line-height: 22px;
}
#changelist #toolbar form #searchbar {
-webkit-flex: 1 0 auto;
flex: 1 0 auto;
width: 0;
height: 22px;
margin: 0 10px 0 6px;
}
#changelist-search .quiet {
width: 100%;
margin: 5px 0 0 25px;
}
#changelist .actions {
display: flex;
flex-wrap: wrap;
padding: 15px 0;
}
#changelist .actions.selected {
border: none;
}
#changelist .actions label {
display: flex;
}
#changelist .actions select {
background: #fff;
}
#changelist .actions .button {
min-width: 48px;
margin: 0 10px;
}
#changelist .actions span.all,
#changelist .actions span.clear,
#changelist .actions span.question,
#changelist .actions span.action-counter {
font-size: 11px;
margin: 0 10px 0 0;
}
#changelist-filter {
width: 200px;
}
.change-list .filtered .results,
.change-list .filtered .paginator,
.filtered #toolbar,
.filtered .actions,
.filtered div.xfull {
margin-right: 230px;
}
#changelist .paginator {
border-top-color: #eee;
}
#changelist .results + .paginator {
border-top: none;
}
/* Forms */
label {
font-size: 14px;
}
.form-row input[type=text],
.form-row input[type=password],
.form-row input[type=email],
.form-row input[type=url],
.form-row input[type=tel],
.form-row input[type=number],
.form-row textarea,
.form-row select,
.form-row .vTextField {
box-sizing: border-box;
margin: 0;
padding: 6px 8px;
min-height: 36px;
font-size: 14px;
}
.form-row select {
height: 36px;
}
.form-row select[multiple] {
height: auto;
min-height: 0;
}
fieldset .fieldBox {
float: none;
margin: 0 -10px;
padding: 0 10px;
}
fieldset .fieldBox + .fieldBox {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
textarea {
max-width: 518px;
max-height: 120px;
}
.aligned label {
padding-top: 6px;
}
.aligned .add-another,
.aligned .related-lookup,
.aligned .datetimeshortcuts,
.aligned .related-lookup + strong {
align-self: center;
margin-left: 15px;
}
form .aligned ul.radiolist {
margin-left: 2px;
}
/* Related widget */
.related-widget-wrapper {
float: none;
}
.related-widget-wrapper-link + .selector {
max-width: calc(100% - 30px);
margin-right: 15px;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 10px;
}
/* Selector */
.selector {
display: flex;
width: 100%;
}
.selector .selector-filter {
display: flex;
align-items: center;
}
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input {
width: auto;
min-height: 0;
flex: 1 1;
}
.selector-available, .selector-chosen {
width: auto;
flex: 1 1;
display: flex;
flex-direction: column;
}
.selector select {
width: 100%;
flex: 1 0 auto;
margin-bottom: 5px;
}
.selector ul.selector-chooser {
width: 26px;
height: 52px;
padding: 2px 0;
margin: auto 15px;
border-radius: 20px;
transform: translateY(-10px);
}
.selector-add, .selector-remove {
width: 20px;
height: 20px;
background-size: 20px auto;
}
.selector-add {
background-position: 0 -120px;
}
.selector-remove {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
align-self: center;
}
.stacked {
flex-direction: column;
max-width: 480px;
}
.stacked > * {
flex: 0 1 auto;
}
.stacked select {
margin-bottom: 0;
}
.stacked .selector-available, .stacked .selector-chosen {
width: auto;
}
.stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto;
transform: none;
}
.stacked .selector-chooser li {
padding: 3px;
}
.stacked .selector-add, .stacked .selector-remove {
background-size: 20px auto;
}
.stacked .selector-add {
background-position: 0 -40px;
}
.stacked .active.selector-add {
background-position: 0 -60px;
}
.stacked .selector-remove {
background-position: 0 0;
}
.stacked .active.selector-remove {
background-position: 0 -20px;
}
.help-tooltip, .selector .help-icon {
display: none;
}
form .form-row p.datetime {
width: 100%;
}
.datetime input {
width: 50%;
max-width: 120px;
}
.datetime span {
font-size: 13px;
}
.datetime .timezonewarning {
display: block;
font-size: 11px;
color: #999;
}
.datetimeshortcuts {
color: #ccc;
}
.inline-group {
overflow: auto;
}
/* Messages */
ul.messagelist li {
padding-left: 55px;
background-position: 30px 12px;
}
ul.messagelist li.error {
background-position: 30px 12px;
}
ul.messagelist li.warning {
background-position: 30px 14px;
}
/* Login */
.login #header {
padding: 15px 20px;
}
.login #branding h1 {
margin: 0;
}
/* GIS */
div.olMap {
max-width: calc(100vw - 30px);
max-height: 300px;
}
.olMap + .clear_features {
display: block;
margin-top: 10px;
}
/* Docs */
.module table.xfull {
width: 100%;
}
pre.literal-block {
overflow: auto;
}
}
/* Mobile */
@media (max-width: 767px) {
/* Layout */
#header, #content, #footer {
padding: 15px;
}
#footer:empty {
padding: 0;
}
div.breadcrumbs {
padding: 10px 15px;
}
/* Dashboard */
.colMS, .colSM {
margin: 0;
}
#content-related, .colSM #content-related {
width: 100%;
margin: 0;
}
#content-related .module {
margin-bottom: 0;
}
#content-related .module h2 {
padding: 10px 15px;
font-size: 16px;
}
/* Changelist */
#changelist {
display: flex;
flex-direction: column;
}
#changelist #toolbar {
order: 1;
padding: 10px;
}
#changelist .xfull {
order: 2;
}
#changelist-form {
order: 3;
}
#changelist-filter {
order: 4;
}
#changelist .actions label {
flex: 1 1;
}
#changelist .actions select {
flex: 1 0;
width: 100%;
}
#changelist .actions span {
flex: 1 0 100%;
}
.change-list .filtered .results, .change-list .filtered .paginator,
.filtered #toolbar, .filtered .actions, .filtered div.xfull {
margin-right: 0;
}
#changelist-filter {
position: static;
width: auto;
margin-top: 30px;
}
.object-tools {
float: none;
margin: 0 0 15px;
padding: 0;
overflow: hidden;
}
.object-tools li {
height: auto;
margin-left: 0;
}
.object-tools li + li {
margin-left: 15px;
}
/* Forms */
.form-row {
padding: 15px 0;
}
.aligned .form-row,
.aligned .form-row > div {
display: flex;
flex-wrap: wrap;
max-width: 100vw;
}
.aligned .form-row > div {
width: calc(100vw - 30px);
}
textarea {
max-width: none;
}
.vURLField {
width: auto;
}
fieldset .fieldBox + .fieldBox {
margin-top: 15px;
padding-top: 15px;
}
fieldset.collapsed .form-row {
display: none;
}
.aligned label {
width: 100%;
padding: 0 0 10px;
}
.aligned label:after {
max-height: 0;
}
.aligned .form-row input,
.aligned .form-row select,
.aligned .form-row textarea {
flex: 1 1 auto;
max-width: 100%;
}
.aligned .checkbox-row {
align-items: center;
}
.aligned .checkbox-row input {
flex: 0 1 auto;
margin: 0;
}
.aligned .vCheckboxLabel {
flex: 1 0;
padding: 1px 0 0 5px;
}
.aligned label + p,
.aligned label + div.help,
.aligned label + div.readonly {
padding: 0;
margin-left: 0;
}
.aligned p.file-upload {
margin-left: 0;
font-size: 13px;
}
span.clearable-file-input {
margin-left: 15px;
}
span.clearable-file-input label {
font-size: 13px;
padding-bottom: 0;
}
.aligned .timezonewarning {
flex: 1 0 100%;
margin-top: 5px;
}
form .aligned .form-row div.help {
width: 100%;
margin: 5px 0 0;
padding: 0;
}
form .aligned ul {
margin-left: 0;
padding-left: 0;
}
form .aligned ul.radiolist {
margin-right: 15px;
margin-bottom: -3px;
}
form .aligned ul.radiolist li + li {
margin-top: 5px;
}
/* Related widget */
.related-widget-wrapper {
width: 100%;
display: flex;
align-items: flex-start;
}
.related-widget-wrapper .selector {
order: 1;
}
.related-widget-wrapper > a {
order: 2;
}
.related-widget-wrapper .radiolist ~ a {
align-self: flex-end;
}
.related-widget-wrapper > select ~ a {
align-self: center;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 15px;
}
/* Selector */
.selector {
flex-direction: column;
}
.selector > * {
float: none;
}
.selector-available, .selector-chosen {
margin-bottom: 0;
flex: 1 1 auto;
}
.selector select {
max-height: 96px;
}
.selector ul.selector-chooser {
display: block;
float: none;
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto 20px;
transform: none;
}
.selector ul.selector-chooser li {
float: left;
}
.selector-remove {
background-position: 0 0;
}
.selector-add {
background-position: 0 -40px;
}
/* Inlines */
.inline-group[data-inline-type="stacked"] .inline-related {
border: 2px solid #eee;
border-radius: 4px;
margin-top: 15px;
overflow: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related > * {
box-sizing: border-box;
}
.inline-group[data-inline-type="stacked"] .inline-related + .inline-related {
margin-top: 30px;
}
.inline-group[data-inline-type="stacked"] .inline-related .module {
padding: 0 10px;
}
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:last-child {
border-bottom: none;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 {
padding: 10px;
border-top-width: 0;
border-bottom-width: 2px;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
margin-right: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
float: none;
flex: 1 1 100%;
margin-top: 5px;
}
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
width: 100%;
}
.inline-group[data-inline-type="stacked"] .aligned label {
width: 100%;
}
.inline-group[data-inline-type="stacked"] div.add-row {
margin-top: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
padding: 0;
}
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
display: block;
padding: 8px 10px 8px 26px;
background-position: 8px 9px;
}
/* Submit row */
.submit-row {
padding: 10px 10px 0;
margin: 0 0 15px;
display: flex;
flex-direction: column;
}
.submit-row > * {
width: 100%;
}
.submit-row input, .submit-row input.default, .submit-row a, .submit-row a.closelink {
float: none;
margin: 0 0 10px;
text-align: center;
}
.submit-row a.closelink {
padding: 10px 0;
}
.submit-row p.deletelink-box {
order: 4;
}
/* Messages */
ul.messagelist li {
padding-left: 40px;
background-position: 15px 12px;
}
ul.messagelist li.error {
background-position: 15px 12px;
}
ul.messagelist li.warning {
background-position: 15px 14px;
}
/* Paginator */
.paginator .this-page, .paginator a:link, .paginator a:visited {
padding: 4px 10px;
}
/* Login */
body.login {
padding: 0 15px;
}
.login #container {
width: auto;
max-width: 480px;
margin: 50px auto;
}
.login #header,
.login #content {
padding: 15px;
}
.login #content-main {
float: none;
}
.login .form-row {
padding: 0;
}
.login .form-row + .form-row {
margin-top: 15px;
}
.login .form-row label {
display: block;
margin: 0 0 5px;
padding: 0;
line-height: 1.2;
}
.login .submit-row {
padding: 15px 0 0;
}
.login br, .login .submit-row label {
display: none;
}
.login .submit-row input {
margin: 0;
text-transform: uppercase;
}
.errornote {
margin: 0 0 20px;
padding: 8px 12px;
font-size: 13px;
}
/* Calendar and clock */
.calendarbox, .clockbox {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
margin: 0;
border: none;
overflow: visible;
}
.calendarbox:before, .clockbox:before {
content: '';
position: fixed;
top: 50%;
left: 50%;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
transform: translate(-50%, -50%);
}
.calendarbox > *, .clockbox > * {
position: relative;
z-index: 1;
}
.calendarbox > div:first-child {
z-index: 2;
}
.calendarbox .calendar, .clockbox h2 {
border-radius: 4px 4px 0 0;
overflow: hidden;
}
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
border-radius: 0 0 4px 4px;
overflow: hidden;
}
.calendar-shortcuts {
padding: 10px 0;
font-size: 12px;
line-height: 12px;
}
.calendar-shortcuts a {
margin: 0 4px;
}
.timelist a {
background: #fff;
padding: 4px;
}
.calendar-cancel {
padding: 8px 10px;
}
.clockbox h2 {
padding: 8px 15px;
}
.calendar caption {
padding: 10px;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
z-index: 1;
top: 10px;
}
/* History */
table#change-history tbody th, table#change-history tbody td {
font-size: 13px;
word-break: break-word;
}
table#change-history tbody th {
width: auto;
}
/* Docs */
table.model tbody th, table.model tbody td {
font-size: 13px;
word-break: break-word;
}
}

View File

@ -0,0 +1,84 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions {
margin-right: 0;
margin-left: 230px;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .related-widget-wrapper-link + .selector {
margin-right: 0;
margin-left: 15px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions {
margin-left: 0;
}
[dir="rtl"] .aligned .add-another,
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .aligned ul {
margin-right: 0;
}
}

269
static/admin/css/rtl.css Normal file
View File

@ -0,0 +1,269 @@
body {
direction: rtl;
}
/* LOGIN */
.login .form-row {
float: right;
}
.login .form-row label {
float: right;
padding-left: 0.5em;
padding-right: 0;
text-align: left;
}
.login .submit-row {
clear: both;
padding: 1em 9.4em 0 0;
}
/* GLOBAL */
th {
text-align: right;
}
.module h2, .module caption {
text-align: right;
}
.module ul, .module ol {
margin-left: 0;
margin-right: 1.5em;
}
.viewlink, .addlink, .changelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.deletelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.object-tools {
float: left;
}
thead th:first-child,
tfoot td:first-child {
border-left: none;
}
/* LAYOUT */
#user-tools {
right: auto;
left: 0;
text-align: left;
}
div.breadcrumbs {
text-align: right;
}
#content-main {
float: right;
}
#content-related {
float: left;
margin-left: -300px;
margin-right: auto;
}
.colMS {
margin-left: 300px;
margin-right: 0;
}
/* SORTABLE TABLES */
table thead th.sorted .sortoptions {
float: left;
}
thead th.sorted .text {
padding-right: 0;
padding-left: 42px;
}
/* dashboard styles */
.dashboard .module table td a {
padding-left: .6em;
padding-right: 16px;
}
/* changelists styles */
.change-list .filtered table {
border-left: none;
border-right: 0px none;
}
#changelist-filter {
right: auto;
left: 0;
border-left: none;
border-right: none;
}
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
margin-right: 0;
margin-left: 280px;
}
#changelist-filter li.selected {
border-left: none;
padding-left: 10px;
margin-left: 0;
border-right: 5px solid #eaeaea;
padding-right: 10px;
margin-right: -15px;
}
.filtered .actions {
margin-left: 280px;
margin-right: 0;
}
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
border-right: none;
border-left: none;
}
/* FORMS */
.aligned label {
padding: 0 0 3px 1em;
float: right;
}
.submit-row {
text-align: left
}
.submit-row p.deletelink-box {
float: right;
}
.submit-row input.default {
margin-left: 0;
}
.vDateField, .vTimeField {
margin-left: 2px;
}
.aligned .form-row input {
margin-left: 5px;
}
form .aligned p.help, form .aligned div.help {
clear: right;
}
form .aligned ul {
margin-right: 163px;
margin-left: 0;
}
form ul.inline li {
float: right;
padding-right: 0;
padding-left: 7px;
}
input[type=submit].default, .submit-row input.default {
float: left;
}
fieldset .fieldBox {
float: right;
margin-left: 20px;
margin-right: 0;
}
.errorlist li {
background-position: 100% 12px;
padding: 0;
}
.errornote {
background-position: 100% 12px;
padding: 10px 12px;
}
/* WIDGETS */
.calendarnav-previous {
top: 0;
left: auto;
right: 10px;
}
.calendarnav-next {
top: 0;
right: auto;
left: 10px;
}
.calendar caption, .calendarbox h2 {
text-align: center;
}
.selector {
float: right;
}
.selector .selector-filter {
text-align: right;
}
.inline-deletelink {
float: left;
}
form .form-row p.datetime {
overflow: hidden;
}
.related-widget-wrapper {
float: right;
}
/* MISC */
.inline-related h2, .inline-group h2 {
text-align: right
}
.inline-related h3 span.delete {
padding-right: 20px;
padding-left: inherit;
left: 10px;
right: inherit;
float:left;
}
.inline-related h3 span.delete label {
margin-left: inherit;
margin-right: 2px;
}
/* IE7 specific bug fixes */
div.colM {
position: relative;
}
.submit-row input {
float: left;
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2015 Kevin Brown, Igor Vaynberg, and Select2 contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,484 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle; }
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-selection--single .select2-selection__clear {
position: relative; }
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px; }
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-search--inline {
float: left; }
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0; }
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051; }
.select2-results {
display: block; }
.select2-results__options {
list-style: none;
margin: 0;
padding: 0; }
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none; }
.select2-results__option[aria-selected] {
cursor: pointer; }
.select2-container--open .select2-dropdown {
left: 0; }
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-search--dropdown {
display: block;
padding: 4px; }
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box; }
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-search--dropdown.select2-search--hide {
display: none; }
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0); }
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important; }
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px; }
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold; }
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px; }
.select2-container--default .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--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto; }
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none; }
.select2-container--default .select2-selection--multiple .select2-selection__placeholder {
color: #999;
margin-top: 5px;
float: left; }
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0; }
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none; }
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa; }
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield; }
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--default .select2-results__option[role=group] {
padding: 0; }
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999; }
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd; }
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em; }
.select2-container--default .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--default .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--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white; }
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px; }
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
.select2-container--classic .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--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto; }
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0; }
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0; }
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none; }
.select2-container--classic .select2-dropdown {
background-color: white;
border: 1px solid transparent; }
.select2-container--classic .select2-dropdown--above {
border-bottom: none; }
.select2-container--classic .select2-dropdown--below {
border-top: none; }
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--classic .select2-results__option[role=group] {
padding: 0; }
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey; }
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: white; }
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb; }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,565 @@
/* SELECTOR (FILTER INTERFACE) */
.selector {
width: 800px;
float: left;
}
.selector select {
width: 380px;
height: 17.2em;
}
.selector-available, .selector-chosen {
float: left;
width: 380px;
text-align: center;
margin-bottom: 5px;
}
.selector-chosen select {
border-top: none;
}
.selector-available h2, .selector-chosen h2 {
border: 1px solid #ccc;
border-radius: 4px 4px 0 0;
}
.selector-chosen h2 {
background: #79aec8;
color: #fff;
}
.selector .selector-available h2 {
background: #f8f8f8;
color: #666;
}
.selector .selector-filter {
background: white;
border: 1px solid #ccc;
border-width: 0 1px;
padding: 8px;
color: #999;
font-size: 10px;
margin: 0;
text-align: left;
}
.selector .selector-filter label,
.inline-group .aligned .selector .selector-filter label {
float: left;
margin: 7px 0 0;
width: 18px;
height: 18px;
padding: 0;
overflow: hidden;
line-height: 1;
}
.selector .selector-available input {
width: 320px;
margin-left: 8px;
}
.selector ul.selector-chooser {
float: left;
width: 22px;
background-color: #eee;
border-radius: 10px;
margin: 10em 5px 0 5px;
padding: 0;
}
.selector-chooser li {
margin: 0;
padding: 3px;
list-style-type: none;
}
.selector select {
padding: 0 10px;
margin: 0 0 10px;
border-radius: 0 0 4px 4px;
}
.selector-add, .selector-remove {
width: 16px;
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.3;
}
.active.selector-add, .active.selector-remove {
opacity: 1;
}
.active.selector-add:hover, .active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
margin: 1px auto 3px;
overflow: hidden;
font-weight: bold;
line-height: 16px;
color: #666;
text-decoration: none;
opacity: 0.3;
}
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
color: #447e9b;
}
a.active.selector-chooseall, a.active.selector-clearall {
opacity: 1;
}
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
cursor: pointer;
}
a.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -176px;
}
a.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -144px;
}
/* STACKED SELECTORS */
.stacked {
float: left;
width: 490px;
}
.stacked select {
width: 480px;
height: 10.1em;
}
.stacked .selector-available, .stacked .selector-chosen {
width: 480px;
}
.stacked .selector-available {
margin-bottom: 0;
}
.stacked .selector-available input {
width: 422px;
}
.stacked ul.selector-chooser {
height: 22px;
width: 50px;
margin: 0 0 10px 40%;
background-color: #eee;
border-radius: 10px;
}
.stacked .selector-chooser li {
float: left;
padding: 3px 3px 3px 5px;
}
.stacked .selector-chooseall, .stacked .selector-clearall {
display: none;
}
.stacked .selector-add {
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
cursor: default;
}
.stacked .active.selector-add {
background-position: 0 -48px;
cursor: pointer;
}
.stacked .selector-remove {
background: url(../img/selector-icons.svg) 0 0 no-repeat;
cursor: default;
}
.stacked .active.selector-remove {
background-position: 0 -16px;
cursor: pointer;
}
.selector .help-icon {
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
display: inline-block;
vertical-align: middle;
margin: -2px 0 0 2px;
width: 13px;
height: 13px;
}
.selector .selector-chosen .help-icon {
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
}
.selector .search-label-icon {
background: url(../img/search.svg) 0 0 no-repeat;
display: inline-block;
height: 18px;
width: 18px;
}
/* DATE AND TIME */
p.datetime {
line-height: 20px;
margin: 0;
padding: 0;
color: #666;
font-weight: bold;
}
.datetime span {
white-space: nowrap;
font-weight: normal;
font-size: 11px;
color: #ccc;
}
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
min-width: 0;
margin-left: 5px;
margin-bottom: 4px;
}
table p.datetime {
font-size: 11px;
margin-left: 0;
padding-left: 0;
}
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
position: relative;
display: inline-block;
vertical-align: middle;
height: 16px;
width: 16px;
overflow: hidden;
}
.datetimeshortcuts .clock-icon {
background: url(../img/icon-clock.svg) 0 0 no-repeat;
}
.datetimeshortcuts a:focus .clock-icon,
.datetimeshortcuts a:hover .clock-icon {
background-position: 0 -16px;
}
.datetimeshortcuts .date-icon {
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
top: -1px;
}
.datetimeshortcuts a:focus .date-icon,
.datetimeshortcuts a:hover .date-icon {
background-position: 0 -16px;
}
.timezonewarning {
font-size: 11px;
color: #999;
}
/* URL */
p.url {
line-height: 20px;
margin: 0;
padding: 0;
color: #666;
font-size: 11px;
font-weight: bold;
}
.url a {
font-weight: normal;
}
/* FILE UPLOADS */
p.file-upload {
line-height: 20px;
margin: 0;
padding: 0;
color: #666;
font-size: 11px;
font-weight: bold;
}
.aligned p.file-upload {
margin-left: 170px;
}
.file-upload a {
font-weight: normal;
}
.file-upload .deletelink {
margin-left: 5px;
}
span.clearable-file-input label {
color: #333;
font-size: 11px;
display: inline;
float: none;
}
/* CALENDARS & CLOCKS */
.calendarbox, .clockbox {
margin: 5px auto;
font-size: 12px;
width: 19em;
text-align: center;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
.clockbox {
width: auto;
}
.calendar {
margin: 0;
padding: 0;
}
.calendar table {
margin: 0;
padding: 0;
border-collapse: collapse;
background: white;
width: 100%;
}
.calendar caption, .calendarbox h2 {
margin: 0;
text-align: center;
border-top: none;
background: #f5dd5d;
font-weight: 700;
font-size: 12px;
color: #333;
}
.calendar th {
padding: 8px 5px;
background: #f8f8f8;
border-bottom: 1px solid #ddd;
font-weight: 400;
font-size: 12px;
text-align: center;
color: #666;
}
.calendar td {
font-weight: 400;
font-size: 12px;
text-align: center;
padding: 0;
border-top: 1px solid #eee;
border-bottom: none;
}
.calendar td.selected a {
background: #79aec8;
color: #fff;
}
.calendar td.nonday {
background: #f8f8f8;
}
.calendar td.today a {
font-weight: 700;
}
.calendar td a, .timelist a {
display: block;
font-weight: 400;
padding: 6px;
text-decoration: none;
color: #444;
}
.calendar td a:focus, .timelist a:focus,
.calendar td a:hover, .timelist a:hover {
background: #79aec8;
color: white;
}
.calendar td a:active, .timelist a:active {
background: #417690;
color: white;
}
.calendarnav {
font-size: 10px;
text-align: center;
color: #ccc;
margin: 0;
padding: 1px 3px;
}
.calendarnav a:link, #calendarnav a:visited,
#calendarnav a:focus, #calendarnav a:hover {
color: #999;
}
.calendar-shortcuts {
background: white;
font-size: 11px;
line-height: 11px;
border-top: 1px solid #eee;
padding: 8px 0;
color: #ccc;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
display: block;
position: absolute;
top: 8px;
width: 15px;
height: 15px;
text-indent: -9999px;
padding: 0;
}
.calendarnav-previous {
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarbox .calendarnav-previous:focus,
.calendarbox .calendarnav-previous:hover {
background-position: 0 -15px;
}
.calendarnav-next {
right: 10px;
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
}
.calendarbox .calendarnav-next:focus,
.calendarbox .calendarnav-next:hover {
background-position: 0 -45px;
}
.calendar-cancel {
margin: 0;
padding: 4px 0;
font-size: 12px;
background: #eee;
border-top: 1px solid #ddd;
color: #333;
}
.calendar-cancel:focus, .calendar-cancel:hover {
background: #ddd;
}
.calendar-cancel a {
color: black;
display: block;
}
ul.timelist, .timelist li {
list-style-type: none;
margin: 0;
padding: 0;
}
.timelist a {
padding: 2px;
}
/* EDIT INLINE */
.inline-deletelink {
float: right;
text-indent: -9999px;
background: url(../img/inline-delete.svg) 0 0 no-repeat;
width: 16px;
height: 16px;
border: 0px none;
}
.inline-deletelink:focus, .inline-deletelink:hover {
cursor: pointer;
}
/* RELATED WIDGET WRAPPER */
.related-widget-wrapper {
float: left; /* display properly in form rows with multiple fields */
overflow: hidden; /* clear floated contents */
}
.related-widget-wrapper-link {
opacity: 0.3;
}
.related-widget-wrapper-link:link {
opacity: .8;
}
.related-widget-wrapper-link:link:focus,
.related-widget-wrapper-link:link:hover {
opacity: 1;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 7px;
}

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,3 @@
Roboto webfont source: https://www.google.com/fonts/specimen/Roboto
WOFF files extracted using https://github.com/majodev/google-webfonts-helper
Weights used in this project: Light (300), Regular (400), Bold (700)

Binary file not shown.

Binary file not shown.

Binary file not shown.

20
static/admin/img/LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Code Charm Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,7 @@
All icons are taken from Font Awesome (http://fontawesome.io/) project.
The Font Awesome font is licensed under the SIL OFL 1.1:
- https://scripts.sil.org/OFL
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
in current folder).

View File

@ -0,0 +1,14 @@
<svg width="15" height="60" viewBox="0 0 1792 7168" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="previous">
<path d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="next">
<path d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#previous" x="0" y="0" fill="#333333" />
<use xlink:href="#previous" x="0" y="1792" fill="#000000" />
<use xlink:href="#next" x="0" y="3584" fill="#333333" />
<use xlink:href="#next" x="0" y="5376" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#70bf2b" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View File

@ -0,0 +1,9 @@
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="icon">
<path d="M192 1664h288v-288h-288v288zm352 0h320v-288h-320v288zm-352-352h288v-320h-288v320zm352 0h320v-320h-320v320zm-352-384h288v-288h-288v288zm736 736h320v-288h-320v288zm-384-736h320v-288h-320v288zm768 736h288v-288h-288v288zm-384-352h320v-320h-320v320zm-352-864v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm736 864h288v-320h-288v320zm-384-384h320v-288h-320v288zm384 0h288v-288h-288v288zm32-480v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm384-64v1280q0 52-38 90t-90 38h-1408q-52 0-90-38t-38-90v-1280q0-52 38-90t90-38h128v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h384v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h128q52 0 90 38t38 90z"/>
</g>
</defs>
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#efb80b" d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -0,0 +1,9 @@
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="icon">
<path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
</svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#dd4646" d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

Some files were not shown because too many files have changed in this diff Show More