From c3d740ade00f54a299231456ee07269b98b12f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 1 Apr 2017 21:45:05 +0100 Subject: [PATCH 01/75] Handle incomplete values from the LDAP Sometime `uid` is not set in the objects fetched from the LDAP. This case has to be handled. Also, the `.uid` and `.cn` attributes of these objects in the python abstractions have a `.value` method which we should use. --- gestioncof/autocomplete.py | 32 ++++++++++++++++++++------------ kfet/autocomplete.py | 32 ++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 1eae6920..98363377 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -14,6 +14,10 @@ from gestioncof.decorators import buro_required class Clipper(object): def __init__(self, clipper, fullname): + if fullname is None: + fullname = "" + assert isinstance(clipper, str) + assert isinstance(fullname, str) self.clipper = clipper self.fullname = fullname @@ -60,18 +64,22 @@ def autocomplete(request): ['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit}) for bit in bits] )) - with Connection(settings.LDAP_SERVER_URL) as conn: - conn.search( - 'dc=spi,dc=ens,dc=fr', ldap_query, - attributes=['uid', 'cn'] - ) - queries['clippers'] = conn.entries - # Clearing redundancies - queries['clippers'] = [ - Clipper(clipper.uid, clipper.cn) - for clipper in queries['clippers'] - if str(clipper.uid) not in usernames - ] + if ldap_query != "(&)": + # If none of the bits were legal, we do not perform the query + entries = None + with Connection(settings.LDAP_SERVER_URL) as conn: + conn.search( + 'dc=spi,dc=ens,dc=fr', ldap_query, + attributes=['uid', 'cn'] + ) + entries = conn.entries + # Clearing redundancies + queries['clippers'] = [ + Clipper(entry.uid.value, entry.cn.value) + for entry in entries + if entry.uid.value is not None + and entry.uid.value not in usernames + ] # Resulting data data.update(queries) diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 64fa52cf..acc6ebd8 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -14,6 +14,10 @@ from kfet.models import Account class Clipper(object): def __init__(self, clipper, fullname): + if fullname is None: + fullname = "" + assert isinstance(clipper, str) + assert isinstance(fullname, str) self.clipper = clipper self.fullname = fullname @@ -79,18 +83,22 @@ def account_create(request): ['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(bit=word) for word in search_words] )) - with Connection(settings.LDAP_SERVER_URL) as conn: - conn.search( - 'dc=spi,dc=ens,dc=fr', ldap_query, - attributes=['uid', 'cn'] - ) - queries['clippers'] = conn.entries - # Clearing redundancies - queries['clippers'] = [ - Clipper(clipper.uid, clipper.cn) - for clipper in queries['clippers'] - if str(clipper.uid) not in usernames - ] + if ldap_query != "(&)": + # If none of the bits were legal, we do not perform the query + entries = None + with Connection(settings.LDAP_SERVER_URL) as conn: + conn.search( + 'dc=spi,dc=ens,dc=fr', ldap_query, + attributes=['uid', 'cn'] + ) + entries = conn.entries + # Clearing redundancies + queries['clippers'] = [ + Clipper(entry.uid.value, entry.cn.value) + for entry in entries + if entry.uid.value is not None + and entry.uid.value not in usernames + ] # Resulting data data.update(queries) From f6d43dffa1e0d45d978dfef9a412cc48933504d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 1 Apr 2017 22:07:32 +0100 Subject: [PATCH 02/75] exclude empty strings from ldap results The uid attribute in a LDAP's entry cannot be an empty string. We need to get an actual identifier. --- gestioncof/autocomplete.py | 2 +- kfet/autocomplete.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 98363377..ccf8804e 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -77,7 +77,7 @@ def autocomplete(request): queries['clippers'] = [ Clipper(entry.uid.value, entry.cn.value) for entry in entries - if entry.uid.value is not None + if entry.uid.value and entry.uid.value not in usernames ] diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index acc6ebd8..c97779c1 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -96,7 +96,7 @@ def account_create(request): queries['clippers'] = [ Clipper(entry.uid.value, entry.cn.value) for entry in entries - if entry.uid.value is not None + if entry.uid.value and entry.uid.value not in usernames ] From e13d68a127eb9c88710a9d195eb815f41390f477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 8 Aug 2017 00:03:08 +0100 Subject: [PATCH 03/75] Ignore PyCharm's files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index f12190af..9e139df9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ venv/ /src media/ *.log + +# PyCharm +.idea +.cache From 88597e62f116fcfa9aa10b53e1de78f0c3bdea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 8 Aug 2017 00:06:03 +0100 Subject: [PATCH 04/75] More verbose secret error reporting --- cof/settings/common.py | 50 ++++++++++++++++++++++------------ cof/settings/secret_example.py | 12 ++++++-- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index ffcb8ee5..73959c71 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -8,26 +8,42 @@ the local development server should be here. import os -# Database credentials try: - from .secret import DBNAME, DBUSER, DBPASSWD + from . import secret except ImportError: - # On the local development VM, theses credentials are in the environment - DBNAME = os.environ["DBNAME"] - DBUSER = os.environ["DBUSER"] - DBPASSWD = os.environ["DBPASSWD"] -except KeyError: - raise RuntimeError("Secrets missing") - - -# Other secrets -try: - from .secret import ( - SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS, - REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT, KFETOPEN_TOKEN, + raise ImportError( + "The secret.py file is missing.\n" + "For a development environment, simply copy secret_example.py" ) -except ImportError: - raise RuntimeError("Secrets missing") + + +def import_secret(name): + """ + Shorthand for importing a value from the secret module and raising an + informative exception if a secret is missing. + """ + try: + return getattr(secret, name) + except AttributeError: + raise RuntimeError("Secret missing: {}".format(name)) + + +SECRET_KEY = import_secret("SECRET_KEY") +ADMINS = import_secret("ADMINS") + +DBNAME = import_secret("DBNAME") +DBUSER = import_secret("DBUSER") +DBPASSWD = import_secret("DBPASSWD") + +REDIS_PASSWD = import_secret("REDIS_PASSWD") +REDIS_DB = import_secret("REDIS_DB") +REDIS_HOST = import_secret("REDIS_HOST") +REDIS_PORT = import_secret("REDIS_PORT") + +RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") +RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") + +KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") BASE_DIR = os.path.dirname( diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index a1d35b68..0dbc91dc 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,10 +1,16 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' -RECAPTCHA_PUBLIC_KEY = "DUMMY" -RECAPTCHA_PRIVATE_KEY = "DUMMY" +ADMINS = None + +DBUSER = "cof_gestion" +DBNAME = "cof_gestion" +DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" + REDIS_PASSWD = "dummy" REDIS_PORT = 6379 REDIS_DB = 0 REDIS_HOST = "127.0.0.1" -ADMINS = None + +RECAPTCHA_PUBLIC_KEY = "DUMMY" +RECAPTCHA_PRIVATE_KEY = "DUMMY" KFETOPEN_TOKEN = "plop" From 784513b3ccee0e2dfd5a2be891e2f17db14facaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 8 Aug 2017 00:12:09 +0100 Subject: [PATCH 05/75] Use utf8 encoding for the mysql database --- provisioning/bootstrap.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index e35af45a..9d210fe8 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -22,6 +22,7 @@ apt-get install -y mysql-server mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'" mysql -uroot -p$DBPASSWD -e "GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'" +mysql -uroot -p$DBPASSWD -e "ALTER DATABASE databasename CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # Configuration de redis REDIS_PASSWD="dummy" @@ -43,11 +44,6 @@ cat >> ~ubuntu/.bashrc < Date: Tue, 8 Aug 2017 00:19:47 +0100 Subject: [PATCH 06/75] Use the right python interpreter in the cron tasks --- provisioning/cron.dev | 4 ++-- provisioning/cron.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/provisioning/cron.dev b/provisioning/cron.dev index 0dab871f..896b5125 100644 --- a/provisioning/cron.dev +++ b/provisioning/cron.dev @@ -6,5 +6,5 @@ DBUSER="cof_gestion" DBNAME="cof_gestion" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" -19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 -*/5 * * * * python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1 +19 */12 * * * date >> /vagrant/rappels.log ; /ubuntu/home/venv/bin/python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 +*/5 * * * * /ubuntu/home/venv/bin/python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1 diff --git a/provisioning/cron.md b/provisioning/cron.md index 840a8716..7aff775b 100644 --- a/provisioning/cron.md +++ b/provisioning/cron.md @@ -9,9 +9,9 @@ envoie les mails de rappels des spectacles à venir (sauf s'ils ont déjà été envoyés). - Un fois toutes les 12 heures me semble bien. -- Penser à utiliser le bon executable python (virtualenvs) et les bonnes - variables d'environnement si besoin. -- Garde les logs peut être une bonne idée. +- Penser à utiliser le bon executable python (virtualenvs) et le bon fichier de + settings pour Django. +- Garder les logs peut être une bonne idée. Exemple : voir le fichier `provisioning/cron.dev`. From 81f3d6ab81dbc0894666b2f99b03e9b2bcadf507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 8 Aug 2017 00:24:00 +0100 Subject: [PATCH 07/75] Move STATIC_ROOT in production --- cof/settings/prod.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cof/settings/prod.py b/cof/settings/prod.py index 5fae5651..f6953479 100644 --- a/cof/settings/prod.py +++ b/cof/settings/prod.py @@ -5,7 +5,8 @@ The settings that are not listed here are imported from .common import os -from .common import * +from .common import * # NOQA +from .common import BASE_DIR DEBUG = False @@ -16,7 +17,13 @@ ALLOWED_HOSTS = [ "dev.cof.ens.fr" ] -STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static") + +STATIC_ROOT = os.path.join( + os.path.dirname(os.path.dirname(BASE_DIR)), + "public", + "static", +) + STATIC_URL = "/gestion/static/" MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") MEDIA_URL = "/gestion/media/" From a6b0c51d394ee04c6e0c789585146444b74a4aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 8 Aug 2017 00:25:13 +0100 Subject: [PATCH 08/75] Add SERVER_EMAIL to the secrets --- cof/settings/common.py | 1 + cof/settings/secret_example.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index 73959c71..bdbf179f 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -30,6 +30,7 @@ def import_secret(name): SECRET_KEY = import_secret("SECRET_KEY") ADMINS = import_secret("ADMINS") +SERVER_EMAIL = import_secret("SERVER_EMAIL") DBNAME = import_secret("DBNAME") DBUSER = import_secret("DBUSER") diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index 0dbc91dc..5e9bc5ca 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,5 +1,6 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' ADMINS = None +SERVER_EMAIL = "root@vagrant" DBUSER = "cof_gestion" DBNAME = "cof_gestion" From cb1d2535175850acbae13d127d0c1c7114e4d939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 9 Aug 2017 20:48:20 +0100 Subject: [PATCH 09/75] Local development settings - Add a new settings file for local development - Update README.md according to the new setup --- .gitignore | 1 + README.md | 79 ++++++++---------------------------------- cof/settings/dev.py | 23 ++---------- cof/settings/local.py | 33 ++++++++++++++++++ manage.py | 2 +- requirements-devel.txt | 2 +- 6 files changed, 53 insertions(+), 87 deletions(-) create mode 100644 cof/settings/local.py diff --git a/.gitignore b/.gitignore index 9e139df9..ab791b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ venv/ /src media/ *.log +*.sqlite3 # PyCharm .idea diff --git a/README.md b/README.md index b6017577..f5e93302 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à entrer dans le navigateur est `localhost:8000`. + #### Serveur de développement type production Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de @@ -79,26 +80,25 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour visualiser la dernière version du code. + ### Installation manuelle -Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il -est fortement conseillé d'utiliser un environnement virtuel pour Python. +Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant, +il est fortement conseillé d'utiliser un environnement virtuel pour Python. -Il vous faudra installer pip, les librairies de développement de python, un -client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées -(Ubuntu, ...) : +Il vous faudra installer pip, les librairies de développement de python ainsi +que sqlite3, un moteur de base de données léger et simple d'utilisation ; sous +Debian et dérivées (Ubuntu, ...) : - sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server + sudo apt-get install python3-pip python3-dev sqlite3 Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF (le dossier où se trouve ce README), et créez-le maintenant : - virtualenv env -p $(which python3) + python3 -m venv venv -L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir -python3, si c'est la version de python par défaut sur votre système, ceci n'est -pas nécessaire. Pour l'activer, il faut faire +Pour l'activer, il faut faire . env/bin/activate @@ -109,61 +109,12 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier pip install -r requirements-devel.txt -Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`. +Pour terminer, copier le fichier `cof/settings/secret_example.py` vers +`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique +pour profiter de faon transparent des mises à jour du fichier d'exemple: -#### Installation avec MySQL + ln -s cof/settngs/secret_example.py cof/settings/secret.py -Il faut maintenant installer MySQL. Si vous n'avez pas déjà MySQL installé sur -votre serveur, il faut l'installer ; sous Debian et dérivées (Ubuntu, ...) : - - sudo apt-get install mysql-server - -Il vous demandera un mot de passe pour le compte d'administration MySQL, -notez-le quelque part (ou n'en mettez pas, le serveur n'est accessible que -localement par défaut). Si vous utilisez une autre distribution, consultez la -documentation de votre distribution pour savoir comment changer ce mot de passe -et démarrer le serveur MySQL (c'est automatique sous Ubuntu). - -Vous devez alors créer un utilisateur local et une base `cof_gestion`, avec le -mot de passe de votre choix (remplacez `mot_de_passe`) : - - mysql -uroot -e "CREATE DATABASE cof_gestion; GRANT ALL PRIVILEGES ON cof_gestion.* TO 'cof_gestion'@'localhost' IDENTIFIER BY 'mot_de_passe'" - -Éditez maintenant le fichier `cof/settings.py` pour y intégrer ces changements ; -la définition de `DATABASES` doit ressembler à (à nouveau, remplacez -`mot_de_passe` de façon appropriée) : - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'cof_gestion', - 'USER': 'cof_gestion', - 'PASSWORD': 'mot_de_passe', - } - } - -#### Installation avec SQLite - -GestioCOF est installé avec MySQL sur la VM COF, et afin d'avoir un -environnement de développement aussi proche que possible de ce qui tourne en -vrai pour éviter les mauvaises surprises, il est conseillé d'utiliser MySQL sur -votre machine de développement également. Toutefois, GestioCOF devrait -fonctionner avec d'autres moteurs SQL, et certains préfèrent utiliser SQLite -pour sa légèreté et facilité d'installation. - -Si vous décidez d'utiliser SQLite, il faut l'installer ; sous Debian et dérivées : - - sudo apt-get install sqlite3 - -puis éditer le fichier `cof/settings.py` pour que la définition de `DATABASES` -ressemble à : - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } - } #### Fin d'installation @@ -188,7 +139,7 @@ Vous êtes prêts à développer ! Lancer GestioCOF en faisant Pour mettre à jour les paquets Python, utiliser la commande suivante : - pip install --upgrade -r requirements.txt -r requirements-devel.txt + pip install --upgrade -r requirements-devel.txt Pour mettre à jour les modèles après une migration, il faut ensuite faire : diff --git a/cof/settings/dev.py b/cof/settings/dev.py index ffd34c7d..61201ea5 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -5,7 +5,8 @@ The settings that are not listed here are imported from .common import os -from .common import * +from .common import * # NOQA +from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE_CLASSES EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -28,26 +29,6 @@ MEDIA_URL = '/media/' # Debug tool bar # --- -# "Versions" panel of django-debug-toolbar <=1.8 is not compatible with -# wagtailmenus. -# See https://github.com/jazzband/django-debug-toolbar/issues/922 -# TODO: Bug should be fixed in ddt 1.9 (not released yet). When fixed, this -# declaration may be removed. -DEBUG_TOOLBAR_PANELS = [ - 'debug_toolbar.panels.timer.TimerPanel', - 'debug_toolbar.panels.settings.SettingsPanel', - 'debug_toolbar.panels.headers.HeadersPanel', - 'debug_toolbar.panels.request.RequestPanel', - 'debug_toolbar.panels.sql.SQLPanel', - 'debug_toolbar.panels.staticfiles.StaticFilesPanel', - 'debug_toolbar.panels.templates.TemplatesPanel', - 'debug_toolbar.panels.cache.CachePanel', - 'debug_toolbar.panels.signals.SignalsPanel', - 'debug_toolbar.panels.logging.LoggingPanel', - 'debug_toolbar.panels.redirects.RedirectsPanel', -] - - def show_toolbar(request): """ On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar diff --git a/cof/settings/local.py b/cof/settings/local.py new file mode 100644 index 00000000..a7ecbe39 --- /dev/null +++ b/cof/settings/local.py @@ -0,0 +1,33 @@ +""" +Django local settings for the cof project. +The settings that are not listed here are imported from .common +""" + +from .dev import * # NOQA + + +# Use sqlite for local development +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "db.sqlite3", + } +} + +# Use the default cache backend for local development +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache" + } +} + +# Use the default in memory asgi backend for local development +CHANNEL_LAYERS = { + "default": { + "BACKEND": "asgiref.inmemory.ChannelLayer", + "ROUTING": "cof.routing.routing", + } +} + +# No need to run collectstatic -> unset STATIC_ROOT +STATIC_ROOT = None diff --git a/manage.py b/manage.py index 7f4e79f6..094ec16f 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.local") from django.core.management import execute_from_command_line diff --git a/requirements-devel.txt b/requirements-devel.txt index 83053f76..de272491 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,4 +1,4 @@ -r requirements.txt -django-debug-toolbar +-e git://github.com/jazzband/django-debug-toolbar.git@88ddc7bdf39c7ff660eac054eab225ac22926754#egg=django-debug-toolbar django-debug-panel ipython From ad15c452375ce7cd9b027e89e96b7fe55a682946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 9 Aug 2017 22:07:49 +0100 Subject: [PATCH 10/75] Switch to nginx + postgres in vagrant --- cof/settings/common.py | 2 +- cof/settings/dev.py | 4 +- provisioning/apache.conf | 39 ------------------- provisioning/bootstrap.sh | 73 +++++++++++++++--------------------- provisioning/daphne.service | 16 ++++++++ provisioning/nginx.conf | 40 ++++++++++++++++++++ provisioning/supervisor.conf | 20 ---------- provisioning/worker.service | 16 ++++++++ requirements.txt | 2 +- 9 files changed, 107 insertions(+), 105 deletions(-) delete mode 100644 provisioning/apache.conf create mode 100644 provisioning/daphne.service create mode 100644 provisioning/nginx.conf delete mode 100644 provisioning/supervisor.conf create mode 100644 provisioning/worker.service diff --git a/cof/settings/common.py b/cof/settings/common.py index bdbf179f..ba0b6044 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -136,7 +136,7 @@ TEMPLATES = [ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', + 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': DBNAME, 'USER': DBUSER, 'PASSWORD': DBPASSWD, diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 61201ea5..01651bba 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -19,9 +19,9 @@ DEBUG = True # --- STATIC_URL = '/static/' -STATIC_ROOT = '/var/www/static/' +STATIC_ROOT = '/srv/gestiocof/static/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') +MEDIA_ROOT = '/srv/gestiocof/media/' MEDIA_URL = '/media/' diff --git a/provisioning/apache.conf b/provisioning/apache.conf deleted file mode 100644 index db5bd602..00000000 --- a/provisioning/apache.conf +++ /dev/null @@ -1,39 +0,0 @@ - - ServerName default - DocumentRoot /var/www/html - - ProxyPreserveHost On - ProxyRequests Off - ProxyPass /static/ ! - ProxyPass /media/ ! - # Pour utiliser un sous-dossier (typiquement /gestion/), il faut faire a la - # place des lignes suivantes: - # - # RequestHeader set Daphne-Root-Path /gestion - # ProxyPass /gestion/ws/ ws://127.0.0.1:8001/ws/ - # ProxyPass /gestion http://127.0.0.1:8001/gestion - # ProxyPassReverse /gestion http://127.0.0.1:8001/gestion - # - # Penser egalement a changer les /static/ et /media/ dans la config apache - # ainsi que dans les settings django. - ProxyPass /ws/ ws://127.0.0.1:8001/ws/ - ProxyPass / http://127.0.0.1:8001/ - ProxyPassReverse / http://127.0.0.1:8001/ - - Alias /media /vagrant/media - Alias /static /var/www/static - - Order deny,allow - Allow from all - - - Order deny,allow - Allow from all - - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 9d210fe8..dd4f1634 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -9,44 +9,48 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" # Installation de paquets utiles apt-get update && apt-get upgrade -y -apt-get install -y python3-pip python3-dev python3-venv libmysqlclient-dev \ - libjpeg-dev git redis-server +apt-get install -y python3-pip python3-dev python3-venv libpq-dev postgresql \ + postgresql-contrib libjpeg-dev nginx git redis-server -# Configuration et installation de mysql. Le mot de passe root est le même que -# le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance -# locale de développement. -echo "mysql-server mysql-server/root_password password $DBPASSWD" | debconf-set-selections -echo "mysql-server mysql-server/root_password_again password $DBPASSWD" | debconf-set-selections +# Postgresql +sudo -u postgres createdb $DBNAME +sudo -u postgres createuser -SdR $DBUSER +sudo -u postgres psql -c "ALTER USER $DBUSER WITH PASSWORD '$DBPASSWD';" +sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DBNAME TO $DBUSER;" -apt-get install -y mysql-server -mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'" -mysql -uroot -p$DBPASSWD -e "GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'" -mysql -uroot -p$DBPASSWD -e "ALTER DATABASE databasename CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# Configuration de redis +# Redis REDIS_PASSWD="dummy" redis-cli CONFIG SET requirepass $REDIS_PASSWD redis-cli -a $REDIS_PASSWD CONFIG REWRITE -# Installation et configuration d'Apache -apt-get install -y apache2 -a2enmod proxy proxy_http proxy_wstunnel headers -cp /vagrant/provisioning/apache.conf /etc/apache2/sites-available/gestiocof.conf -a2ensite gestiocof -a2dissite 000-default -service apache2 restart -mkdir /var/www/static -chown -R ubuntu:www-data /var/www/static +# Contenu static +mkdir -p /srv/gestiocof/{media,static} +chown -R ubuntu:www-data /srv/gestiocof + +# Nginx +ln -s -f /vagrant/provisioning/nginx.conf /etc/nginx/sites-enabled/gestiocof.conf +rm -f /etc/nginx/sites-enabled/default +systemctl reload nginx + +# Environnement virtuel python +sudo -H -u ubuntu python3 -m venv ~ubuntu/venv +sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip +sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r /vagrant/requirements-devel.txt + +# Daphne + runworker +cp /vagrant/provisioning/daphne.service /etc/systemd/system/daphne.service +cp /vagrant/provisioning/worker.service /etc/systemd/system/worker.service +systemctl enable daphne.service +systemctl enable worker.service +systemctl start daphne.service +systemctl start worker.service # Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh` cat >> ~ubuntu/.bashrc < Date: Thu, 10 Aug 2017 15:02:08 +0200 Subject: [PATCH 11/75] [WIP] Tests for kfet views --- kfet/tests/test_views.py | 360 ++++++++++++++++++++++++++++++++++++--- kfet/tests/testcases.py | 142 +++++++++++++++ kfet/tests/utils.py | 105 ++++++++++++ 3 files changed, 585 insertions(+), 22 deletions(-) create mode 100644 kfet/tests/testcases.py create mode 100644 kfet/tests/utils.py diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index ec9d6ffc..f7457786 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1,26 +1,243 @@ +import json from decimal import Decimal -from unittest.mock import patch -from django.test import TestCase, Client -from django.contrib.auth.models import User +from django.contrib.auth.models import Group, Permission +from django.core.urlresolvers import reverse +from django.test import Client, TestCase from django.utils import timezone -from ..models import Account, OperationGroup, Checkout, Operation +from ..models import Account, Checkout, Operation, OperationGroup + +from .testcases import ViewTestCaseMixin +from .utils import create_team, create_user -class AccountTests(TestCase): - """Account related views""" +class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.login.genericteam' + url_expected = '/k-fet/login/genericteam' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + logged_in_username = r.wsgi_request.user.username + self.assertEqual(logged_in_username, 'kfet_genericteam') + + +class AccountListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account' + url_expected = '/k-fet/accounts/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class AccountValidFreeTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.is_validandfree.ajax' + url_expected = '/k-fet/accounts/is_validandfree' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok_isvalid_isfree(self): + """Upper case trigramme not taken is valid and free.""" + r = self.client.get(self.url, {'trigramme': 'AAA'}) + self.assertDictEqual(json.loads(r.content.decode('utf-8')), { + 'is_valid': True, + 'is_free': True, + }) + + def test_ok_isvalid_notfree(self): + """Already taken trigramme is not free, but valid.""" + r = self.client.get(self.url, {'trigramme': '000'}) + self.assertDictEqual(json.loads(r.content.decode('utf-8')), { + 'is_valid': True, + 'is_free': False, + }) + + def test_ok_notvalid_isfree(self): + """Lower case if forbidden but free.""" + r = self.client.get(self.url, {'trigramme': 'aaa'}) + self.assertDictEqual(json.loads(r.content.decode('utf-8')), { + 'is_valid': False, + 'is_free': True, + }) + + +class AccountCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.create' + url_expected = '/k-fet/accounts/new' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def users_extra(self): + return { + 'team__add_account': create_team( + 'team__add_account', '101', + perms=['kfet.add_account'], + ), + } + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + post_data = { + 'trigramme': 'AAA', + 'username': 'plopplopplop', + 'first_name': 'first', + 'last_name': 'last', + 'email': 'email@domain.net', + } + + client = Client() + client.login( + username='team__add_account', + password='team__add_account', + ) + r = client.post(self.url, post_data) + + self.assertRedirects(r, self.url) + a = Account.objects.get(trigramme='AAA') + self.assertEqual(a.username, 'plopplopplop') + + def test_post_forbidden(self): + post_data = { + 'trigramme': 'AAA', + 'username': 'plopplopplop', + 'first_name': 'first', + 'last_name': 'last', + 'email': 'email@domain.net', + } + + # A team member (without kfet.add_account) is authenticated with + # self.client. + r = self.client.post(self.url, post_data) + + self.assertEqual(r.status_code, 200) + with self.assertRaises(Account.DoesNotExist): + Account.objects.get(trigramme='AAA') + + +class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase): + urls_conf = [ + { + 'name': 'kfet.account.create.fromuser', + 'kwargs': {'username': 'user'}, + 'expected': '/k-fet/accounts/new/user/user', + }, + { + 'name': 'kfet.account.create.fromclipper', + 'kwargs': { + 'login_clipper': 'myclipper', + 'fullname': 'first last1 last2', + }, + 'expected': ( + '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2' + ), + }, + { + 'name': 'kfet.account.create.empty', + 'expected': '/k-fet/accounts/new/empty', + }, + ] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_fromuser(self): + r = self.client.get(self.t_urls[0]) + + user = self.users['user'] + + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['user_form'].instance, user) + self.assertEqual(r.context['cof_form'].instance, user.profile) + self.assertIn('account_form', r.context) + + def test_fromclipper(self): + r = self.client.get(self.t_urls[1]) + + self.assertEqual(r.status_code, 200) + self.assertIn('user_form', r.context) + self.assertIn('cof_form', r.context) + self.assertIn('account_form', r.context) + + def test_empty(self): + r = self.client.get(self.t_urls[0]) + + self.assertEqual(r.status_code, 200) + self.assertIn('user_form', r.context) + self.assertIn('cof_form', r.context) + self.assertIn('account_form', r.context) + + +class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.create.autocomplete' + url_expected = '/k-fet/autocomplete/account_new' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url, {'q': 'first'}) + self.assertEqual(r.status_code, 200) + self.assertListEqual(list(r.context['users_notcof']), []) + self.assertListEqual(list(r.context['users_cof']), []) + self.assertListEqual( + list(r.context['kfet']), + [(self.accounts['user'], self.users['user'])], + ) + + +class AccountSearchViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.search.autocomplete' + url_expected = '/k-fet/autocomplete/account_search' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url, {'q': 'first'}) + self.assertEqual(r.status_code, 200) + self.assertListEqual( + list(r.context['accounts']), + [('000', 'first last')], + ) + + +class AccountReadViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.read' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def users_extra(self): + return { + 'user1': create_user('user1', '001'), + } def setUp(self): - # A user and its account - self.user = User.objects.create_user(username="foobar", password="foo") - acc = Account.objects.create( - trigramme="FOO", cofprofile=self.user.profile - ) + super().setUp() + + user1_acc = self.accounts['user1'] + team_acc = self.accounts['team'] # Dummy operations and operation groups checkout = Checkout.objects.create( - created_by=acc, name="checkout", + created_by=team_acc, name="checkout", valid_from=timezone.now(), valid_to=timezone.now() + timezone.timedelta(days=365) ) @@ -30,7 +247,7 @@ class AccountTests(TestCase): ] OperationGroup.objects.bulk_create([ OperationGroup( - on_acc=acc, checkout=checkout, at=at, is_cof=False, + on_acc=user1_acc, checkout=checkout, at=at, is_cof=False, amount=amount ) for (at, amount) in opeg_data @@ -47,13 +264,112 @@ class AccountTests(TestCase): amount=Decimal('3') ) - @patch('gestioncof.signals.messages') - def test_account_read(self, mock_messages): - """We can query the "Account - Read" page.""" + def test_ok(self): + """We can query the "Account - Read" page.""" + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_ok_self(self): client = Client() - self.assertTrue(client.login( - username="foobar", - password="foo" - )) - resp = client.get("/k-fet/accounts/FOO") - self.assertEqual(200, resp.status_code) + client.login(username='user1', password='user1') + r = client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.update' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001/edit' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def users_extra(self): + return { + 'user1': create_user('user1', '001'), + } + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_ok_self(self): + client = Client() + client.login(username='user1', password='user1') + r = client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class BaseAccountGroupViewTests(ViewTestCaseMixin): + auth_user = 'team__manage_perms' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return { + 'team__manage_perms': create_team( + 'team__manage_perms', '101', + perms=['kfet.manage_perms'], + ), + } + + +class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase): + url_name = 'kfet.account.group' + url_expected = '/k-fet/accounts/groups' + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + self.assertQuerysetEqual( + r.context['groups'], + Group.objects.filter(name__icontains='K-Fêt'), + ordered=False, + ) + + +class AccountGroupCreateViewTests(BaseAccountGroupViewTests, TestCase): + url_name = 'kfet.account.group.create' + url_expected = '/k-fet/accounts/groups/new' + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class AccountGroupUpdateViewTests(BaseAccountGroupViewTests, TestCase): + url_name = 'kfet.account.group.update' + url_kwargs = {'pk': 42} + url_expected = '/k-fet/accounts/groups/42/edit' + + def setUp(self): + super().setUp() + self.group1 = Group.objects.create(pk=42, name='K-Fêt - Group') + self.group1.permissions = [ + Permission.objects.get( + content_type__app_label='kfet', + codename='is_team', + ) + ] + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + post_data = { + 'name': 'Group42', + 'permissions': ( + self.group1.permissions + .values_list('pk', flat=True) + ), + } + + r = self.client.post(self.url, post_data) + + self.assertRedirects(r, reverse('kfet.account.group')) + + self.group1.refresh_from_db() + self.assertEqual(self.group1.name, 'K-Fêt Group42') diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py new file mode 100644 index 00000000..c2e1b848 --- /dev/null +++ b/kfet/tests/testcases.py @@ -0,0 +1,142 @@ +from unittest import mock + +from django.core.urlresolvers import reverse +from django.http import QueryDict +from django.test import Client + +from .utils import create_root, create_team, create_user + + +class ViewTestCaseMixin: + url_name = None + url_expected = None + + auth_user = None + auth_forbidden = [] + + def setUp(self): + # Signals handlers on login/logout send messages. + # Due to the way the Django' test Client performs login, this raise an + # error. As workaround, we mock the Django' messages module. + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + self.users = {} + self.accounts = {} + + for label, user in {**self.users_base, **self.users_extra}.items(): + self.register_user(label, user) + + if self.auth_user: + # The wrapper is a sanity check. + self.assertTrue( + self.client.login( + username=self.auth_user, + password=self.auth_user, + ) + ) + + @property + def users_base(self): + # Format desc: username, password, trigramme + return { + # user, user, 000 + 'user': create_user(), + # team, team, 100 + 'team': create_team(), + # root, root, 200 + 'root': create_root(), + } + + @property + def users_extra(self): + return {} + + def register_user(self, label, user): + self.users[label] = user + if hasattr(user.profile, 'account_kfet'): + self.accounts[label] = user.profile.account_kfet + + @property + def urls_conf(self): + return [{ + 'name': self.url_name, + 'args': getattr(self, 'url_args', []), + 'kwargs': getattr(self, 'url_kwargs', {}), + 'expected': self.url_expected, + }] + + @property + def t_urls(self): + return [ + reverse( + url_conf['name'], + args=url_conf.get('args', []), + kwargs=url_conf.get('kwargs', {}), + ) + for url_conf in self.urls_conf] + + @property + def url(self): + return self.t_urls[0] + + def assertForbidden(self, response): + request = response.wsgi_request + + try: + try: + # Is this an HTTP Forbidden response ? + self.assertEqual(response.status_code, 403) + except AssertionError: + # A redirection to the login view is fine too. + + # Let's build the login url with the 'next' param on current + # page. + full_path = request.get_full_path() + + querystring = QueryDict(mutable=True) + querystring['next'] = full_path + + login_url = '/login?' + querystring.urlencode(safe='/') + + # We don't focus on what the login view does. + # So don't fetch the redirect. + self.assertRedirects( + response, login_url, + fetch_redirect_response=False, + ) + except AssertionError: + raise AssertionError( + "%(http_method)s request at %(path)s should be forbidden for " + "%(username)s user.\n" + "Response isn't 403, nor a redirect to login view. Instead, " + "response code is %(code)d." % { + 'http_method': request.method, + 'path': request.get_full_path(), + 'username': ( + "'{}'".format(request.user.username) + if request.user.username + else 'anonymous' + ), + 'code': response.status_code, + } + ) + + def assertForbiddenKfet(self, response): + self.assertEqual(response.status_code, 200) + form = response.context['form'] + self.assertIn("Permission refusée", form.non_field_errors) + + def test_urls(self): + for url, conf in zip(self.t_urls, self.urls_conf): + self.assertEqual(url, conf['expected']) + + def test_forbidden(self): + for creds in self.auth_forbidden: + for url in self.t_urls: + client = Client() + if creds is not None: + client.login(username=creds, password=creds) + r = client.get(url) + self.assertForbidden(r) diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py new file mode 100644 index 00000000..4b739003 --- /dev/null +++ b/kfet/tests/utils.py @@ -0,0 +1,105 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission + +from ..models import Account + + +User = get_user_model() + + +def user_add_perms(user, perms_labels): + """ + Add perms to a user. + + Args: + user (User instance) + perms (list of str 'app.perm_name') + + Returns: + The same user (refetched from DB to avoid missing perms) + + """ + u_labels = set(perms_labels) + + perms = [] + for label in u_labels: + app_label, codename = label.split('.', 1) + perms.append( + Permission.objects.get( + content_type__app_label=app_label, + codename=codename, + ) + ) + + user.user_permissions.add(*perms) + + # If permissions have already been fetched for this user, we need to reload + # it to avoid using of the previous permissions cache. + # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching + return User.objects.get(pk=user.pk) + + +def _create_user_and_account(user_attrs, account_attrs, perms=None): + user_attrs.setdefault('password', user_attrs['username']) + user = User.objects.create_user(**user_attrs) + + account_attrs['cofprofile'] = user.profile + kfet_pwd = account_attrs.pop('password', None) + + account = Account.objects.create(**account_attrs) + + if perms is not None: + user = user_add_perms(user, perms) + + if 'kfet.is_team' in perms: + if kfet_pwd is None: + kfet_pwd = 'kfetpwd_{}'.format(user_attrs['password']) + account.change_pwd(kfet_pwd) + account.save() + + return user + + +def create_user(username='user', trigramme='000', **kwargs): + user_attrs = kwargs.setdefault('user_attrs', {}) + + user_attrs.setdefault('username', username) + user_attrs.setdefault('first_name', 'first') + user_attrs.setdefault('last_name', 'last') + user_attrs.setdefault('email', 'mail@user.net') + + account_attrs = kwargs.setdefault('account_attrs', {}) + account_attrs.setdefault('trigramme', trigramme) + + return _create_user_and_account(**kwargs) + + +def create_team(username='team', trigramme='100', **kwargs): + user_attrs = kwargs.setdefault('user_attrs', {}) + + user_attrs.setdefault('username', username) + user_attrs.setdefault('first_name', 'team') + user_attrs.setdefault('last_name', 'member') + user_attrs.setdefault('email', 'mail@team.net') + + account_attrs = kwargs.setdefault('account_attrs', {}) + account_attrs.setdefault('trigramme', trigramme) + + perms = kwargs.setdefault('perms', []) + perms.append('kfet.is_team') + + return _create_user_and_account(**kwargs) + + +def create_root(username='root', trigramme='200', **kwargs): + user_attrs = kwargs.setdefault('user_attrs', {}) + + user_attrs.setdefault('username', username) + user_attrs.setdefault('first_name', 'super') + user_attrs.setdefault('last_name', 'user') + user_attrs.setdefault('email', 'mail@root.net') + + account_attrs = kwargs.setdefault('account_attrs', {}) + account_attrs.setdefault('trigramme', trigramme) + + return _create_user_and_account(**kwargs) From bd1dace8e8b8b6632a1354e87c2769247a9ff22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 10 Aug 2017 16:31:09 +0100 Subject: [PATCH 12/75] Fix symlink for secret.py --- README.md | 2 +- provisioning/bootstrap.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5e93302..9ba4642b 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Pour terminer, copier le fichier `cof/settings/secret_example.py` vers `cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique pour profiter de faon transparent des mises à jour du fichier d'exemple: - ln -s cof/settngs/secret_example.py cof/settings/secret.py + ln -s secret_example.py cof/settings/secret.py #### Fin d'installation diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index dd4f1634..f604cad0 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -61,7 +61,7 @@ EOF cd /vagrant # Préparation de Django -ln -s -f cof/settings/secret_example.py cof/settings/secret.py +ln -s -f secret_example.py cof/settings/secret.py sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings.dev' bash provisioning/prepare_django.sh # Installation du cron pour les mails de rappels From 4075fcaa643a14d21a350db79a987bed4852e2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 10 Aug 2017 16:35:41 +0100 Subject: [PATCH 13/75] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ba4642b..982f69fd 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier Pour terminer, copier le fichier `cof/settings/secret_example.py` vers `cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique -pour profiter de faon transparent des mises à jour du fichier d'exemple: +pour profiter de façon transparente des mises à jour du fichier d'exemple: ln -s secret_example.py cof/settings/secret.py From 73296ea251b14725f97ec9a7629c8d27f362324a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 10 Aug 2017 16:55:36 +0100 Subject: [PATCH 14/75] Setup django before launching daphne + Django --- provisioning/bootstrap.sh | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index f604cad0..827db06d 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -38,6 +38,14 @@ sudo -H -u ubuntu python3 -m venv ~ubuntu/venv sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r /vagrant/requirements-devel.txt +# Préparation de Django +cd /vagrant +ln -s -f secret_example.py cof/settings/secret.py +sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings.dev' bash provisioning/prepare_django.sh + +# Installation du cron pour les mails de rappels +sudo -H -u ubuntu crontab provisioning/cron.dev + # Daphne + runworker cp /vagrant/provisioning/daphne.service /etc/systemd/system/daphne.service cp /vagrant/provisioning/worker.service /etc/systemd/system/worker.service @@ -57,12 +65,3 @@ source ~/venv/bin/activate # On va dans /vagrant où se trouve le code de gestioCOF cd /vagrant EOF - -cd /vagrant - -# Préparation de Django -ln -s -f secret_example.py cof/settings/secret.py -sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings.dev' bash provisioning/prepare_django.sh - -# Installation du cron pour les mails de rappels -sudo -H -u ubuntu crontab provisioning/cron.dev From dae418af3d89105ecc54a74a14dc82b7eabba30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 10 Aug 2017 16:56:03 +0100 Subject: [PATCH 15/75] Handle websockets in nginx.conf --- provisioning/nginx.conf | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/provisioning/nginx.conf b/provisioning/nginx.conf index 07f290f3..d2f55bd0 100644 --- a/provisioning/nginx.conf +++ b/provisioning/nginx.conf @@ -9,7 +9,7 @@ server { server_name localhost; # All the trafic is routed to Daphne - location ~ ^/ { + location / { # A copy-paste of what we have in production proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; @@ -18,21 +18,42 @@ server { proxy_set_header X-SSL-Client-Serial $ssl_client_serial; proxy_set_header X-SSL-Client-Verify $ssl_client_verify; proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; + # Reverse-proxy proxy_pass http://gestiocof; } + # Upgrading the connection when handling websockets + location /ws/ { + # A copy-paste of what we have in production + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-SSL-Client-Serial $ssl_client_serial; + proxy_set_header X-SSL-Client-Verify $ssl_client_verify; + proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; + + # See http://nginx.org/en/docs/http/websocket.html + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_pass http://gestiocof; + } + # Static files - location ~ ^/static/ { - root /srv/gestiocof/static/; + location /static/ { + root /srv/gestiocof/; access_log off; add_header Cache-Control "public"; expires 7d; } # Uploaded media - location ~ ^/media/ { - root /srv/gestiocof/static/; + location /media/ { + root /srv/gestiocof/; access_log off; add_header Cache-Control "public"; expires 7d; From 522acafb2ee68645af72eafa3c48dcd21b1dbf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 10 Aug 2017 16:56:20 +0100 Subject: [PATCH 16/75] Add python dependency for Debian9 --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index c12798dc..f3964212 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,6 @@ channels==1.1.5 python-dateutil wagtail==1.10.* wagtailmenus==2.2.* + +# Production tools +wheel From 853fa57ce4945abfd3541844430a4b1ab0954c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 11 Aug 2017 15:45:19 +0100 Subject: [PATCH 17/75] In README.md: typo env -> venv --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 982f69fd..e5ac882f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ fortement conseillé), déplacez-vous dans le dossier où est installé GestioCO Pour l'activer, il faut faire - . env/bin/activate + . venv/bin/activate dans le même dossier. From 59231661968b0435add613edf5c5c0ebc30d3e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 11 Aug 2017 17:24:09 +0100 Subject: [PATCH 18/75] Specify the full path of the sqlite database --- cof/settings/local.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cof/settings/local.py b/cof/settings/local.py index a7ecbe39..6e1f0802 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -3,14 +3,17 @@ Django local settings for the cof project. The settings that are not listed here are imported from .common """ +import os + from .dev import * # NOQA +from .dev import BASE_DIR # Use sqlite for local development DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "db.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3") } } From 4d026407d104ebc260c051b340540a9e14be038b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 11 Aug 2017 23:56:02 +0100 Subject: [PATCH 19/75] Quicker setup for local development --- README.md | 39 +++++++++++++++------------------- provisioning/bootstrap.sh | 5 ++++- provisioning/prepare_django.sh | 3 --- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e5ac882f..0e84146b 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,17 @@ entrer dans le navigateur est `localhost:8000`. #### Serveur de développement type production -Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de -façon similaire à la version en production : on utilise +Juste histoire de jouer, pas indispensable pour développer : + +La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF +comme en production : on utilise [Daphne](https://github.com/django/daphne/) et `python manage.py runworker` -derrière un reverse-proxy apache. Le tout est monitoré par -[supervisor](http://supervisord.org/). +derrière un reverse-proxy nginx. Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url `localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code -change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour -visualiser la dernière version du code. +change, il faut relancer le worker avec `sudo systemctl restart worker.service` +pour visualiser la dernière version du code. ### Installation manuelle @@ -87,7 +88,7 @@ Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant, il est fortement conseillé d'utiliser un environnement virtuel pour Python. Il vous faudra installer pip, les librairies de développement de python ainsi -que sqlite3, un moteur de base de données léger et simple d'utilisation ; sous +que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous Debian et dérivées (Ubuntu, ...) : sudo apt-get install python3-pip python3-dev sqlite3 @@ -107,29 +108,23 @@ dans le même dossier. Vous pouvez maintenant installer les dépendances Python depuis le fichier `requirements-devel.txt` : + pip install -U pip pip install -r requirements-devel.txt Pour terminer, copier le fichier `cof/settings/secret_example.py` vers `cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique -pour profiter de façon transparente des mises à jour du fichier d'exemple: +pour profiter de façon transparente des mises à jour du fichier: ln -s secret_example.py cof/settings/secret.py #### Fin d'installation -Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante : +Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base +de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des +données bidons bien pratiques pour développer avec la commande suivante : - python manage.py migrate - -Charger les mails indispensables au bon fonctionnement de GestioCOF : - - python manage.py syncmails - -Une base de donnée pré-remplie est disponible en lançant les commandes : - - python manage.py loaddata gestion sites articles - python manage.py loaddevdata + bash provisioning/prepare_django.sh Vous êtes prêts à développer ! Lancer GestioCOF en faisant @@ -148,6 +143,6 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire : ## Documentation utilisateur -Une brève documentation utilisateur pour se familiariser plus vite avec l'outil -est accessible sur le -[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home). +Une brève documentation utilisateur est accessible sur le +[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une +idée de la façon dont le COF utilise GestioCOF. diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 827db06d..4505db3f 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -41,7 +41,10 @@ sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r /vagrant/requirements-devel.tx # Préparation de Django cd /vagrant ln -s -f secret_example.py cof/settings/secret.py -sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings.dev' bash provisioning/prepare_django.sh +sudo -H -u ubuntu \ + DJANGO_SETTINGS_MODULE='cof.settings.dev' \ + bash -c ". ~/venv/bin/activate && bash provisioning/prepare_django.sh" +/home/ubuntu/venv/bin/python manage.py collectstatic --noinput --settings cof.settings.dev # Installation du cron pour les mails de rappels sudo -H -u ubuntu crontab provisioning/cron.dev diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index 4ec1a70f..1818a0cd 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -1,9 +1,6 @@ #!/bin/bash -# Doit être lancé par bootstrap.sh -source ~/venv/bin/activate python manage.py migrate python manage.py loaddata gestion sites articles python manage.py loaddevdata python manage.py syncmails -python manage.py collectstatic --noinput From 4c08962e093f31b65ad8a154887912685d99f9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 13 Aug 2017 13:39:13 +0100 Subject: [PATCH 20/75] Hide more stuff in secret.py --- cof/settings/prod.py | 4 ---- cof/settings/secret_example.py | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cof/settings/prod.py b/cof/settings/prod.py index f6953479..286b5547 100644 --- a/cof/settings/prod.py +++ b/cof/settings/prod.py @@ -27,7 +27,3 @@ STATIC_ROOT = os.path.join( STATIC_URL = "/gestion/static/" MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") MEDIA_URL = "/gestion/media/" - -LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636" - -EMAIL_HOST = "nef.ens.fr" diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index 5e9bc5ca..e9c0e63c 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -14,4 +14,7 @@ REDIS_HOST = "127.0.0.1" RECAPTCHA_PUBLIC_KEY = "DUMMY" RECAPTCHA_PRIVATE_KEY = "DUMMY" +EMAIL_HOST = None + KFETOPEN_TOKEN = "plop" +LDAP_SERVER_URL = None From 2a519bfedf1c650453cafa84d61cc79e90f892e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 13 Aug 2017 14:36:45 +0100 Subject: [PATCH 21/75] Add SCRIPT_NAME to the production-like server --- README.md | 6 ++-- cof/settings/dev.py | 4 +-- provisioning/bootstrap.sh | 5 +-- provisioning/nginx.conf | 67 ++++++++++++++++++--------------------- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0e84146b..01f4ead2 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ comme en production : on utilise derrière un reverse-proxy nginx. Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url -`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code -change, il faut relancer le worker avec `sudo systemctl restart worker.service` -pour visualiser la dernière version du code. +`localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le +code change, il faut relancer le worker avec `sudo systemctl restart +worker.service` pour visualiser la dernière version du code. ### Installation manuelle diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 01651bba..9c622063 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -3,10 +3,8 @@ Django development settings for the cof project. The settings that are not listed here are imported from .common """ -import os - from .common import * # NOQA -from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE_CLASSES +from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 4505db3f..69bbcf4c 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -24,8 +24,9 @@ REDIS_PASSWD="dummy" redis-cli CONFIG SET requirepass $REDIS_PASSWD redis-cli -a $REDIS_PASSWD CONFIG REWRITE -# Contenu static -mkdir -p /srv/gestiocof/{media,static} +# Contenu statique +mkdir -p /srv/gestiocof/media +mkdir -p /srv/gestiocof/static chown -R ubuntu:www-data /srv/gestiocof # Nginx diff --git a/provisioning/nginx.conf b/provisioning/nginx.conf index d2f55bd0..015e1712 100644 --- a/provisioning/nginx.conf +++ b/provisioning/nginx.conf @@ -7,45 +7,15 @@ server { listen 80; server_name localhost; + root /srv/gestiocof/; - # All the trafic is routed to Daphne - location / { - # A copy-paste of what we have in production - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-SSL-Client-Serial $ssl_client_serial; - proxy_set_header X-SSL-Client-Verify $ssl_client_verify; - proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; - - # Reverse-proxy - proxy_pass http://gestiocof; - } - - # Upgrading the connection when handling websockets - location /ws/ { - # A copy-paste of what we have in production - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-SSL-Client-Serial $ssl_client_serial; - proxy_set_header X-SSL-Client-Verify $ssl_client_verify; - proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; - - # See http://nginx.org/en/docs/http/websocket.html - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_pass http://gestiocof; - } + # / → /gestion/ + # /gestion → /gestion/ + rewrite ^/$ /gestion/; + rewrite ^/gestion$ /gestion/; # Static files location /static/ { - root /srv/gestiocof/; access_log off; add_header Cache-Control "public"; expires 7d; @@ -53,9 +23,34 @@ server { # Uploaded media location /media/ { - root /srv/gestiocof/; access_log off; add_header Cache-Control "public"; expires 7d; } + + location /gestion/ { + # A copy-paste of what we have in production + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-SSL-Client-Serial $ssl_client_serial; + proxy_set_header X-SSL-Client-Verify $ssl_client_verify; + proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; + proxy_set_header Daphne-Root-Path /gestion; + + location /gestion/ws/ { + # See http://nginx.org/en/docs/http/websocket.html + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_pass http://gestiocof/ws/; + } + + location /gestion/ { + proxy_pass http://gestiocof; + } + } } From 2cfce1c921d3a75185a6131061ce5edc797d7c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 16 Aug 2017 17:45:59 +0200 Subject: [PATCH 22/75] Add tests for kfet views. kfet.tests.testcases embed mixins for TestCase: - TestCaseMixin provides assertion helpers, - ViewTestCaseMixin provides a few basic tests, which are common to every view. kfet.tests.utils provides helpers for users and permissions management. Each kfet view get a testcase (at least very basic) in kfet.tests.test_views. --- kfet/tests/test_tests_utils.py | 95 ++ kfet/tests/test_views.py | 2121 ++++++++++++++++++++++++++++++-- kfet/tests/testcases.py | 180 ++- kfet/tests/utils.py | 75 +- 4 files changed, 2265 insertions(+), 206 deletions(-) create mode 100644 kfet/tests/test_tests_utils.py diff --git a/kfet/tests/test_tests_utils.py b/kfet/tests/test_tests_utils.py new file mode 100644 index 00000000..8308bd5b --- /dev/null +++ b/kfet/tests/test_tests_utils.py @@ -0,0 +1,95 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from gestioncof.models import CofProfile + +from ..models import Account +from .testcases import TestCaseMixin +from .utils import ( + create_user, create_team, create_root, get_perms, user_add_perms, +) + + +User = get_user_model() + + +class UserHelpersTests(TestCaseMixin, TestCase): + + def test_create_user(self): + """create_user creates a basic user and its account.""" + u = create_user() + a = u.profile.account_kfet + + self.assertInstanceExpected(u, { + 'get_full_name': 'first last', + 'username': 'user', + }) + self.assertFalse(u.user_permissions.exists()) + + self.assertEqual('000', a.trigramme) + + def test_create_team(self): + u = create_team() + a = u.profile.account_kfet + + self.assertInstanceExpected(u, { + 'get_full_name': 'team member', + 'username': 'team', + }) + self.assertTrue(u.has_perm('kfet.is_team')) + + self.assertEqual('100', a.trigramme) + + def test_create_root(self): + u = create_root() + a = u.profile.account_kfet + + self.assertInstanceExpected(u, { + 'get_full_name': 'super user', + 'username': 'root', + 'is_superuser': True, + 'is_staff': True, + }) + + self.assertEqual('200', a.trigramme) + + +class PermHelpersTest(TestCaseMixin, TestCase): + + def setUp(self): + cts = ContentType.objects.get_for_models(Account, CofProfile) + self.perm1 = Permission.objects.create( + content_type=cts[Account], + codename='test_perm', + name='Perm for test', + ) + self.perm2 = Permission.objects.create( + content_type=cts[CofProfile], + codename='another_test_perm', + name='Another one', + ) + self.perm_team = Permission.objects.get( + content_type__app_label='kfet', + codename='is_team', + ) + + def test_get_perms(self): + perms = get_perms('kfet.test_perm', 'gestioncof.another_test_perm') + self.assertDictEqual(perms, { + 'kfet.test_perm': self.perm1, + 'gestioncof.another_test_perm': self.perm2, + }) + + def test_user_add_perms(self): + user = User.objects.create_user(username='user', password='user') + user.user_permissions.add(self.perm1) + + user_add_perms(user, ['kfet.is_team', 'gestioncof.another_test_perm']) + + self.assertQuerysetEqual( + user.user_permissions.all(), + map(repr, [self.perm1, self.perm2, self.perm_team]), + ordered=False, + ) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index f7457786..5ac82f33 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1,15 +1,21 @@ import json +from datetime import datetime, timedelta from decimal import Decimal +from unittest import mock -from django.contrib.auth.models import Group, Permission +from django.contrib.auth.models import Group from django.core.urlresolvers import reverse from django.test import Client, TestCase from django.utils import timezone -from ..models import Account, Checkout, Operation, OperationGroup - +from ..config import kfet_config +from ..models import ( + Account, Article, ArticleCategory, Checkout, CheckoutStatement, Inventory, + InventoryArticle, Operation, OperationGroup, Order, OrderArticle, Supplier, + SupplierArticle, Transfer, TransferGroup, +) from .testcases import ViewTestCaseMixin -from .utils import create_team, create_user +from .utils import create_team, create_user, get_perms class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase): @@ -22,8 +28,8 @@ class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - logged_in_username = r.wsgi_request.user.username - self.assertEqual(logged_in_username, 'kfet_genericteam') + logged_in = r.wsgi_request.user + self.assertEqual(logged_in.username, 'kfet_genericteam') class AccountListViewTests(ViewTestCaseMixin, TestCase): @@ -74,16 +80,23 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase): url_name = 'kfet.account.create' url_expected = '/k-fet/accounts/new' + http_methods = ['GET', 'POST'] + auth_user = 'team' auth_forbidden = [None, 'user'] + post_data = { + 'trigramme': 'AAA', + 'username': 'plopplopplop', + 'first_name': 'first', + 'last_name': 'last', + 'email': 'email@domain.net', + } + @property def users_extra(self): return { - 'team__add_account': create_team( - 'team__add_account', '101', - perms=['kfet.add_account'], - ), + 'team1': create_team('team1', '101', perms=['kfet.add_account']), } def test_get_ok(self): @@ -91,91 +104,69 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) def test_post_ok(self): - post_data = { - 'trigramme': 'AAA', + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.account.create')) + + account = Account.objects.get(trigramme='AAA') + + self.assertInstanceExpected(account, { 'username': 'plopplopplop', 'first_name': 'first', 'last_name': 'last', - 'email': 'email@domain.net', - } - - client = Client() - client.login( - username='team__add_account', - password='team__add_account', - ) - r = client.post(self.url, post_data) - - self.assertRedirects(r, self.url) - a = Account.objects.get(trigramme='AAA') - self.assertEqual(a.username, 'plopplopplop') + }) def test_post_forbidden(self): - post_data = { - 'trigramme': 'AAA', - 'username': 'plopplopplop', - 'first_name': 'first', - 'last_name': 'last', - 'email': 'email@domain.net', - } - - # A team member (without kfet.add_account) is authenticated with - # self.client. - r = self.client.post(self.url, post_data) - - self.assertEqual(r.status_code, 200) - with self.assertRaises(Account.DoesNotExist): - Account.objects.get(trigramme='AAA') + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) class AccountCreateAjaxViewTests(ViewTestCaseMixin, TestCase): - urls_conf = [ - { - 'name': 'kfet.account.create.fromuser', - 'kwargs': {'username': 'user'}, - 'expected': '/k-fet/accounts/new/user/user', + urls_conf = [{ + 'name': 'kfet.account.create.fromuser', + 'kwargs': {'username': 'user'}, + 'expected': '/k-fet/accounts/new/user/user', + }, { + 'name': 'kfet.account.create.fromclipper', + 'kwargs': { + 'login_clipper': 'myclipper', + 'fullname': 'first last1 last2', }, - { - 'name': 'kfet.account.create.fromclipper', - 'kwargs': { - 'login_clipper': 'myclipper', - 'fullname': 'first last1 last2', - }, - 'expected': ( - '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2' - ), - }, - { - 'name': 'kfet.account.create.empty', - 'expected': '/k-fet/accounts/new/empty', - }, - ] + 'expected': ( + '/k-fet/accounts/new/clipper/myclipper/first%20last1%20last2' + ), + }, { + 'name': 'kfet.account.create.empty', + 'expected': '/k-fet/accounts/new/empty', + }] auth_user = 'team' auth_forbidden = [None, 'user'] def test_fromuser(self): r = self.client.get(self.t_urls[0]) + self.assertEqual(r.status_code, 200) user = self.users['user'] - self.assertEqual(r.status_code, 200) self.assertEqual(r.context['user_form'].instance, user) self.assertEqual(r.context['cof_form'].instance, user.profile) self.assertIn('account_form', r.context) def test_fromclipper(self): r = self.client.get(self.t_urls[1]) - self.assertEqual(r.status_code, 200) + self.assertIn('user_form', r.context) self.assertIn('cof_form', r.context) self.assertIn('account_form', r.context) def test_empty(self): - r = self.client.get(self.t_urls[0]) - + r = self.client.get(self.t_urls[2]) self.assertEqual(r.status_code, 200) + self.assertIn('user_form', r.context) self.assertIn('cof_form', r.context) self.assertIn('account_form', r.context) @@ -191,12 +182,11 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url, {'q': 'first'}) self.assertEqual(r.status_code, 200) - self.assertListEqual(list(r.context['users_notcof']), []) - self.assertListEqual(list(r.context['users_cof']), []) - self.assertListEqual( - list(r.context['kfet']), - [(self.accounts['user'], self.users['user'])], - ) + self.assertEqual(len(r.context['users_notcof']), 0) + self.assertEqual(len(r.context['users_cof']), 0) + self.assertSetEqual(set(r.context['kfet']), set([ + (self.accounts['user'], self.users['user']), + ])) class AccountSearchViewTests(ViewTestCaseMixin, TestCase): @@ -209,10 +199,9 @@ class AccountSearchViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url, {'q': 'first'}) self.assertEqual(r.status_code, 200) - self.assertListEqual( - list(r.context['accounts']), - [('000', 'first last')], - ) + self.assertSetEqual(set(r.context['accounts']), set([ + ('000', 'first last'), + ])) class AccountReadViewTests(ViewTestCaseMixin, TestCase): @@ -281,43 +270,105 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): url_kwargs = {'trigramme': '001'} url_expected = '/k-fet/accounts/001/edit' + http_methods = ['GET', 'POST'] + auth_user = 'team' auth_forbidden = [None, 'user'] + post_data = { + # User + 'first_name': 'The first', + 'last_name': 'The last', + 'email': '', + # Group + 'groups[]': [], + # Account + 'trigramme': '051', + 'nickname': '', + 'promo': '', + # 'is_frozen': not checked + # Account password + 'pwd1': '', + 'pwd2': '', + } + @property def users_extra(self): return { 'user1': create_user('user1', '001'), + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_account', + ]), } - def test_ok(self): + def test_get_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - def test_ok_self(self): + def test_get_ok_self(self): client = Client() client.login(username='user1', password='user1') r = client.get(self.url) self.assertEqual(r.status_code, 200) + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') -class BaseAccountGroupViewTests(ViewTestCaseMixin): - auth_user = 'team__manage_perms' + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.account.read', args=['051'])) + + self.accounts['user1'].refresh_from_db() + self.users['user1'].refresh_from_db() + + self.assertInstanceExpected(self.accounts['user1'], { + 'first_name': 'The first', + 'last_name': 'The last', + 'trigramme': '051', + }) + + def test_post_ok_self(self): + client = Client() + client.login(username='user1', password='user1') + + post_data = { + 'first_name': 'The first', + 'last_name': 'The last', + } + + r = client.post(self.url, post_data) + self.assertRedirects(r, reverse('kfet.account.read', args=['001'])) + + self.accounts['user1'].refresh_from_db() + self.users['user1'].refresh_from_db() + + self.assertInstanceExpected(self.accounts['user1'], { + 'first_name': 'The first', + 'last_name': 'The last', + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.group' + url_expected = '/k-fet/accounts/groups' + + auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] @property def users_extra(self): return { - 'team__manage_perms': create_team( - 'team__manage_perms', '101', - perms=['kfet.manage_perms'], - ), + 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), } - -class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase): - url_name = 'kfet.account.group' - url_expected = '/k-fet/accounts/groups' + def setUp(self): + super().setUp() + self.group1 = Group.objects.create(name='K-Fêt - Group1') + self.group2 = Group.objects.create(name='K-Fêt - Group2') def test_ok(self): r = self.client.get(self.url) @@ -325,51 +376,1893 @@ class AccountGroupListViewTests(BaseAccountGroupViewTests, TestCase): self.assertQuerysetEqual( r.context['groups'], - Group.objects.filter(name__icontains='K-Fêt'), + map(repr, [self.group1, self.group2]), ordered=False, ) -class AccountGroupCreateViewTests(BaseAccountGroupViewTests, TestCase): +class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): url_name = 'kfet.account.group.create' url_expected = '/k-fet/accounts/groups/new' - def test_ok(self): - r = self.client.get(self.url) - self.assertEqual(r.status_code, 200) + http_methods = ['GET', 'POST'] + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] -class AccountGroupUpdateViewTests(BaseAccountGroupViewTests, TestCase): - url_name = 'kfet.account.group.update' - url_kwargs = {'pk': 42} - url_expected = '/k-fet/accounts/groups/42/edit' + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), + } + + @property + def post_data(self): + return { + 'name': 'The Group', + 'permissions': [ + str(self.perms['kfet.is_team'].pk), + str(self.perms['kfet.manage_perms'].pk), + ], + } def setUp(self): super().setUp() - self.group1 = Group.objects.create(pk=42, name='K-Fêt - Group') - self.group1.permissions = [ - Permission.objects.get( - content_type__app_label='kfet', - codename='is_team', - ) - ] + self.perms = get_perms( + 'kfet.is_team', + 'kfet.manage_perms', + ) def test_get_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) def test_post_ok(self): - post_data = { - 'name': 'Group42', - 'permissions': ( - self.group1.permissions - .values_list('pk', flat=True) - ), - } - - r = self.client.post(self.url, post_data) - + r = self.client.post(self.url, self.post_data) self.assertRedirects(r, reverse('kfet.account.group')) - self.group1.refresh_from_db() - self.assertEqual(self.group1.name, 'K-Fêt Group42') + group = Group.objects.get(name='K-Fêt The Group') + + self.assertQuerysetEqual( + group.permissions.all(), + map(repr, [ + self.perms['kfet.is_team'], + self.perms['kfet.manage_perms'], + ]), + ordered=False, + ) + + +class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.group.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def url_kwargs(self): + return {'pk': self.group.pk} + + @property + def url_expected(self): + return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), + } + + @property + def post_data(self): + return { + 'name': 'The Group', + 'permissions': [ + str(self.perms['kfet.is_team'].pk), + str(self.perms['kfet.manage_perms'].pk), + ], + } + + def setUp(self): + super().setUp() + self.perms = get_perms( + 'kfet.is_team', + 'kfet.manage_perms', + ) + self.group = Group.objects.create(name='K-Fêt - Group') + self.group.permissions = self.perms.values() + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.account.group')) + + self.group.refresh_from_db() + + self.assertEqual(self.group.name, 'K-Fêt The Group') + self.assertQuerysetEqual( + self.group.permissions.all(), + map(repr, [ + self.perms['kfet.is_team'], + self.perms['kfet.manage_perms'], + ]), + ordered=False, + ) + + +class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.negative' + url_expected = '/k-fet/accounts/negatives' + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.view_negs']), + } + + def setUp(self): + super().setUp() + account = self.accounts['user'] + account.balance = -5 + account.save() + account.update_negative() + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['negatives'], + map(repr, [self.accounts['user'].negative]), + ordered=False, + ) + + +class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.stat.operation.list' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001/stat/operations/list' + + auth_user = 'user1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return {'user1': create_user('user1', '001')} + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + base_url = reverse('kfet.account.stat.operation', args=['001']) + + expected_stats = [{ + 'label': 'Derniers mois', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['month'], + 'types': ["['purchase']"], + 'scale_last': ['True'], + }, + }, + }, { + 'label': 'Dernières semaines', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['week'], + 'types': ["['purchase']"], + 'scale_last': ['True'], + }, + }, + }, { + 'label': 'Derniers jours', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['day'], + 'types': ["['purchase']"], + 'scale_last': ['True'], + }, + }, + }] + + for stat, expected in zip(content['stats'], expected_stats): + expected_url = expected.pop('url') + self.assertUrlsEqual(stat['url'], expected_url) + self.assertDictContainsSubset(expected, stat) + + +class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.stat.operation' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001/stat/operations' + + auth_user = 'user1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return {'user1': create_user('user1', '001')} + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.stat.balance.list' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001/stat/balance/list' + + auth_user = 'user1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return {'user1': create_user('user1', '001')} + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + base_url = reverse('kfet.account.stat.balance', args=['001']) + + expected_stats = [{ + 'label': 'Tout le temps', + 'url': base_url, + }, { + 'label': '1 an', + 'url': { + 'path': base_url, + 'query': {'last_days': ['365']}, + }, + }, { + 'label': '6 mois', + 'url': { + 'path': base_url, + 'query': {'last_days': ['183']}, + }, + }, { + 'label': '3 mois', + 'url': { + 'path': base_url, + 'query': {'last_days': ['90']}, + }, + }, { + 'label': '30 jours', + 'url': { + 'path': base_url, + 'query': {'last_days': ['30']}, + }, + }] + + for stat, expected in zip(content['stats'], expected_stats): + expected_url = expected.pop('url') + self.assertUrlsEqual(stat['url'], expected_url) + self.assertDictContainsSubset(expected, stat) + + +class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.stat.balance' + url_kwargs = {'trigramme': '001'} + url_expected = '/k-fet/accounts/001/stat/balance' + + auth_user = 'user1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return {'user1': create_user('user1', '001')} + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class CheckoutListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkout' + url_expected = '/k-fet/checkouts/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + self.checkout1 = Checkout.objects.create( + name='Checkout 1', + created_by=self.accounts['team'], + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + self.checkout2 = Checkout.objects.create( + name='Checkout 2', + created_by=self.accounts['team'], + valid_from=self.now + timedelta(days=10), + valid_to=self.now + timedelta(days=15), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['checkouts'], + map(repr, [self.checkout1, self.checkout2]), + ordered=False, + ) + + +class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkout.create' + url_expected = '/k-fet/checkouts/new' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + post_data = { + 'name': 'Checkout', + 'valid_from': '2017-10-08 17:45:00', + 'valid_to': '2017-11-08 16:00:00', + 'balance': '3.14', + # 'is_protected': not checked + } + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.add_checkout']), + } + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + + checkout = Checkout.objects.get(name='Checkout') + self.assertRedirects(r, checkout.get_absolute_url()) + + self.assertInstanceExpected(checkout, { + 'name': 'Checkout', + 'valid_from': timezone.make_aware(datetime(2017, 10, 8, 17, 45)), + 'valid_to': timezone.make_aware(datetime(2017, 11, 8, 16, 00)), + 'balance': Decimal('3.14'), + 'is_protected': False, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class CheckoutReadViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkout.read' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.checkout.pk} + + @property + def url_expected(self): + return '/k-fet/checkouts/{}'.format(self.checkout.pk) + + def setUp(self): + super().setUp() + self.checkout = Checkout.objects.create( + name='Checkout', + created_by=self.accounts['team'], + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['checkout'], self.checkout) + + +class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkout.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + post_data = { + 'name': 'Checkout updated', + 'valid_from': '2018-01-01 08:00:00', + 'valid_to': '2018-07-01 16:00:00', + } + + @property + def url_kwargs(self): + return {'pk': self.checkout.pk} + + @property + def url_expected(self): + return '/k-fet/checkouts/{}/edit'.format(self.checkout.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_checkout', + ]), + } + + def setUp(self): + super().setUp() + self.checkout = Checkout.objects.create( + name='Checkout', + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + balance='3.14', + is_protected=False, + created_by=self.accounts['team'], + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, self.checkout.get_absolute_url()) + + self.checkout.refresh_from_db() + + self.assertInstanceExpected(self.checkout, { + 'name': 'Checkout updated', + 'valid_from': timezone.make_aware(datetime(2018, 1, 1, 8, 0, 0)), + 'valid_to': timezone.make_aware(datetime(2018, 7, 1, 16, 0, 0)), + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkoutstatement' + url_expected = '/k-fet/checkouts/statements/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + self.checkout1 = Checkout.objects.create( + created_by=self.accounts['team'], + name='Checkout 1', + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + self.checkout2 = Checkout.objects.create( + created_by=self.accounts['team'], + name='Checkout 2', + valid_from=self.now + timedelta(days=10), + valid_to=self.now + timedelta(days=15), + ) + self.statement1 = CheckoutStatement.objects.create( + checkout=self.checkout1, + by=self.accounts['team'], + balance_old=5, + balance_new=0, + amount_taken=5, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + expected_statements = ( + list(self.checkout1.statements.all()) + + list(self.checkout2.statements.all()) + ) + + self.assertQuerysetEqual( + r.context['checkoutstatements'], + map(repr, expected_statements), + ) + + +class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkoutstatement.create' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + post_data = { + # Let + 'balance_001': 0, 'balance_002': 0, 'balance_005': 0, + 'balance_01': 0, 'balance_02': 0, 'balance_05': 0, + 'balance_1': 1, 'balance_2': 0, 'balance_5': 0, + 'balance_10': 1, 'balance_20': 0, 'balance_50': 0, + 'balance_100': 1, 'balance_200': 0, 'balance_500': 0, + # Taken + 'taken_001': 0, 'taken_002': 0, 'taken_005': 0, + 'taken_01': 0, 'taken_02': 0, 'taken_05': 0, + 'taken_1': 2, 'taken_2': 0, 'taken_5': 0, + 'taken_10': 2, 'taken_20': 0, 'taken_50': 0, + 'taken_100': 2, 'taken_200': 0, 'taken_500': 0, + 'taken_cheque': 0, + # 'not_count': not checked + } + + @property + def url_kwargs(self): + return {'pk_checkout': self.checkout.pk} + + @property + def url_expected(self): + return '/k-fet/checkouts/{}/statements/add'.format(self.checkout.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '001', perms=[ + 'kfet.add_checkoutstatement', + ]), + } + + def setUp(self): + super().setUp() + self.checkout = Checkout.objects.create( + name='Checkout', + created_by=self.accounts['team'], + balance=5, + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + @mock.patch('django.utils.timezone.now') + def test_post_ok(self, mock_now): + self.now += timedelta(days=2) + mock_now.return_value = self.now + + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, self.checkout.get_absolute_url()) + + statement = CheckoutStatement.objects.get(at=self.now) + + self.assertInstanceExpected(statement, { + 'by': self.accounts['team1'], + 'checkout': self.checkout, + 'balance_old': Decimal('5'), + 'balance_new': Decimal('111'), + 'amount_taken': Decimal('222'), + 'amount_error': Decimal('328'), + 'at': self.now, + 'not_count': False, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.checkoutstatement.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + post_data = { + 'amount_taken': 3, + 'amount_error': 2, + 'balance_old': 8, + 'balance_new': 5, + # Taken + 'taken_001': 0, 'taken_002': 0, 'taken_005': 0, + 'taken_01': 0, 'taken_02': 0, 'taken_05': 0, + 'taken_1': 1, 'taken_2': 1, 'taken_5': 0, + 'taken_10': 0, 'taken_20': 0, 'taken_50': 0, + 'taken_100': 0, 'taken_200': 0, 'taken_500': 0, + 'taken_cheque': 0, + } + + @property + def url_kwargs(self): + return { + 'pk_checkout': self.checkout.pk, + 'pk': self.statement.pk, + } + + @property + def url_expected(self): + return '/k-fet/checkouts/{pk_checkout}/statements/{pk}/edit'.format( + pk_checkout=self.checkout.pk, + pk=self.statement.pk, + ) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_checkoutstatement', + ]), + } + + def setUp(self): + super().setUp() + self.checkout = Checkout.objects.create( + name='Checkout', + created_by=self.accounts['team'], + balance=5, + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + self.statement = CheckoutStatement.objects.create( + by=self.accounts['team'], + checkout=self.checkout, + balance_new=5, + balance_old=8, + amount_error=2, + amount_taken=5, + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + @mock.patch('django.utils.timezone.now') + def test_post_ok(self, mock_now): + self.now += timedelta(days=2) + mock_now.return_value = self.now + + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, self.checkout.get_absolute_url()) + + self.statement.refresh_from_db() + + self.assertInstanceExpected(self.statement, { + 'taken_1': 1, + 'taken_2': 1, + 'balance_new': 5, + 'balance_old': 8, + 'amount_error': 0, + 'amount_taken': 3, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.category' + url_expected = '/k-fet/categories/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + self.category1 = ArticleCategory.objects.create(name='Category 1') + self.category2 = ArticleCategory.objects.create(name='Category 2') + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + self.assertQuerysetEqual( + r.context['categories'], + map(repr, [self.category1, self.category2]), + ) + + +class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.category.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.category.pk} + + @property + def url_expected(self): + return '/k-fet/categories/{}/edit'.format(self.category.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_articlecategory', + ]), + } + + @property + def post_data(self): + return { + 'name': 'The Category', + # 'has_addcost': not checked + } + + def setUp(self): + super().setUp() + self.category = ArticleCategory.objects.create(name='Category') + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.category')) + + self.category.refresh_from_db() + + self.assertInstanceExpected(self.category, { + 'name': 'The Category', + 'has_addcost': False, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class ArticleListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article' + url_expected = '/k-fet/articles/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + self.article1 = Article.objects.create( + name='Article 1', + category=category, + ) + self.article2 = Article.objects.create( + name='Article 2', + category=category, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertQuerysetEqual( + r.context['articles'], + map(repr, [self.article1, self.article2]), + ) + + +class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article.create' + url_expected = '/k-fet/articles/new' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.add_article']), + } + + @property + def post_data(self): + return { + 'name': 'Article', + 'category': self.category.pk, + 'stock': 5, + 'price': '2.5', + } + + def setUp(self): + super().setUp() + self.category = ArticleCategory.objects.create(name='Category') + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + + article = Article.objects.get(name='Article') + + self.assertRedirects(r, article.get_absolute_url()) + + self.assertInstanceExpected(article, { + 'name': 'Article', + 'category': self.category, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class ArticleReadViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article.read' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.article.pk} + + @property + def url_expected(self): + return '/k-fet/articles/{}'.format(self.article.pk) + + def setUp(self): + super().setUp() + self.article = Article.objects.create( + name='Article', + category=ArticleCategory.objects.create(name='Category'), + stock=5, + price=Decimal('2.5'), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.context['article'], self.article) + + +class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.article.pk} + + @property + def url_expected(self): + return '/k-fet/articles/{}/edit'.format(self.article.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_article', + ]), + } + + @property + def post_data(self): + return { + 'name': 'The Article', + 'category': self.article.category.pk, + 'is_sold': '1', + 'price': '3.5', + 'box_type': 'carton', + # 'hidden': not checked + } + + def setUp(self): + super().setUp() + self.category = ArticleCategory.objects.create(name='Category') + self.article = Article.objects.create( + name='Article', + category=self.category, + stock=5, + price=Decimal('2.5'), + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + + self.assertRedirects(r, self.article.get_absolute_url()) + + self.article.refresh_from_db() + + self.assertInstanceExpected(self.article, { + 'name': 'The Article', + 'price': Decimal('3.5'), + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article.stat.sales.list' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.article.pk} + + @property + def url_expected(self): + return '/k-fet/articles/{}/stat/sales/list'.format(self.article.pk) + + def setUp(self): + super().setUp() + self.article = Article.objects.create( + name='Article', + category=ArticleCategory.objects.create(name='Category'), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + base_url = reverse('kfet.article.stat.sales', args=[self.article.pk]) + + expected_stats = [ + { + 'label': 'Derniers mois', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['month'], + 'scale_last': ['True'], + }, + }, + }, + { + 'label': 'Dernières semaines', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['week'], + 'scale_last': ['True'], + }, + }, + }, + { + 'label': 'Derniers jours', + 'url': { + 'path': base_url, + 'query': { + 'scale_n_steps': ['7'], + 'scale_name': ['day'], + 'scale_last': ['True'], + }, + }, + }, + ] + + for stat, expected in zip(content['stats'], expected_stats): + expected_url = expected.pop('url') + self.assertUrlsEqual(stat['url'], expected_url) + self.assertDictContainsSubset(expected, stat) + + +class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.article.stat.sales' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.article.pk} + + @property + def url_expected(self): + return '/k-fet/articles/{}/stat/sales'.format(self.article.pk) + + def setUp(self): + super().setUp() + self.article = Article.objects.create( + name='Article', + category=ArticleCategory.objects.create(name='Category'), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class KPsulViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul' + url_expected = '/k-fet/k-psul/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.checkout_data' + url_expected = '/k-fet/k-psul/checkout_data' + + http_methods = ['POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + self.checkout = Checkout.objects.create( + name='Checkout', + balance=Decimal('10'), + created_by=self.accounts['team'], + valid_from=self.now, + valid_to=self.now + timedelta(days=5), + ) + + def test_ok(self): + r = self.client.post(self.url, {'pk': self.checkout.pk}) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + expected = { + 'name': 'Checkout', + 'balance': '10.00', + } + + self.assertDictContainsSubset(expected, content) + + self.assertSetEqual(set(content.keys()), set([ + 'balance', 'id', 'name', 'valid_from', 'valid_to', + 'last_statement_at', 'last_statement_balance', + 'last_statement_by_first_name', 'last_statement_by_last_name', + 'last_statement_by_trigramme', + ])) + + +class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.perform_operations' + url_expected = '/k-fet/k-psul/perform_operations' + + http_methods = ['POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + pass + + +class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.cancel_operations' + url_expected = '/k-fet/k-psul/cancel_operations' + + http_methods = ['POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + pass + + +class KPsulArticlesData(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.articles_data' + url_expected = '/k-fet/k-psul/articles_data' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Catégorie') + self.article1 = Article.objects.create( + category=category, + name='Article 1', + ) + self.article2 = Article.objects.create( + category=category, + name='Article 2', + price=Decimal('2.5'), + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + articles = content['articles'] + + expected_list = [{ + 'category__name': 'Catégorie', + 'name': 'Article 1', + 'price': '0.00', + }, { + 'category__name': 'Catégorie', + 'name': 'Article 2', + 'price': '2.50', + }] + + for expected, article in zip(expected_list, articles): + self.assertDictContainsSubset(expected, article) + self.assertSetEqual(set(article.keys()), set([ + 'id', 'name', 'price', 'stock', + 'category_id', 'category__name', 'category__has_addcost', + ])) + + +class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.update_addcost' + url_expected = '/k-fet/k-psul/update_addcost' + + http_methods = ['POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + post_data = { + 'trigramme': '000', + 'amount': '0.5', + } + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.manage_addcosts', + ]), + } + + def test_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertEqual(r.status_code, 200) + + self.assertEqual( + kfet_config.addcost_for, + Account.objects.get(trigramme='000'), + ) + self.assertEqual(kfet_config.addcost_amount, Decimal('0.5')) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbidden(r) + + +class KPsulGetSettings(ViewTestCaseMixin, TestCase): + url_name = 'kfet.kpsul.get_settings' + url_expected = '/k-fet/k-psul/get_settings' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.history.json' + url_expected = '/k-fet/history.json' + + auth_user = 'user' + auth_forbidden = [None] + + def test_ok(self): + r = self.client.post(self.url) + self.assertEqual(r.status_code, 200) + + +class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.account.read.json' + url_expected = '/k-fet/accounts/read.json' + + http_methods = ['POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.post(self.url, {'trigramme': '000'}) + self.assertEqual(r.status_code, 200) + + content = json.loads(r.content.decode('utf-8')) + + expected = { + 'name': 'first last', + 'trigramme': '000', + 'balance': '0.00', + } + self.assertDictContainsSubset(expected, content) + + self.assertSetEqual(set(content.keys()), set([ + 'balance', 'departement', 'email', 'id', 'is_cof', 'is_frozen', + 'name', 'nickname', 'promo', 'trigramme', + ])) + + +class SettingsListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.settings' + url_expected = '/k-fet/settings/' + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_settings', + ]), + } + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.settings.update' + url_expected = '/k-fet/settings/edit' + + http_methods = ['GET', 'POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def post_data(self): + return { + 'kfet_reduction_cof': '25', + 'kfet_addcost_amount': '0.5', + 'kfet_addcost_for': self.accounts['user'].pk, + 'kfet_overdraft_duration': '2 00:00:00', + 'kfet_overdraft_amount': '25', + 'kfet_cancel_duration': '00:20:00', + } + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_settings', + ]), + } + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertequal(r.status_code, 200) + + def test_post_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.settings')) + + self.assertDictEqual(dict(kfet_config.list()), { + 'reduction_cof': Decimal('25'), + 'addcost_amount': Decimal('0.5'), + 'addcost_for': self.accounts['user'], + 'overdraft_duration': timedelta(day=2), + 'overdraft_amount': Decimal('25'), + 'kfet_cancel_duration': timedelta(minutes=20), + }) + + +class TransferListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.transfers' + url_expected = '/k-fet/transfers/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class TransferCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.transfers.create' + url_expected = '/k-fet/transfers/new' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class TransferPerformViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.transfers.perform' + url_expected = '/k-fet/transfers/perform' + + http_methods = ['POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + # Required + 'kfet.add_transfer', + # Convenience + 'kfet.perform_negative_operations', + ]), + } + + @property + def post_data(self): + return { + # General + 'comment': '', + # Formset management + 'form-TOTAL_FORMS': '10', + 'form-INITIAL_FORMS': '0', + 'form-MIN_NUM_FORMS': '1', + 'form-MAX_NUM_FORMS': '1000', + # Transfer 1 + 'form-0-from_acc': str(self.accounts['user'].pk), + 'form-0-to_acc': str(self.accounts['team'].pk), + 'form-0-amount': '3.5', + # Transfer 2 + 'form-1-from_acc': str(self.accounts['team'].pk), + 'form-1-to_acc': str(self.accounts['team1'].pk), + 'form-1-amount': '2.4', + } + + def test_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertEqual(r.status_code, 200) + + user = self.accounts['user'] + user.refresh_from_db() + self.assertEqual(user.balance, Decimal('-3.5')) + + team = self.accounts['team'] + team.refresh_from_db() + self.assertEqual(team.balance, Decimal('1.1')) + + team1 = self.accounts['team1'] + team1.refresh_from_db() + self.assertEqual(team1.balance, Decimal('2.4')) + + +class TransferCancelViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.transfers.cancel' + url_expected = '/k-fet/transfers/cancel' + + http_methods = ['POST'] + + auth_user = 'team1' + auth_forbidden = [None, 'user', 'team'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + # Convenience + 'kfet.perform_negative_operations', + ]), + } + + @property + def post_data(self): + return { + 'transfers[]': [self.transfer1.pk, self.transfer2.pk], + } + + def setUp(self): + super().setUp() + group = TransferGroup.objects.create() + self.transfer1 = Transfer.objects.create( + group=group, + from_acc=self.accounts['user'], + to_acc=self.accounts['team'], + amount='3.5', + ) + self.transfer2 = Transfer.objects.create( + group=group, + from_acc=self.accounts['team'], + to_acc=self.accounts['root'], + amount='2.4', + ) + + def test_ok(self): + r = self.client.post(self.url, self.post_data) + self.assertEqual(r.status_code, 200) + + user = self.accounts['user'] + user.refresh_from_db() + self.assertEqual(user.balance, Decimal('3.5')) + + team = self.accounts['team'] + team.refresh_from_db() + self.assertEqual(team.balance, Decimal('-1.1')) + + root = self.accounts['root'] + root.refresh_from_db() + self.assertEqual(root.balance, Decimal('-2.4')) + + +class InventoryListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.inventory' + url_expected = '/k-fet/inventaires/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + self.inventory = Inventory.objects.create( + by=self.accounts['team'], + ) + category = ArticleCategory.objects.create(name='Category') + article = Article.objects.create( + name='Article', + category=category, + ) + InventoryArticle.objects.create( + inventory=self.inventory, + article=article, + stock_old=5, + stock_new=0, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + inventories = r.context['inventories'] + self.assertQuerysetEqual( + inventories, + map(repr, [self.inventory]), + ) + + +class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.inventory.create' + url_expected = '/k-fet/inventaires/new' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.add_inventory', + ]), + } + + @property + def post_data(self): + return { + # Formset management + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-MIN_NUM_FORMS': '0', + 'form-MAX_NUM_FORMS': '1000', + # Article 1 + 'form-0-article': str(self.article1.pk), + 'form-0-stock_new': '5', + # Article 2 + 'form-1-article': str(self.article2.pk), + 'form-1-stock_new': '10', + } + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + self.article1 = Article.objects.create( + category=category, + name='Article 1', + ) + self.article2 = Article.objects.create( + category=category, + name='Article 2', + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.inventory')) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class InventoryReadViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.inventory.read' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.inventory.pk} + + @property + def url_expected(self): + return '/k-fet/inventaires/{}'.format(self.inventory.pk) + + def setUp(self): + super().setUp() + self.inventory = Inventory.objects.create( + by=self.accounts['team'], + ) + category = ArticleCategory.objects.create(name='Category') + article = Article.objects.create( + name='Article', + category=category, + ) + InventoryArticle.objects.create( + inventory=self.inventory, + article=article, + stock_old=5, + stock_new=0, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class OrderListViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.order' + url_expected = '/k-fet/orders/' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + article = Article.objects.create(name='Article', category=category) + + supplier = Supplier.objects.create(name='Supplier') + SupplierArticle.objects.create(supplier=supplier, article=article) + + self.order = Order.objects.create(supplier=supplier) + OrderArticle.objects.create( + order=self.order, + article=article, + quantity_ordered=24, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + orders = r.context['orders'] + self.assertQuerysetEqual( + orders, + map(repr, [self.order]), + ) + + +class OrderReadViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.order.read' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.order.pk} + + @property + def url_expected(self): + return '/k-fet/orders/{}'.format(self.order.pk) + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + article = Article.objects.create(name='Article', category=category) + + supplier = Supplier.objects.create(name='Supplier') + SupplierArticle.objects.create(supplier=supplier, article=article) + + self.order = Order.objects.create(supplier=supplier) + OrderArticle.objects.create( + order=self.order, + article=article, + quantity_ordered=24, + ) + + def test_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + +class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.order.supplier.update' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.supplier.pk} + + @property + def url_expected(self): + return '/k-fet/orders/suppliers/{}/edit'.format(self.supplier.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.change_supplier', + ]), + } + + @property + def post_data(self): + return { + 'name': 'The Supplier', + 'phone': '', + 'comment': '', + 'address': '', + 'email': '', + } + + def setUp(self): + super().setUp() + self.supplier = Supplier.objects.create(name='Supplier') + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + def test_post_ok(self): + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.order')) + + self.supplier.refresh_from_db() + self.assertEqual(self.supplier.name, 'The Supplier') + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class OrderCreateViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.order.new' + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.supplier.pk} + + @property + def url_expected(self): + return '/k-fet/orders/suppliers/{}/new-order'.format(self.supplier.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=['kfet.add_order']), + } + + @property + def post_data(self): + return { + # Formset management + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '1', + 'form-MIN_NUM_FORMS': '0', + 'form-MAX_NUM_FORMS': '1000', + # Article + 'form-0-article': self.article.pk, + 'form-0-quantity_ordered': '20', + } + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + self.article = Article.objects.create( + name='Article', + category=category, + ) + + self.supplier = Supplier.objects.create(name='Supplier') + SupplierArticle.objects.create( + supplier=self.supplier, + article=self.article, + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + @mock.patch('django.utils.timezone.now') + def test_post_ok(self, mock_now): + mock_now.return_value = self.now + + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + + order = Order.objects.get(at=self.now) + + self.assertRedirects(r, reverse('kfet.order.read', args=[order.pk])) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) + + +class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase): + url_name = 'kfet.order.to_inventory' + + http_methods = ['GET', 'POST'] + + auth_user = 'team' + auth_forbidden = [None, 'user'] + + @property + def url_kwargs(self): + return {'pk': self.order.pk} + + @property + def url_expected(self): + return '/k-fet/orders/{}/to_inventory'.format(self.order.pk) + + @property + def users_extra(self): + return { + 'team1': create_team('team1', '101', perms=[ + 'kfet.order_to_inventory', + ]), + } + + @property + def post_data(self): + return { + # Formset mangaement + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '1', + 'form-MIN_NUM_FORMS': '0', + 'form-MAX_NUM_FORMS': '1000', + # Article 1 + 'form-0-article': self.article.pk, + 'form-0-quantity_received': '20', + 'form-0-price_HT': '', + 'form-0-TVA': '', + 'form-0-rights': '', + } + + def setUp(self): + super().setUp() + category = ArticleCategory.objects.create(name='Category') + self.article = Article.objects.create( + name='Article', + category=category, + ) + + supplier = Supplier.objects.create(name='Supplier') + SupplierArticle.objects.create(supplier=supplier, article=self.article) + + self.order = Order.objects.create(supplier=supplier) + OrderArticle.objects.create( + order=self.order, + article=self.article, + quantity_ordered=24, + ) + + def test_get_ok(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + @mock.patch('django.utils.timezone.now') + def test_post_ok(self, mock_now): + mock_now.return_value = self.now + + client = Client() + client.login(username='team1', password='team1') + + r = client.post(self.url, self.post_data) + self.assertRedirects(r, reverse('kfet.order')) + + inventory = Inventory.objects.first() + + self.assertInstanceExpected(inventory, { + 'by': self.accounts['team1'], + 'at': self.now, + 'order': self.order, + }) + self.assertQuerysetEqual( + inventory.articles.all(), + map(repr, [self.article]), + ) + + compte = InventoryArticle.objects.get(article=self.article) + + self.assertInstanceExpected(compte, { + 'stock_old': 0, + 'stock_new': 20, + 'stock_error': 0, + }) + + def test_post_forbidden(self): + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index c2e1b848..977345e7 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -1,16 +1,114 @@ from unittest import mock +from urllib.parse import parse_qs, urlparse from django.core.urlresolvers import reverse from django.http import QueryDict from django.test import Client +from django.utils import timezone from .utils import create_root, create_team, create_user -class ViewTestCaseMixin: +class TestCaseMixin: + def assertForbidden(self, response): + request = response.wsgi_request + + try: + try: + # Is this an HTTP Forbidden response ? + self.assertEqual(response.status_code, 403) + except AssertionError: + # A redirection to the login view is fine too. + + # Let's build the login url with the 'next' param on current + # page. + full_path = request.get_full_path() + + querystring = QueryDict(mutable=True) + querystring['next'] = full_path + + login_url = '/login?' + querystring.urlencode(safe='/') + + # We don't focus on what the login view does. + # So don't fetch the redirect. + self.assertRedirects( + response, login_url, + fetch_redirect_response=False, + ) + except AssertionError: + raise AssertionError( + "%(http_method)s request at %(path)s should be forbidden for " + "%(username)s user.\n" + "Response isn't 403, nor a redirect to login view. Instead, " + "response code is %(code)d." % { + 'http_method': request.method, + 'path': request.get_full_path(), + 'username': ( + "'{}'".format(request.user) + if request.user.is_authenticated() + else 'anonymous' + ), + 'code': response.status_code, + } + ) + + def assertForbiddenKfet(self, response, form_ctx='form'): + try: + self.assertEqual(response.status_code, 200) + try: + form = response.context[form_ctx] + self.assertIn("Permission refusée", form.non_field_errors()) + except (AssertionError, AttributeError, KeyError): + messages = [str(msg) for msg in response.context['messages']] + self.assertIn("Permission refusée", messages) + except AssertionError: + request = response.wsgi_request + raise AssertionError( + "%(http_method)s request at %(path)s should raise an error " + "for %(username)s user.\n" + "Cannot find any errors in non-field errors of form " + "'%(form_ctx)s', nor in messages." % { + 'http_method': request.method, + 'path': request.get_full_path(), + 'username': ( + "'%s'" % request.user + if request.user.is_authenticated() + else 'anonymous' + ), + 'form_ctx': form_ctx, + } + ) + + def assertInstanceExpected(self, instance, expected): + for attr, expected_value in expected.items(): + value = getattr(instance, attr) + if callable(value): + value = value() + self.assertEqual(value, expected_value) + + def assertUrlsEqual(self, actual, expected): + if type(expected) == dict: + parsed = urlparse(actual) + checks = ['scheme', 'netloc', 'path', 'params'] + for check in checks: + self.assertEqual( + getattr(parsed, check), + expected.get(check, ''), + ) + self.assertDictEqual( + parse_qs(parsed.query), + expected.get('query', {}), + ) + else: + self.assertEqual(actual, expected) + + +class ViewTestCaseMixin(TestCaseMixin): url_name = None url_expected = None + http_methods = ['GET'] + auth_user = None auth_forbidden = [] @@ -22,6 +120,11 @@ class ViewTestCaseMixin: patcher_messages.start() self.addCleanup(patcher_messages.stop) + # A test can mock 'django.utils.timezone.now' and give this as return + # value. E.g. it is useful if the test checks values of 'auto_now' or + # 'auto_now_add' fields. + self.now = timezone.now() + self.users = {} self.accounts = {} @@ -58,6 +161,11 @@ class ViewTestCaseMixin: if hasattr(user.profile, 'account_kfet'): self.accounts[label] = user.profile.account_kfet + def get_user(self, label): + if self.auth_user is not None: + return self.auth_user + return self.auth_user_mapping.get(label) + @property def urls_conf(self): return [{ @@ -81,62 +189,24 @@ class ViewTestCaseMixin: def url(self): return self.t_urls[0] - def assertForbidden(self, response): - request = response.wsgi_request - - try: - try: - # Is this an HTTP Forbidden response ? - self.assertEqual(response.status_code, 403) - except AssertionError: - # A redirection to the login view is fine too. - - # Let's build the login url with the 'next' param on current - # page. - full_path = request.get_full_path() - - querystring = QueryDict(mutable=True) - querystring['next'] = full_path - - login_url = '/login?' + querystring.urlencode(safe='/') - - # We don't focus on what the login view does. - # So don't fetch the redirect. - self.assertRedirects( - response, login_url, - fetch_redirect_response=False, - ) - except AssertionError: - raise AssertionError( - "%(http_method)s request at %(path)s should be forbidden for " - "%(username)s user.\n" - "Response isn't 403, nor a redirect to login view. Instead, " - "response code is %(code)d." % { - 'http_method': request.method, - 'path': request.get_full_path(), - 'username': ( - "'{}'".format(request.user.username) - if request.user.username - else 'anonymous' - ), - 'code': response.status_code, - } - ) - - def assertForbiddenKfet(self, response): - self.assertEqual(response.status_code, 200) - form = response.context['form'] - self.assertIn("Permission refusée", form.non_field_errors) - def test_urls(self): for url, conf in zip(self.t_urls, self.urls_conf): self.assertEqual(url, conf['expected']) def test_forbidden(self): - for creds in self.auth_forbidden: - for url in self.t_urls: - client = Client() - if creds is not None: - client.login(username=creds, password=creds) - r = client.get(url) - self.assertForbidden(r) + for method in self.http_methods: + for user in self.auth_forbidden: + for url in self.t_urls: + self.check_forbidden(method, url, user) + + def check_forbidden(self, method, url, user=None): + method = method.lower() + client = Client() + if user is not None: + client.login(username=user, password=user) + + send_request = getattr(client, method) + data = getattr(self, '{}_data'.format(method), {}) + + r = send_request(url, data) + self.assertForbidden(r) diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py index 4b739003..4681da67 100644 --- a/kfet/tests/utils.py +++ b/kfet/tests/utils.py @@ -7,44 +7,14 @@ from ..models import Account User = get_user_model() -def user_add_perms(user, perms_labels): - """ - Add perms to a user. - - Args: - user (User instance) - perms (list of str 'app.perm_name') - - Returns: - The same user (refetched from DB to avoid missing perms) - - """ - u_labels = set(perms_labels) - - perms = [] - for label in u_labels: - app_label, codename = label.split('.', 1) - perms.append( - Permission.objects.get( - content_type__app_label=app_label, - codename=codename, - ) - ) - - user.user_permissions.add(*perms) - - # If permissions have already been fetched for this user, we need to reload - # it to avoid using of the previous permissions cache. - # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching - return User.objects.get(pk=user.pk) - - def _create_user_and_account(user_attrs, account_attrs, perms=None): - user_attrs.setdefault('password', user_attrs['username']) - user = User.objects.create_user(**user_attrs) + user_pwd = user_attrs.pop('password', user_attrs['username']) + user = User.objects.create(**user_attrs) + user.set_password(user_pwd) + user.save() account_attrs['cofprofile'] = user.profile - kfet_pwd = account_attrs.pop('password', None) + kfet_pwd = account_attrs.pop('password', 'kfetpwd_{}'.format(user_pwd)) account = Account.objects.create(**account_attrs) @@ -52,8 +22,6 @@ def _create_user_and_account(user_attrs, account_attrs, perms=None): user = user_add_perms(user, perms) if 'kfet.is_team' in perms: - if kfet_pwd is None: - kfet_pwd = 'kfetpwd_{}'.format(user_attrs['password']) account.change_pwd(kfet_pwd) account.save() @@ -98,8 +66,41 @@ def create_root(username='root', trigramme='200', **kwargs): user_attrs.setdefault('first_name', 'super') user_attrs.setdefault('last_name', 'user') user_attrs.setdefault('email', 'mail@root.net') + user_attrs['is_superuser'] = user_attrs['is_staff'] = True account_attrs = kwargs.setdefault('account_attrs', {}) account_attrs.setdefault('trigramme', trigramme) return _create_user_and_account(**kwargs) + + +def get_perms(*labels): + perms = {} + for label in set(labels): + app_label, codename = label.split('.', 1) + perms[label] = Permission.objects.get( + content_type__app_label=app_label, + codename=codename, + ) + return perms + + +def user_add_perms(user, perms_labels): + """ + Add perms to a user. + + Args: + user (User instance) + perms (list of str 'app.perm_name') + + Returns: + The same user (refetched from DB to avoid missing perms) + + """ + perms = get_perms(*perms_labels) + user.user_permissions.add(*perms.values()) + + # If permissions have already been fetched for this user, we need to reload + # it to avoid using of the previous permissions cache. + # https://docs.djangoproject.com/en/1.11/topics/auth/default/#permission-caching + return User.objects.get(pk=user.pk) From 414b0eb433e86e6f758ef180537a04542fc28e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 16 Aug 2017 21:28:16 +0200 Subject: [PATCH 23/75] Add missing perms to view/edit kfet config --- kfet/migrations/0057_add_perms_config.py | 18 ++++++++++++++++++ kfet/models.py | 2 ++ kfet/urls.py | 8 ++------ kfet/views.py | 9 ++++++++- 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 kfet/migrations/0057_add_perms_config.py diff --git a/kfet/migrations/0057_add_perms_config.py b/kfet/migrations/0057_add_perms_config.py new file mode 100644 index 00000000..1300665f --- /dev/null +++ b/kfet/migrations/0057_add_perms_config.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0056_change_account_meta'), + ] + + operations = [ + migrations.AlterModelOptions( + name='account', + options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('can_force_close', 'Fermer manuellement la K-Fêt'), ('see_config', 'Voir la configuration K-Fêt'), ('change_config', 'Modifier la configuration K-Fêt'))}, + ), + ] diff --git a/kfet/models.py b/kfet/models.py index ec146ad9..8b209468 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -75,6 +75,8 @@ class Account(models.Model): ('special_add_account', "Créer un compte avec une balance initiale"), ('can_force_close', "Fermer manuellement la K-Fêt"), + ('see_config', "Voir la configuration K-Fêt"), + ('change_config', "Modifier la configuration K-Fêt"), ) def __str__(self): diff --git a/kfet/urls.py b/kfet/urls.py index c3499b18..17ded7b8 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -188,13 +188,9 @@ urlpatterns = [ # Settings urls # ----- - url(r'^settings/$', - permission_required('kfet.change_settings') - (views.SettingsList.as_view()), + url(r'^settings/$', views.config_list, name='kfet.settings'), - url(r'^settings/edit$', - permission_required('kfet.change_settings') - (views.SettingsUpdate.as_view()), + url(r'^settings/edit$', views.config_update, name='kfet.settings.update'), diff --git a/kfet/views.py b/kfet/views.py index 5e451c9c..ec772a05 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1452,6 +1452,9 @@ class SettingsList(TemplateView): template_name = 'kfet/settings.html' +config_list = permission_required('kfet.see_config')(SettingsList.as_view()) + + class SettingsUpdate(SuccessMessageMixin, FormView): form_class = KFetConfigForm template_name = 'kfet/settings_update.html' @@ -1460,13 +1463,17 @@ class SettingsUpdate(SuccessMessageMixin, FormView): def form_valid(self, form): # Checking permission - if not self.request.user.has_perm('kfet.change_settings'): + if not self.request.user.has_perm('kfet.change_config'): form.add_error(None, 'Permission refusée') return self.form_invalid(form) form.save() return super().form_valid(form) +config_update = ( + permission_required('kfet.change_config')(SettingsUpdate.as_view()) +) + # ----- # Transfer views From b4b15ab371dde565761511232c3311de758bc230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 16 Aug 2017 22:30:17 +0200 Subject: [PATCH 24/75] Tests of kfet config views pass --- kfet/tests/test_views.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 5ac82f33..1cd83ffd 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1653,7 +1653,7 @@ class SettingsListViewTests(ViewTestCaseMixin, TestCase): def users_extra(self): return { 'team1': create_team('team1', '101', perms=[ - 'kfet.change_settings', + 'kfet.see_config', ]), } @@ -1686,26 +1686,34 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): def users_extra(self): return { 'team1': create_team('team1', '101', perms=[ - 'kfet.change_settings', + 'kfet.change_config', ]), } def test_get_ok(self): r = self.client.get(self.url) - self.assertequal(r.status_code, 200) + self.assertEqual(r.status_code, 200) def test_post_ok(self): r = self.client.post(self.url, self.post_data) - self.assertRedirects(r, reverse('kfet.settings')) + # Redirect is skipped because client may lack permissions. + self.assertRedirects( + r, + reverse('kfet.settings'), + fetch_redirect_response=False, + ) - self.assertDictEqual(dict(kfet_config.list()), { + expected_config = { 'reduction_cof': Decimal('25'), 'addcost_amount': Decimal('0.5'), 'addcost_for': self.accounts['user'], - 'overdraft_duration': timedelta(day=2), + 'overdraft_duration': timedelta(days=2), 'overdraft_amount': Decimal('25'), - 'kfet_cancel_duration': timedelta(minutes=20), - }) + 'cancel_duration': timedelta(minutes=20), + } + + for key, expected in expected_config.items(): + self.assertEqual(getattr(kfet_config, key), expected) class TransferListViewTests(ViewTestCaseMixin, TestCase): From 22d8317dee76a603ed102f486d0956bf5bfbb896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 8 Aug 2017 19:03:42 +0200 Subject: [PATCH 25/75] Fix kfet.open.tests Due to messages sent in signals handlers, the tests were failing. --- kfet/open/tests.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/kfet/open/tests.py b/kfet/open/tests.py index 1d6d5529..476eb6c0 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -1,5 +1,6 @@ import json from datetime import timedelta +from unittest import mock from django.contrib.auth.models import AnonymousUser, Permission, User from django.test import Client @@ -118,6 +119,11 @@ class OpenKfetViewsTest(ChannelTestCase): """OpenKfet views unit-tests suite.""" def setUp(self): + # Need this (and here) because of '.login' in setUp + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + # get some permissions perms = { 'kfet.is_team': Permission.objects.get(codename='is_team'), @@ -194,7 +200,8 @@ class OpenKfetConsumerTest(ChannelTestCase): OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'}) self.assertIsNone(c.receive()) - def test_team_user(self): + @mock.patch('gestioncof.signals.messages') + def test_team_user(self, mock_messages): """Team user is added to kfet.open.team group.""" # setup team user and its client t = User.objects.create_user('team', '', 'team') @@ -224,6 +231,11 @@ class OpenKfetScenarioTest(ChannelTestCase): """OpenKfet functionnal tests suite.""" def setUp(self): + # Need this (and here) because of '.login' in setUp + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + # anonymous client (for views) self.c = Client() # anonymous client (for websockets) From b4338ce8dbaeb5291dd7c98f16ee4f69a0a323a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 16 Aug 2017 22:54:40 +0200 Subject: [PATCH 26/75] View 'search account' should be restricted. --- kfet/autocomplete.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 09057d4a..0a9bb42c 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -106,6 +106,7 @@ def account_create(request): return render(request, "kfet/account_create_autocomplete.html", data) +@teamkfet_required def account_search(request): if "q" not in request.GET: raise Http404 From be1e67626c0fb8a9341a38501c3337b2872ccad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 16 Aug 2017 23:04:22 +0200 Subject: [PATCH 27/75] Most data of suppliers should be optionnal. --- kfet/migrations/0058_amend_supplier.py | 39 ++++++++++++++++++++++++++ kfet/models.py | 20 +++++++------ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 kfet/migrations/0058_amend_supplier.py diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0058_amend_supplier.py new file mode 100644 index 00000000..0b45dade --- /dev/null +++ b/kfet/migrations/0058_amend_supplier.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0057_add_perms_config'), + ] + + operations = [ + migrations.AlterField( + model_name='supplier', + name='address', + field=models.TextField(verbose_name='adresse', blank=True), + ), + migrations.AlterField( + model_name='supplier', + name='articles', + field=models.ManyToManyField(verbose_name='articles vendus', through='kfet.SupplierArticle', related_name='suppliers', to='kfet.Article'), + ), + migrations.AlterField( + model_name='supplier', + name='comment', + field=models.TextField(verbose_name='commentaire', blank=True), + ), + migrations.AlterField( + model_name='supplier', + name='email', + field=models.EmailField(max_length=254, verbose_name='adresse mail', blank=True), + ), + migrations.AlterField( + model_name='supplier', + name='phone', + field=models.CharField(max_length=20, verbose_name='téléphone', blank=True), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 8b209468..fb0d8813 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -8,6 +8,7 @@ from gestioncof.models import CofProfile from django.utils.six.moves import reduce from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ from django.db import transaction from django.db.models import F from datetime import date @@ -524,21 +525,24 @@ class InventoryArticle(models.Model): self.stock_error = self.stock_new - self.stock_old super(InventoryArticle, self).save(*args, **kwargs) -@python_2_unicode_compatible + class Supplier(models.Model): articles = models.ManyToManyField( Article, - through = 'SupplierArticle', - related_name = "suppliers") - name = models.CharField("nom", max_length = 45) - address = models.TextField("adresse") - email = models.EmailField("adresse mail") - phone = models.CharField("téléphone", max_length = 10) - comment = models.TextField("commentaire") + verbose_name=_("articles vendus"), + through='SupplierArticle', + related_name='suppliers', + ) + name = models.CharField(_("nom"), max_length=45) + address = models.TextField(_("adresse"), blank=True) + email = models.EmailField(_("adresse mail"), blank=True) + phone = models.CharField(_("téléphone"), max_length=20, blank=True) + comment = models.TextField(_("commentaire"), blank=True) def __str__(self): return self.name + class SupplierArticle(models.Model): supplier = models.ForeignKey( Supplier, on_delete = models.PROTECT) From d8391e54a5a5abb5b52f9d3c6867ddaefae92880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 1 Sep 2017 12:39:17 +0200 Subject: [PATCH 28/75] Add docs to kfet TestCases --- kfet/tests/testcases.py | 132 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 10 deletions(-) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 977345e7..e2fc09ff 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -10,7 +10,18 @@ from .utils import create_root, create_team, create_user class TestCaseMixin: + """Extends TestCase for kfet application tests.""" + def assertForbidden(self, response): + """ + Test that the response (retrieved with a Client) is a denial of access. + + The response should verify one of the following: + - its HTTP response code is 403, + - it redirects to the login page with a GET parameter named 'next' + whose value is the url of the requested page. + + """ request = response.wsgi_request try: @@ -53,6 +64,18 @@ class TestCaseMixin: ) def assertForbiddenKfet(self, response, form_ctx='form'): + """ + Test that a response (retrieved with a Client) contains error due to + lack of kfet permissions. + + It checks that 'Permission refusée' is present in the non-field errors + of the form of response context at key 'form_ctx', or present in + messages. + + This should be used for pages which can be accessed by the kfet team + members, but require additionnal permission(s) to make an operation. + + """ try: self.assertEqual(response.status_code, 200) try: @@ -80,6 +103,10 @@ class TestCaseMixin: ) def assertInstanceExpected(self, instance, expected): + """ + Test that the values of the attributes and without-argument methods of + 'instance' are equal to 'expected' pairs. + """ for attr, expected_value in expected.items(): value = getattr(instance, attr) if callable(value): @@ -87,23 +114,104 @@ class TestCaseMixin: self.assertEqual(value, expected_value) def assertUrlsEqual(self, actual, expected): + """ + Test that the url 'actual' is as 'expected'. + + Arguments: + actual (str): Url to verify. + expected: Two forms are accepted. + * (str): Expected url. Strings equality is checked. + * (dict): Its keys must be attributes of 'urlparse(actual)'. + Equality is checked for each present key, except for + 'query' which must be a dict of the expected query string + parameters. + + """ if type(expected) == dict: parsed = urlparse(actual) - checks = ['scheme', 'netloc', 'path', 'params'] - for check in checks: - self.assertEqual( - getattr(parsed, check), - expected.get(check, ''), - ) - self.assertDictEqual( - parse_qs(parsed.query), - expected.get('query', {}), - ) + for part, expected_part in expected.items(): + if part == 'query': + self.assertDictEqual( + parse_qs(parsed.query), + expected.get('query', {}), + ) + else: + self.assertEqual(getattr(parsed, part), expected_part) else: self.assertEqual(actual, expected) class ViewTestCaseMixin(TestCaseMixin): + """ + TestCase extension to ease tests of kfet views. + + + Urls concerns + ------------- + + # Basic usage + + Attributes: + url_name (str): Name of view under test, as given to 'reverse' + function. + url_args (list, optional): Will be given to 'reverse' call. + url_kwargs (dict, optional): Same. + url_expcted (str): What 'reverse' should return given previous + attributes. + + View url can then be accessed at the 'url' attribute. + + # Advanced usage + + If multiple combinations of url name, args, kwargs can be used for a view, + it is possible to define 'urls_conf' attribute. It must be a list whose + each item is a dict defining arguments for 'reverse' call ('name', 'args', + 'kwargs' keys) and its expected result ('expected' key). + + The reversed urls can be accessed at the 't_urls' attribute. + + + Users concerns + -------------- + + During setup, three users are created with their kfet account: + - 'user': a basic user without any permission, account trigramme: 000, + - 'team': a user with kfet.is_team permission, account trigramme: 100, + - 'root': a superuser, account trigramme: 200. + Their password is their username. + + One can create additionnal users with 'users_extra' attribute, or prevent + these 3 users to be created with 'users_base' attribute. See these two + properties for further informations. + + By using 'register_user' method, these users can then be accessed at + 'users' attribute by their label. Similarly, their kfet account is + registered on 'accounts' attribute. + + A user label can be given to 'auth_user' attribute. The related user is + then authenticated on self.client during test setup. Its value defaults to + 'None', meaning no user is authenticated. + + + Automated tests + --------------- + + # Url reverse + + Based on url-related attributes/properties, the test 'test_urls' checks + that expected url is returned by 'reverse' (once with basic url usage and + each for advanced usage). + + # Forbidden responses + + The 'test_forbidden' test verifies that each user, from labels of + 'auth_forbidden' attribute, can't access the url(s), i.e. response should + be a 403, or a redirect to login view. + + Tested HTTP requests are given by 'http_methods' attribute. Additional data + can be given by defining an attribute '_data'. + + """ url_name = None url_expected = None @@ -113,6 +221,9 @@ class ViewTestCaseMixin(TestCaseMixin): auth_forbidden = [] def setUp(self): + """ + Warning: Do not forget to call super().setUp() in subclasses. + """ # Signals handlers on login/logout send messages. # Due to the way the Django' test Client performs login, this raise an # error. As workaround, we mock the Django' messages module. @@ -125,6 +236,7 @@ class ViewTestCaseMixin(TestCaseMixin): # 'auto_now_add' fields. self.now = timezone.now() + # These attributes register users and accounts instances. self.users = {} self.accounts = {} From 997b63d6b69828ee6c82b226b7891e877a1507ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 1 Sep 2017 13:35:32 +0200 Subject: [PATCH 29/75] More docs for kfet.tests.utils --- kfet/tests/utils.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/kfet/tests/utils.py b/kfet/tests/utils.py index 4681da67..30eb05ad 100644 --- a/kfet/tests/utils.py +++ b/kfet/tests/utils.py @@ -8,6 +8,21 @@ User = get_user_model() def _create_user_and_account(user_attrs, account_attrs, perms=None): + """ + Create a user and its account, and assign permissions to this user. + + Arguments + user_attrs (dict): User data (first name, last name, password...). + account_attrs (dict): Account data (department, kfet password...). + perms (list of str: 'app.perm'): These permissions will be assigned to + the created user. No permission are assigned by default. + + If 'password' is not given in 'user_attrs', username is used as password. + + If 'kfet.is_team' is in 'perms' and 'password' is not in 'account_attrs', + the account password is 'kfetpwd_'. + + """ user_pwd = user_attrs.pop('password', user_attrs['username']) user = User.objects.create(**user_attrs) user.set_password(user_pwd) @@ -29,6 +44,27 @@ def _create_user_and_account(user_attrs, account_attrs, perms=None): def create_user(username='user', trigramme='000', **kwargs): + """ + Create a user without any permission and its kfet account. + + username and trigramme are accepted as arguments (defaults to 'user' and + '000'). + + user_attrs, account_attrs and perms can be given as keyword arguments to + customize the user and its kfet account. + + # Default values + + User + * username: user + * password: user + * first_name: first + * last_name: last + * email: mail@user.net + Account + * trigramme: 000 + + """ user_attrs = kwargs.setdefault('user_attrs', {}) user_attrs.setdefault('username', username) @@ -43,6 +79,28 @@ def create_user(username='user', trigramme='000', **kwargs): def create_team(username='team', trigramme='100', **kwargs): + """ + Create a user, member of the kfet team, and its kfet account. + + username and trigramme are accepted as arguments (defaults to 'team' and + '100'). + + user_attrs, account_attrs and perms can be given as keyword arguments to + customize the user and its kfet account. + + # Default values + + User + * username: team + * password: team + * first_name: team + * last_name: member + * email: mail@team.net + Account + * trigramme: 100 + * kfet password: kfetpwd_team + + """ user_attrs = kwargs.setdefault('user_attrs', {}) user_attrs.setdefault('username', username) @@ -60,6 +118,29 @@ def create_team(username='team', trigramme='100', **kwargs): def create_root(username='root', trigramme='200', **kwargs): + """ + Create a superuser and its kfet account. + + username and trigramme are accepted as arguments (defaults to 'root' and + '200'). + + user_attrs, account_attrs and perms can be given as keyword arguments to + customize the user and its kfet account. + + # Default values + + User + * username: root + * password: root + * first_name: super + * last_name: user + * email: mail@root.net + * is_staff, is_superuser: True + Account + * trigramme: 200 + * kfet password: kfetpwd_root + + """ user_attrs = kwargs.setdefault('user_attrs', {}) user_attrs.setdefault('username', username) @@ -75,6 +156,7 @@ def create_root(username='root', trigramme='200', **kwargs): def get_perms(*labels): + """Return Permission instances from a list of '.'.""" perms = {} for label in set(labels): app_label, codename = label.split('.', 1) From af97c0cda606ab112646ae850bc1b8c356ce6057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 1 Sep 2017 16:37:14 +0200 Subject: [PATCH 30/75] Improve users management on kfet TestCase, and Py34 compat --- kfet/tests/test_views.py | 81 ++++++++++++++-------------------------- kfet/tests/testcases.py | 45 ++++++++++++++++++---- 2 files changed, 64 insertions(+), 62 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 1cd83ffd..ff9803c9 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -93,8 +93,7 @@ class AccountCreateViewTests(ViewTestCaseMixin, TestCase): 'email': 'email@domain.net', } - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.add_account']), } @@ -212,8 +211,7 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team' auth_forbidden = [None, 'user'] - @property - def users_extra(self): + def get_users_extra(self): return { 'user1': create_user('user1', '001'), } @@ -292,8 +290,7 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): 'pwd2': '', } - @property - def users_extra(self): + def get_users_extra(self): return { 'user1': create_user('user1', '001'), 'team1': create_team('team1', '101', perms=[ @@ -359,8 +356,7 @@ class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), } @@ -390,8 +386,7 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), } @@ -449,8 +444,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/accounts/groups/{}/edit'.format(self.group.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.manage_perms']), } @@ -502,8 +496,7 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.view_negs']), } @@ -533,8 +526,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): auth_user = 'user1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return {'user1': create_user('user1', '001')} def test_ok(self): @@ -594,8 +586,7 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): auth_user = 'user1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return {'user1': create_user('user1', '001')} def test_ok(self): @@ -611,8 +602,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): auth_user = 'user1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return {'user1': create_user('user1', '001')} def test_ok(self): @@ -666,8 +656,7 @@ class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): auth_user = 'user1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return {'user1': create_user('user1', '001')} def test_ok(self): @@ -724,8 +713,7 @@ class CheckoutCreateViewTests(ViewTestCaseMixin, TestCase): # 'is_protected': not checked } - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.add_checkout']), } @@ -807,8 +795,7 @@ class CheckoutUpdateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/checkouts/{}/edit'.format(self.checkout.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_checkout', @@ -927,8 +914,7 @@ class CheckoutStatementCreateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/checkouts/{}/statements/add'.format(self.checkout.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '001', perms=[ 'kfet.add_checkoutstatement', @@ -1014,8 +1000,7 @@ class CheckoutStatementUpdateViewTests(ViewTestCaseMixin, TestCase): pk=self.statement.pk, ) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_checkoutstatement', @@ -1109,8 +1094,7 @@ class ArticleCategoryUpdateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/categories/{}/edit'.format(self.category.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_articlecategory', @@ -1188,8 +1172,7 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team' auth_forbidden = [None, 'user'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.add_article']), } @@ -1276,8 +1259,7 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/articles/{}/edit'.format(self.article.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_article', @@ -1564,8 +1546,7 @@ class KPsulUpdateAddcost(ViewTestCaseMixin, TestCase): 'amount': '0.5', } - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.manage_addcosts', @@ -1649,8 +1630,7 @@ class SettingsListViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.see_config', @@ -1682,8 +1662,7 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): 'kfet_cancel_duration': '00:20:00', } - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_config', @@ -1749,8 +1728,7 @@ class TransferPerformViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ # Required @@ -1806,8 +1784,7 @@ class TransferCancelViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team1' auth_forbidden = [None, 'user', 'team'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ # Convenience @@ -1898,8 +1875,7 @@ class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): auth_user = 'team' auth_forbidden = [None, 'user'] - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.add_inventory', @@ -2069,8 +2045,7 @@ class SupplierUpdateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/orders/suppliers/{}/edit'.format(self.supplier.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.change_supplier', @@ -2124,8 +2099,7 @@ class OrderCreateViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/orders/suppliers/{}/new-order'.format(self.supplier.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=['kfet.add_order']), } @@ -2195,8 +2169,7 @@ class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase): def url_expected(self): return '/k-fet/orders/{}/to_inventory'.format(self.order.pk) - @property - def users_extra(self): + def get_users_extra(self): return { 'team1': create_team('team1', '101', perms=[ 'kfet.order_to_inventory', diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index e2fc09ff..d7d7eac5 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse from django.http import QueryDict from django.test import Client from django.utils import timezone +from django.utils.functional import cached_property from .utils import create_root, create_team, create_user @@ -180,9 +181,9 @@ class ViewTestCaseMixin(TestCaseMixin): - 'root': a superuser, account trigramme: 200. Their password is their username. - One can create additionnal users with 'users_extra' attribute, or prevent - these 3 users to be created with 'users_base' attribute. See these two - properties for further informations. + One can create additionnal users with 'get_users_extra' method, or prevent + these 3 users to be created with 'get_users_base' method. See these two + methods for further informations. By using 'register_user' method, these users can then be accessed at 'users' attribute by their label. Similarly, their kfet account is @@ -240,7 +241,7 @@ class ViewTestCaseMixin(TestCaseMixin): self.users = {} self.accounts = {} - for label, user in {**self.users_base, **self.users_extra}.items(): + for label, user in dict(self.users_base, **self.users_extra).items(): self.register_user(label, user) if self.auth_user: @@ -252,8 +253,20 @@ class ViewTestCaseMixin(TestCaseMixin): ) ) - @property - def users_base(self): + def tearDown(self): + del self.users_base + del self.users_extra + + def get_users_base(self): + """ + Dict of . + + Note: Don't access yourself this property. Use 'users_base' attribute + which cache the returned value from here. + It allows to give functions calls, which creates users instances, as + values here. + + """ # Format desc: username, password, trigramme return { # user, user, 000 @@ -264,10 +277,26 @@ class ViewTestCaseMixin(TestCaseMixin): 'root': create_root(), } - @property - def users_extra(self): + @cached_property + def users_base(self): + return self.get_users_base() + + def get_users_extra(self): + """ + Dict of . + + Note: Don't access yourself this property. Use 'users_base' attribute + which cache the returned value from here. + It allows to give functions calls, which create users instances, as + values here. + + """ return {} + @cached_property + def users_extra(self): + return self.get_users_extra() + def register_user(self, label, user): self.users[label] = user if hasattr(user.profile, 'account_kfet'): From fb5ba5fb1b6bd08825a5f248e7ddc2cf19aba3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 3 Sep 2017 14:42:38 +0200 Subject: [PATCH 31/75] Fix kfet navbar on small devices --- kfet/open/static/kfetopen/kfet-open.css | 25 ++++++++++++++++++++++--- kfet/open/static/kfetopen/kfet-open.js | 4 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/kfet/open/static/kfetopen/kfet-open.css b/kfet/open/static/kfetopen/kfet-open.css index d44318cd..a7068626 100644 --- a/kfet/open/static/kfetopen/kfet-open.css +++ b/kfet/open/static/kfetopen/kfet-open.css @@ -14,10 +14,16 @@ .kfetopen .base { height: 50px; - padding: 15px; + max-width: 16px; - display: inline-flex; + margin-left: 5px; + margin-right: 5px; + + display: flex; + flex-wrap: wrap; + align-content: center; align-items: center; + justify-content: center; } .kfetopen .details { @@ -34,10 +40,23 @@ height: 10px; border-radius: 50%; transition: background 0.15s; + margin: 3px; } .kfetopen .warning { - margin-left: 15px; + display: none; +} + +@media (min-width: 576px) { + .kfetopen .base { + max-width: none; + margin-left: 15px; + margin-right: 15px; + } + + .kfetopen .warning { + margin-left: 15px; + } } .kfetopen .status-text { diff --git a/kfet/open/static/kfetopen/kfet-open.js b/kfet/open/static/kfetopen/kfet-open.js index b86cc5bc..74f18d8a 100644 --- a/kfet/open/static/kfetopen/kfet-open.js +++ b/kfet/open/static/kfetopen/kfet-open.js @@ -74,10 +74,10 @@ OpenKfet.prototype = { if (this.admin) { this.add_class(this.admin_status); if (this.force_close) { - this.dom.warning.addClass('in'); + this.dom.warning.show().addClass('in'); this.dom.force_close_btn.html(this.force_text['deactivate']); } else { - this.dom.warning.removeClass('in'); + this.dom.warning.removeClass('in').hide(); this.dom.force_close_btn.html(this.force_text['activate']); } } From 51f4bf3fb5e3709f77df4de9beb951361ed6ec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 4 Sep 2017 13:25:09 +0200 Subject: [PATCH 32/75] Clipper logins may be > 8 characters --- gestioncof/forms.py | 4 ---- gestioncof/migrations/0011_longer_clippers.py | 19 +++++++++++++++++++ gestioncof/models.py | 4 +++- gestioncof/views.py | 7 ++----- kfet/forms.py | 12 +++--------- 5 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 gestioncof/migrations/0011_longer_clippers.py diff --git a/gestioncof/forms.py b/gestioncof/forms.py index 3a519a39..7e36fb92 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -10,7 +10,6 @@ from django.contrib.auth.models import User from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.forms.formsets import BaseFormSet, formset_factory from django.db.models import Max -from django.core.validators import MinLengthValidator from gestioncof.models import CofProfile, EventCommentValue, \ CalendarSubscription, Club @@ -203,9 +202,6 @@ class RegistrationUserForm(forms.ModelForm): super(RegistrationUserForm, self).__init__(*args, **kw) self.fields['username'].help_text = "" - def force_long_username(self): - self.fields['username'].validators = [MinLengthValidator(9)] - class Meta: model = User fields = ("username", "first_name", "last_name", "email") diff --git a/gestioncof/migrations/0011_longer_clippers.py b/gestioncof/migrations/0011_longer_clippers.py new file mode 100644 index 00000000..631d0ea8 --- /dev/null +++ b/gestioncof/migrations/0011_longer_clippers.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gestioncof', '0010_delete_custommail'), + ] + + operations = [ + migrations.AlterField( + model_name='cofprofile', + name='login_clipper', + field=models.CharField(verbose_name='Login clipper', blank=True, max_length=32), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 2215b296..7a8a2577 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -38,7 +38,9 @@ TYPE_COMMENT_FIELD = ( @python_2_unicode_compatible class CofProfile(models.Model): user = models.OneToOneField(User, related_name="profile") - login_clipper = models.CharField("Login clipper", max_length=8, blank=True) + login_clipper = models.CharField( + "Login clipper", max_length=32, blank=True + ) is_cof = models.BooleanField("Membre du COF", default=False) num = models.IntegerField("Numéro d'adhérent", blank=True, default=0) phone = models.CharField("Téléphone", max_length=20, blank=True) diff --git a/gestioncof/views.py b/gestioncof/views.py index 944d9dc2..b53a9e08 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -384,7 +384,6 @@ def registration_form2(request, login_clipper=None, username=None, elif not login_clipper: # new user user_form = RegistrationPassUserForm() - user_form.force_long_username() profile_form = RegistrationProfileForm() event_formset = EventFormset(events=events, prefix='events') clubs_form = ClubsForm() @@ -427,12 +426,10 @@ def registration(request): user_form = RegistrationUserForm(request_dict, instance=member) if member.profile.login_clipper: login_clipper = member.profile.login_clipper - else: - user_form.force_long_username() except User.DoesNotExist: - user_form.force_long_username() + pass else: - user_form.force_long_username() + pass # ----- # Validation des formulaires diff --git a/kfet/forms.py b/kfet/forms.py index 7acd0880..e4afaa09 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -3,7 +3,6 @@ from decimal import Decimal from django import forms from django.core.exceptions import ValidationError -from django.core.validators import MinLengthValidator from django.contrib.auth.models import User, Group, Permission from django.contrib.contenttypes.models import ContentType from django.forms import modelformset_factory @@ -105,21 +104,16 @@ class CofRestrictForm(CofForm): class Meta(CofForm.Meta): fields = ['departement'] -class UserForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - from_clipper = kwargs.pop('from_clipper', False) - new_user = kwargs.get('instance') is None and not from_clipper - super(UserForm, self).__init__(*args, **kwargs) - if new_user: - self.fields['username'].validators = [MinLengthValidator(9)] +class UserForm(forms.ModelForm): class Meta: - model = User + model = User fields = ['username', 'first_name', 'last_name', 'email'] help_texts = { 'username': '' } + class UserRestrictForm(UserForm): class Meta(UserForm.Meta): fields = ['first_name', 'last_name'] From 50432d969f6bd73df4d594061e019821b463ab30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 4 Sep 2017 13:25:45 +0200 Subject: [PATCH 33/75] Update available promos for account creation --- kfet/migrations/0054_update_promos.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 kfet/migrations/0054_update_promos.py diff --git a/kfet/migrations/0054_update_promos.py b/kfet/migrations/0054_update_promos.py new file mode 100644 index 00000000..2691e903 --- /dev/null +++ b/kfet/migrations/0054_update_promos.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0053_created_at'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='promo', + field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017)], default=2017, null=True), + ), + ] From 35b352ac1d83113ad5e06131b906279e26529eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 4 Sep 2017 14:50:12 +0200 Subject: [PATCH 34/75] Fix mistake introduced in 51f4bf3fb5e370 --- kfet/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index de6c906e..1ae31283 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -282,10 +282,10 @@ def get_account_create_forms(request=None, username=None, login_clipper=None, # Form créations if request: - user_form = UserForm(request.POST, initial=user_initial, from_clipper=True) + user_form = UserForm(request.POST, initial=user_initial) cof_form = CofForm(request.POST, initial=cof_initial) else: - user_form = UserForm(initial=user_initial, from_clipper=True) + user_form = UserForm(initial=user_initial) cof_form = CofForm(initial=cof_initial) # Protection (read-only) des champs username et login_clipper From 439f49c3ba1959f590b334536fe04faaac077c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 5 Sep 2017 15:21:19 +0200 Subject: [PATCH 35/75] =?UTF-8?q?We=20=E2=99=A5=20hardcoding=20stuff?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/urls.py | 2 +- gestioncof/views.py | 64 +++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 9a562e7e..57c2e8f2 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -10,7 +10,7 @@ export_patterns = [ url(r'^mega/avecremarques$', views.export_mega_remarksonly), url(r'^mega/participants$', views.export_mega_participants), url(r'^mega/orgas$', views.export_mega_orgas), - url(r'^mega/(?P.+)$', views.export_mega_bytype), + # url(r'^mega/(?P.+)$', views.export_mega_bytype), url(r'^mega$', views.export_mega), ] diff --git a/gestioncof/views.py b/gestioncof/views.py index b53a9e08..a29ffb88 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -595,13 +595,13 @@ def csv_export_mega(filename, qs): @buro_required def export_mega_remarksonly(request): - filename = 'remarques_mega_2016.csv' + filename = 'remarques_mega_2017.csv' response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename=' + filename writer = unicodecsv.writer(response) - event = Event.objects.get(title="Mega 2016") - commentfield = event.commentfields.get(name="Commentaires") + event = Event.objects.get(title="MEGA 2017") + commentfield = event.commentfields.get(name="Commentaire") for val in commentfield.values.all(): reg = val.registration user = reg.user @@ -613,50 +613,52 @@ def export_mega_remarksonly(request): return response -@buro_required -def export_mega_bytype(request, type): - types = {"orga-actif": "Orga élève", - "orga-branleur": "Orga étudiant", - "conscrit-eleve": "Conscrit élève", - "conscrit-etudiant": "Conscrit étudiant"} - - if type not in types: - raise Http404 - - event = Event.objects.get(title="Mega 2016") - type_option = event.options.get(name="Type") - participant_type = type_option.choices.get(value=types[type]).id - qs = EventRegistration.objects.filter(event=event).filter( - options__id__exact=participant_type) - return csv_export_mega(type + '_mega_2016.csv', qs) +# @buro_required +# def export_mega_bytype(request, type): +# types = {"orga-actif": "Orga élève", +# "orga-branleur": "Orga étudiant", +# "conscrit-eleve": "Conscrit élève", +# "conscrit-etudiant": "Conscrit étudiant"} +# +# if type not in types: +# raise Http404 +# +# event = Event.objects.get(title="MEGA 2017") +# type_option = event.options.get(name="Type") +# participant_type = type_option.choices.get(value=types[type]).id +# qs = EventRegistration.objects.filter(event=event).filter( +# options__id__exact=participant_type) +# return csv_export_mega(type + '_mega_2017.csv', qs) @buro_required def export_mega_orgas(request): - event = Event.objects.get(title="Mega 2016") - type_option = event.options.get(name="Conscrit ou orga ?") - participant_type = type_option.choices.get(value="Vieux").id - qs = EventRegistration.objects.filter(event=event).exclude( - options__id=participant_type) - return csv_export_mega('orgas_mega_2016.csv', qs) + event = Event.objects.get(title="MEGA 2017") + type_option = event.options.get(name="Conscrit/Orga ?") + participant_type = type_option.choices.get(value="Orga").id + qs = EventRegistration.objects.filter(event=event).filter( + options__id=participant_type + ) + return csv_export_mega('orgas_mega_2017.csv', qs) @buro_required def export_mega_participants(request): - event = Event.objects.get(title="Mega 2016") - type_option = event.options.get(name="Conscrit ou orga ?") + event = Event.objects.get(title="MEGA 2017") + type_option = event.options.get(name="Conscrit/Orga ?") participant_type = type_option.choices.get(value="Conscrit").id qs = EventRegistration.objects.filter(event=event).filter( - options__id=participant_type) - return csv_export_mega('participants_mega_2016.csv', qs) + options__id=participant_type + ) + return csv_export_mega('participants_mega_2017.csv', qs) @buro_required def export_mega(request): - event = Event.objects.filter(title="Mega 2016") + event = Event.objects.filter(title="MEGA 2017") qs = EventRegistration.objects.filter(event=event) \ .order_by("user__username") - return csv_export_mega('all_mega_2016.csv', qs) + return csv_export_mega('all_mega_2017.csv', qs) @buro_required From faed7bff73081519ed5781710827f82a9c6346bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 10 Sep 2017 02:32:21 +0200 Subject: [PATCH 36/75] =?UTF-8?q?fix=20=3Fnext=3D=E2=80=A6=20on=20K-F?= =?UTF-8?q?=C3=AAt=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/templates/kfet/base_nav.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index ba582bcf..abcb8e18 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -94,7 +94,7 @@ {% endif %}
  • - + Déconnexion
  • @@ -103,7 +103,7 @@ {% endif %} {% if user.is_authenticated and not perms.kfet.is_team %}
  • - +
  • From 9e6188786871db4cb8ab42bebe5a7a7a886e3bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 11 Sep 2017 16:42:14 +0200 Subject: [PATCH 37/75] =?UTF-8?q?K-F=C3=AAt'=20groups=20edits=20don't=20re?= =?UTF-8?q?move=20non-kfet=20app=20permissions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #168. --- kfet/forms.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/kfet/forms.py b/kfet/forms.py index 2d07581d..6ef3aefb 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -167,10 +167,22 @@ class GroupForm(forms.ModelForm): name = self.cleaned_data['name'] return 'K-Fêt %s' % name + def clean_permissions(self): + kfet_perms = self.cleaned_data['permissions'] + # TODO: With Django >=1.11, the QuerySet method 'difference' can be used. + # other_groups = self.instance.permissions.difference( + # self.fields['permissions'].queryset + # ) + other_perms = self.instance.permissions.exclude( + pk__in=[p.pk for p in self.fields['permissions'].queryset], + ) + return list(kfet_perms) + list(other_perms) + class Meta: - model = Group + model = Group fields = ['name', 'permissions'] + class AccountNegativeForm(forms.ModelForm): class Meta: model = AccountNegative From 368ee3190fac23f8dfaa1c66625e428e5c232d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 12 Sep 2017 00:14:52 +0200 Subject: [PATCH 38/75] Update CI: use postgres --- .gitlab-ci.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2635b7b..5080ef32 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,24 +1,20 @@ services: - - mysql:latest + - postgres:latest - redis:latest variables: # GestioCOF settings - DJANGO_SETTINGS_MODULE: "cof.settings_dev" - DBNAME: "cof_gestion" - DBUSER: "cof_gestion" - DBPASSWD: "cof_password" - DBHOST: "mysql" + DJANGO_SETTINGS_MODULE: "cof.settings.prod" + DBHOST: "postgres" REDIS_HOST: "redis" # Cached packages PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" - # mysql service configuration - MYSQL_DATABASE: "$DBNAME" - MYSQL_USER: "$DBUSER" - MYSQL_PASSWORD: "$DBPASSWD" - MYSQL_ROOT_PASSWORD: "root_password" + # postgres service configuration + POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" + POSTGRES_USER: "cof_gestion" + POSTGRES_DB: "cof_gestion" cache: @@ -29,13 +25,12 @@ cache: before_script: - mkdir -p vendor/{python,pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client - - mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST" - -e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'" + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py # Remove the old test database if it has not been done yet - - mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST" + - psql --username=cof_gestion --password="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" --host="$DBHOST" -e "DROP DATABASE test_$DBNAME" || true - - pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt + - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt test: stage: test From 1921f05eba5a5a05ff08c7ee75189d4cb6a556af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 12 Sep 2017 09:22:41 +0200 Subject: [PATCH 39/75] Move STATIC_ROOT in production --- cof/settings/prod.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cof/settings/prod.py b/cof/settings/prod.py index 286b5547..2ffdf02f 100644 --- a/cof/settings/prod.py +++ b/cof/settings/prod.py @@ -21,6 +21,7 @@ ALLOWED_HOSTS = [ STATIC_ROOT = os.path.join( os.path.dirname(os.path.dirname(BASE_DIR)), "public", + "gestion", "static", ) From bf61e41b50eac097093d424f58252ed618168da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 13 Sep 2017 01:57:31 +0200 Subject: [PATCH 40/75] Move auth-related from 'kfet' app to 'kfet.auth'. --- cof/settings/common.py | 7 +- kfet/auth/__init__.py | 1 + kfet/auth/apps.py | 8 +++ kfet/{ => auth}/backends.py | 0 kfet/auth/context_processors.py | 10 +++ kfet/auth/fields.py | 20 ++++++ kfet/auth/forms.py | 44 ++++++++++++ kfet/{ => auth}/middleware.py | 2 +- kfet/auth/migrations/0001_initial.py | 21 ++++++ kfet/auth/migrations/__init__.py | 0 kfet/auth/models.py | 5 ++ .../templates/kfet/login_genericteam.html | 0 kfet/{tests/test_forms.py => auth/tests.py} | 0 kfet/auth/views.py | 69 +++++++++++++++++++ kfet/context_processors.py | 11 --- kfet/forms.py | 61 ++-------------- .../0058_delete_genericteamtoken.py | 17 +++++ kfet/models.py | 6 +- kfet/views.py | 65 ++--------------- 19 files changed, 213 insertions(+), 134 deletions(-) create mode 100644 kfet/auth/__init__.py create mode 100644 kfet/auth/apps.py rename kfet/{ => auth}/backends.py (100%) create mode 100644 kfet/auth/context_processors.py create mode 100644 kfet/auth/fields.py create mode 100644 kfet/auth/forms.py rename kfet/{ => auth}/middleware.py (95%) create mode 100644 kfet/auth/migrations/0001_initial.py create mode 100644 kfet/auth/migrations/__init__.py create mode 100644 kfet/auth/models.py rename kfet/{ => auth}/templates/kfet/login_genericteam.html (100%) rename kfet/{tests/test_forms.py => auth/tests.py} (100%) create mode 100644 kfet/auth/views.py create mode 100644 kfet/migrations/0058_delete_genericteamtoken.py diff --git a/cof/settings/common.py b/cof/settings/common.py index ba0b6044..92759d21 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -90,6 +90,7 @@ INSTALLED_APPS = [ 'wagtailmenus', 'modelcluster', 'taggit', + 'kfet.auth', 'kfet.cms', ] @@ -99,7 +100,7 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'kfet.middleware.KFetAuthenticationMiddleware', + 'kfet.auth.middleware.KFetAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -127,7 +128,7 @@ TEMPLATES = [ 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', - 'kfet.context_processors.auth', + 'kfet.auth.context_processors.auth', 'kfet.context_processors.config', ], }, @@ -190,7 +191,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'gestioncof.shared.COFCASBackend', - 'kfet.backends.GenericTeamBackend', + 'kfet.auth.backends.GenericTeamBackend', ) RECAPTCHA_USE_SSL = True diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py new file mode 100644 index 00000000..63392684 --- /dev/null +++ b/kfet/auth/__init__.py @@ -0,0 +1 @@ +default_app_config = 'kfet.auth.apps.KFetAuthConfig' diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py new file mode 100644 index 00000000..ab791d18 --- /dev/null +++ b/kfet/auth/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class KFetAuthConfig(AppConfig): + name = 'kfet.auth' + label = 'kfetauth' + verbose_name = _("K-Fêt - Authentification et Autorisation") diff --git a/kfet/backends.py b/kfet/auth/backends.py similarity index 100% rename from kfet/backends.py rename to kfet/auth/backends.py diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py new file mode 100644 index 00000000..07c9537f --- /dev/null +++ b/kfet/auth/context_processors.py @@ -0,0 +1,10 @@ +from django.contrib.auth.context_processors import PermWrapper + + +def auth(request): + if hasattr(request, 'real_user'): + return { + 'user': request.real_user, + 'perms': PermWrapper(request.real_user), + } + return {} diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py new file mode 100644 index 00000000..28ba1c9e --- /dev/null +++ b/kfet/auth/fields.py @@ -0,0 +1,20 @@ +from django import forms +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.forms import widgets + + +class KFetPermissionsField(forms.ModelMultipleChoiceField): + + def __init__(self, *args, **kwargs): + queryset = Permission.objects.filter( + content_type__in=ContentType.objects.filter(app_label="kfet"), + ) + super().__init__( + queryset=queryset, + widget=widgets.CheckboxSelectMultiple, + *args, **kwargs + ) + + def label_from_instance(self, obj): + return obj.name diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py new file mode 100644 index 00000000..0c9fa53b --- /dev/null +++ b/kfet/auth/forms.py @@ -0,0 +1,44 @@ +from django import forms +from django.contrib.auth.models import Group, User + +from .fields import KFetPermissionsField + + +class GroupForm(forms.ModelForm): + permissions = KFetPermissionsField() + + def clean_name(self): + name = self.cleaned_data['name'] + return 'K-Fêt %s' % name + + def clean_permissions(self): + kfet_perms = self.cleaned_data['permissions'] + # TODO: With Django >=1.11, the QuerySet method 'difference' can be + # used. + # other_groups = self.instance.permissions.difference( + # self.fields['permissions'].queryset + # ) + other_perms = self.instance.permissions.exclude( + pk__in=[p.pk for p in self.fields['permissions'].queryset], + ) + return list(kfet_perms) + list(other_perms) + + class Meta: + model = Group + fields = ['name', 'permissions'] + + +class UserGroupForm(forms.ModelForm): + groups = forms.ModelMultipleChoiceField( + Group.objects.filter(name__icontains='K-Fêt'), + label='Statut équipe', + required=False) + + def clean_groups(self): + kfet_groups = self.cleaned_data.get('groups') + other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') + return list(kfet_groups) + list(other_groups) + + class Meta: + model = User + fields = ['groups'] diff --git a/kfet/middleware.py b/kfet/auth/middleware.py similarity index 95% rename from kfet/middleware.py rename to kfet/auth/middleware.py index 9502d393..1a930c3b 100644 --- a/kfet/middleware.py +++ b/kfet/auth/middleware.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User -from kfet.backends import KFetBackend +from .backends import KFetBackend class KFetAuthenticationMiddleware(object): diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py new file mode 100644 index 00000000..30dfca70 --- /dev/null +++ b/kfet/auth/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0006_require_contenttypes_0002'), + ] + + operations = [ + migrations.CreateModel( + name='GenericTeamToken', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), + ('token', models.CharField(unique=True, max_length=50)), + ], + ), + ] diff --git a/kfet/auth/migrations/__init__.py b/kfet/auth/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kfet/auth/models.py b/kfet/auth/models.py new file mode 100644 index 00000000..53aef6c9 --- /dev/null +++ b/kfet/auth/models.py @@ -0,0 +1,5 @@ +from django.db import models + + +class GenericTeamToken(models.Model): + token = models.CharField(max_length=50, unique=True) diff --git a/kfet/templates/kfet/login_genericteam.html b/kfet/auth/templates/kfet/login_genericteam.html similarity index 100% rename from kfet/templates/kfet/login_genericteam.html rename to kfet/auth/templates/kfet/login_genericteam.html diff --git a/kfet/tests/test_forms.py b/kfet/auth/tests.py similarity index 100% rename from kfet/tests/test_forms.py rename to kfet/auth/tests.py diff --git a/kfet/auth/views.py b/kfet/auth/views.py new file mode 100644 index 00000000..ce44b007 --- /dev/null +++ b/kfet/auth/views.py @@ -0,0 +1,69 @@ +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib.auth import authenticate, login +from django.contrib.auth.decorators import permission_required +from django.contrib.auth.models import Group, User +from django.core.urlresolvers import reverse_lazy +from django.db.models import Prefetch +from django.shortcuts import render +from django.utils.crypto import get_random_string +from django.views.generic.edit import CreateView, UpdateView + +from django_cas_ng.views import logout as cas_logout_view + +from kfet.decorators import teamkfet_required + +from .forms import GroupForm +from .models import GenericTeamToken + + +@teamkfet_required +def login_genericteam(request): + # Check si besoin de déconnecter l'utilisateur de CAS + cas_logout = None + if request.user.profile.login_clipper: + # Récupèration de la vue de déconnexion de CAS + # Ici, car request sera modifié après + next_page = request.META.get('HTTP_REFERER', None) + cas_logout = cas_logout_view(request, next_page=next_page) + + # Authentification du compte générique + token = GenericTeamToken.objects.create(token=get_random_string(50)) + user = authenticate(username="kfet_genericteam", token=token.token) + login(request, user) + + messages.success(request, "Connecté en utilisateur partagé") + + return cas_logout or render(request, "kfet/login_genericteam.html") + + +@permission_required('kfet.manage_perms') +def account_group(request): + user_pre = Prefetch( + 'user_set', + queryset=User.objects.select_related('profile__account_kfet'), + ) + groups = ( + Group.objects + .filter(name__icontains='K-Fêt') + .prefetch_related('permissions', user_pre) + ) + return render(request, 'kfet/account_group.html', { + 'groups': groups, + }) + + +class AccountGroupCreate(SuccessMessageMixin, CreateView): + model = Group + template_name = 'kfet/account_group_form.html' + form_class = GroupForm + success_message = 'Nouveau groupe : %(name)s' + success_url = reverse_lazy('kfet.account.group') + + +class AccountGroupUpdate(SuccessMessageMixin, UpdateView): + queryset = Group.objects.filter(name__icontains='K-Fêt') + template_name = 'kfet/account_group_form.html' + form_class = GroupForm + success_message = 'Groupe modifié : %(name)s' + success_url = reverse_lazy('kfet.account.group') diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 4c7b4fe4..04feec81 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -1,18 +1,7 @@ # -*- coding: utf-8 -*- -from django.contrib.auth.context_processors import PermWrapper - from kfet.config import kfet_config -def auth(request): - if hasattr(request, 'real_user'): - return { - 'user': request.real_user, - 'perms': PermWrapper(request.real_user), - } - return {} - - def config(request): return {'kfet_config': kfet_config} diff --git a/kfet/forms.py b/kfet/forms.py index 6ef3aefb..66638e6c 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -5,9 +5,8 @@ from decimal import Decimal from django import forms from django.core.exceptions import ValidationError -from django.contrib.auth.models import User, Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.forms import modelformset_factory, widgets +from django.contrib.auth.models import User +from django.forms import modelformset_factory from django.utils import timezone from djconfig.forms import ConfigForm @@ -18,6 +17,8 @@ from kfet.models import ( TransferGroup, Supplier) from gestioncof.models import CofProfile +from .auth.forms import UserGroupForm # noqa + # ----- # Widgets @@ -128,60 +129,6 @@ class UserRestrictTeamForm(UserForm): fields = ['first_name', 'last_name', 'email'] -class UserGroupForm(forms.ModelForm): - groups = forms.ModelMultipleChoiceField( - Group.objects.filter(name__icontains='K-Fêt'), - label='Statut équipe', - required=False) - - def clean_groups(self): - kfet_groups = self.cleaned_data.get('groups') - other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') - return list(kfet_groups) + list(other_groups) - - class Meta: - model = User - fields = ['groups'] - - -class KFetPermissionsField(forms.ModelMultipleChoiceField): - - def __init__(self, *args, **kwargs): - queryset = Permission.objects.filter( - content_type__in=ContentType.objects.filter(app_label="kfet"), - ) - super().__init__( - queryset=queryset, - widget=widgets.CheckboxSelectMultiple, - *args, **kwargs - ) - - def label_from_instance(self, obj): - return obj.name - - -class GroupForm(forms.ModelForm): - permissions = KFetPermissionsField() - - def clean_name(self): - name = self.cleaned_data['name'] - return 'K-Fêt %s' % name - - def clean_permissions(self): - kfet_perms = self.cleaned_data['permissions'] - # TODO: With Django >=1.11, the QuerySet method 'difference' can be used. - # other_groups = self.instance.permissions.difference( - # self.fields['permissions'].queryset - # ) - other_perms = self.instance.permissions.exclude( - pk__in=[p.pk for p in self.fields['permissions'].queryset], - ) - return list(kfet_perms) + list(other_perms) - - class Meta: - model = Group - fields = ['name', 'permissions'] - class AccountNegativeForm(forms.ModelForm): class Meta: diff --git a/kfet/migrations/0058_delete_genericteamtoken.py b/kfet/migrations/0058_delete_genericteamtoken.py new file mode 100644 index 00000000..ea8b55cd --- /dev/null +++ b/kfet/migrations/0058_delete_genericteamtoken.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0057_merge'), + ] + + operations = [ + migrations.DeleteModel( + name='GenericTeamToken', + ), + ] diff --git a/kfet/models.py b/kfet/models.py index ec146ad9..b06114d7 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -14,6 +14,8 @@ from datetime import date import re import hashlib +from .auth.models import GenericTeamToken # noqa + from .config import kfet_config from .utils import to_ukf @@ -710,7 +712,3 @@ class Operation(models.Model): return templates[self.type].format(nb=self.article_nb, article=self.article, amount=self.amount) - - -class GenericTeamToken(models.Model): - token = models.CharField(max_length = 50, unique = True) diff --git a/kfet/views.py b/kfet/views.py index c7eb677b..386eddb6 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -12,34 +12,30 @@ from django.views.generic.edit import CreateView, UpdateView from django.core.urlresolvers import reverse, reverse_lazy from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth import authenticate, login -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User, Permission, Group +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User, Permission from django.http import JsonResponse, Http404 from django.forms import formset_factory from django.db import transaction from django.db.models import F, Sum, Prefetch, Count from django.db.models.functions import Coalesce from django.utils import timezone -from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator -from django_cas_ng.views import logout as cas_logout_view - from gestioncof.models import CofProfile from kfet.config import kfet_config from kfet.decorators import teamkfet_required from kfet.models import ( Account, Checkout, Article, AccountNegative, - CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, + CheckoutStatement, Supplier, SupplierArticle, Inventory, InventoryArticle, Order, OrderArticle, Operation, OperationGroup, TransferGroup, Transfer, ArticleCategory) from kfet.forms import ( AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm, UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm, AccountPwdForm, AccountNegativeForm, UserRestrictForm, AccountRestrictForm, - GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, + CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm, KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, KPsulOperationFormSet, AddcostForm, FilterHistoryForm, @@ -54,25 +50,9 @@ import heapq import statistics from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale - -@teamkfet_required -def login_genericteam(request): - # Check si besoin de déconnecter l'utilisateur de CAS - cas_logout = None - if request.user.profile.login_clipper: - # Récupèration de la vue de déconnexion de CAS - # Ici, car request sera modifié après - next_page = request.META.get('HTTP_REFERER', None) - cas_logout = cas_logout_view(request, next_page=next_page) - - # Authentification du compte générique - token = GenericTeamToken.objects.create(token=get_random_string(50)) - user = authenticate(username="kfet_genericteam", token=token.token) - login(request, user) - - messages.success(request, "Connecté en utilisateur partagé") - - return cas_logout or render(request, "kfet/login_genericteam.html") +from .auth.views import ( # noqa + account_group, login_genericteam, AccountGroupCreate, AccountGroupUpdate, +) def put_cleaned_data_in_dict(dict, form): @@ -505,37 +485,6 @@ def account_update(request, trigramme): }) -@permission_required('kfet.manage_perms') -def account_group(request): - user_pre = Prefetch( - 'user_set', - queryset=User.objects.select_related('profile__account_kfet'), - ) - groups = ( - Group.objects - .filter(name__icontains='K-Fêt') - .prefetch_related('permissions', user_pre) - ) - return render(request, 'kfet/account_group.html', { - 'groups': groups, - }) - - -class AccountGroupCreate(SuccessMessageMixin, CreateView): - model = Group - template_name = 'kfet/account_group_form.html' - form_class = GroupForm - success_message = 'Nouveau groupe : %(name)s' - success_url = reverse_lazy('kfet.account.group') - -class AccountGroupUpdate(SuccessMessageMixin, UpdateView): - queryset = Group.objects.filter(name__icontains='K-Fêt') - template_name = 'kfet/account_group_form.html' - form_class = GroupForm - success_message = 'Groupe modifié : %(name)s' - success_url = reverse_lazy('kfet.account.group') - - class AccountNegativeList(ListView): queryset = ( AccountNegative.objects From 4091185a684a6d01d15f55fa452f3fa39d8ba8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 20 Sep 2017 18:19:15 +0200 Subject: [PATCH 41/75] import LDAP_SERVER_URL in settings --- cof/settings/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index ba0b6044..799ecc52 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -45,6 +45,7 @@ RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") +LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") BASE_DIR = os.path.dirname( From d89ba1efe5fd7878bf4883481e5bbf270f6ecb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 20 Sep 2017 18:21:59 +0200 Subject: [PATCH 42/75] Fix catalogue behaviour if id=0 --- bda/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bda/views.py b/bda/views.py index d6da2e9f..84b6c9d3 100644 --- a/bda/views.py +++ b/bda/views.py @@ -782,9 +782,9 @@ def catalogue(request, request_type): .select_related('location') .prefetch_related('quote_set') ) - if categories_id: + if categories_id and 0 not in categories_id: shows_qs = shows_qs.filter(category__id__in=categories_id) - if locations_id: + if locations_id and 0 not in locations_id: shows_qs = shows_qs.filter(location__id__in=locations_id) # On convertit les descriptions à envoyer en une liste facilement From 6f2652c4858b3bc2e20093422388659c38bde02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 20 Sep 2017 18:23:36 +0200 Subject: [PATCH 43/75] Prod quick hack for Mega export --- gestioncof/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index c5701510..ec9f6efd 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -577,7 +577,7 @@ def export_members(request): writer = unicodecsv.writer(response) for profile in CofProfile.objects.filter(is_cof=True).all(): user = profile.user - bits = [profile.id, user.username, user.first_name, user.last_name, + bits = [user.id, user.username, user.first_name, user.last_name, user.email, profile.phone, profile.occupation, profile.departement, profile.type_cotiz] writer.writerow([str(bit) for bit in bits]) @@ -596,7 +596,7 @@ def csv_export_mega(filename, qs): comments = "---".join( [comment.content for comment in reg.comments.all()]) bits = [user.username, user.first_name, user.last_name, user.email, - profile.phone, profile.id, + profile.phone, user.id, profile.comments if profile.comments else "", comments] writer.writerow([str(bit) for bit in bits]) From 1d19d1797c0e3949890c317964eae367e3bb4dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 21 Sep 2017 23:39:27 +0200 Subject: [PATCH 44/75] Clean setup/retrieve of kfet generic account --- kfet/auth/__init__.py | 3 ++ kfet/auth/apps.py | 5 +++ kfet/auth/backends.py | 20 ++++-------- kfet/auth/migrations/0001_initial.py | 3 ++ kfet/auth/tests.py | 16 +++++++++ kfet/auth/utils.py | 28 ++++++++++++++++ kfet/migrations/0059_create_generic.py | 45 ++++++++++++++++++++++++++ kfet/models.py | 7 ++++ 8 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 kfet/auth/utils.py create mode 100644 kfet/migrations/0059_create_generic.py diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py index 63392684..00926030 100644 --- a/kfet/auth/__init__.py +++ b/kfet/auth/__init__.py @@ -1 +1,4 @@ default_app_config = 'kfet.auth.apps.KFetAuthConfig' + +KFET_GENERIC_USERNAME = 'kfet_genericteam' +KFET_GENERIC_TRIGRAMME = 'GNR' diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index ab791d18..03742843 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.db.models.signals import post_migrate from django.utils.translation import ugettext_lazy as _ @@ -6,3 +7,7 @@ class KFetAuthConfig(AppConfig): name = 'kfet.auth' label = 'kfetauth' verbose_name = _("K-Fêt - Authentification et Autorisation") + + def ready(self): + from .utils import setup_kfet_generic_user + post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index fb9538d0..c972fb55 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -2,10 +2,13 @@ import hashlib -from django.contrib.auth.models import User, Permission -from gestioncof.models import CofProfile +from django.contrib.auth import get_user_model from kfet.models import Account, GenericTeamToken +from .utils import get_kfet_generic_user + +User = get_user_model() + class KFetBackend(object): def authenticate(self, request): @@ -29,18 +32,7 @@ class GenericTeamBackend(object): def authenticate(self, username=None, token=None): valid_token = GenericTeamToken.objects.get(token=token) if username == 'kfet_genericteam' and valid_token: - # Création du user s'il n'existe pas déjà - user, _ = User.objects.get_or_create(username='kfet_genericteam') - profile, _ = CofProfile.objects.get_or_create(user=user) - account, _ = Account.objects.get_or_create( - cofprofile=profile, - trigramme='GNR') - - # Ajoute la permission kfet.is_team à ce user - perm_is_team = Permission.objects.get(codename='is_team') - user.user_permissions.add(perm_is_team) - - return user + return get_kfet_generic_user() return None def get_user(self, user_id): diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py index 30dfca70..061570a8 100644 --- a/kfet/auth/migrations/0001_initial.py +++ b/kfet/auth/migrations/0001_initial.py @@ -8,6 +8,9 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0006_require_contenttypes_0002'), + # Following dependency allows using Account model to set up the kfet + # generic user in post_migrate receiver. + ('kfet', '0058_delete_genericteamtoken'), ] operations = [ diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 7f129a3f..47cc2376 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -4,6 +4,10 @@ from django.test import TestCase from django.contrib.auth.models import User, Group from kfet.forms import UserGroupForm +from kfet.models import Account + +from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME +from .utils import get_kfet_generic_user class UserGroupFormTests(TestCase): @@ -54,3 +58,15 @@ class UserGroupFormTests(TestCase): [repr(g) for g in [self.other_group] + self.kfet_groups], ordered=False, ) + + +class KFetGenericUserTests(TestCase): + + def test_exists(self): + """ + The account is set up when app is ready, so it should exist. + """ + generic = Account.objects.get_generic() + self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME) + self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME) + self.assertEqual(get_kfet_generic_user(), generic.user) diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py new file mode 100644 index 00000000..78f31028 --- /dev/null +++ b/kfet/auth/utils.py @@ -0,0 +1,28 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission + +from kfet.models import Account + +User = get_user_model() + + +def get_kfet_generic_user(): + """ + Return the user related to the kfet generic account. + """ + return Account.objects.get_generic().user + + +def setup_kfet_generic_user(**kwargs): + """ + First steps of setup of the kfet generic user are done in a migration, as + it is more robust against database schema changes. + Following steps cannot be done from migration. + """ + generic = get_kfet_generic_user() + generic.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', + codename='is_team', + ) + ) diff --git a/kfet/migrations/0059_create_generic.py b/kfet/migrations/0059_create_generic.py new file mode 100644 index 00000000..4f04770c --- /dev/null +++ b/kfet/migrations/0059_create_generic.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + +from kfet.auth import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME + + +def setup_kfet_generic_user(apps, schema_editor): + """ + Setup models instances for the kfet generic account. + + Username and trigramme are retrieved from kfet.auth.__init__ module. + Other data are registered here. + + See also setup_kfet_generic_user from kfet.auth.utils module. + """ + User = apps.get_model('auth', 'User') + CofProfile = apps.get_model('gestioncof', 'CofProfile') + Account = apps.get_model('kfet', 'Account') + + user, _ = User.objects.update_or_create( + username=KFET_GENERIC_USERNAME, + defaults={ + 'first_name': 'Compte générique K-Fêt', + }, + ) + profile, _ = CofProfile.objects.update_or_create(user=user) + account, _ = Account.objects.update_or_create( + cofprofile=profile, + defaults={ + 'trigramme': KFET_GENERIC_TRIGRAMME, + }, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0058_delete_genericteamtoken'), + ] + + operations = [ + migrations.RunPython(setup_kfet_generic_user), + ] diff --git a/kfet/models.py b/kfet/models.py index b06114d7..9aefb782 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -14,6 +14,7 @@ from datetime import date import re import hashlib +from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa from .config import kfet_config @@ -35,6 +36,12 @@ class AccountManager(models.Manager): return super().get_queryset().select_related('cofprofile__user', 'negative') + def get_generic(self): + """ + Get the kfet generic account instance. + """ + return self.get(trigramme=KFET_GENERIC_TRIGRAMME) + class Account(models.Model): objects = AccountManager() From e5d19811e859ba366fe595bbbf345b97df83bb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 22 Sep 2017 23:31:46 +0200 Subject: [PATCH 45/75] Clean code related to kfet password --- kfet/auth/backends.py | 10 +--------- kfet/auth/utils.py | 6 ++++++ kfet/models.py | 19 ++++++++++++++----- kfet/tests/test_models.py | 25 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 kfet/tests/test_models.py diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index c972fb55..1c9290d6 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- - -import hashlib - from django.contrib.auth import get_user_model from kfet.models import Account, GenericTeamToken @@ -18,12 +15,7 @@ class KFetBackend(object): return None try: - password_sha256 = ( - hashlib.sha256(password.encode('utf-8')) - .hexdigest() - ) - account = Account.objects.get(password=password_sha256) - return account.cofprofile.user + return Account.objects.get_by_password(password).user except Account.DoesNotExist: return None diff --git a/kfet/auth/utils.py b/kfet/auth/utils.py index 78f31028..0edc555d 100644 --- a/kfet/auth/utils.py +++ b/kfet/auth/utils.py @@ -1,3 +1,5 @@ +import hashlib + from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission @@ -26,3 +28,7 @@ def setup_kfet_generic_user(**kwargs): codename='is_team', ) ) + + +def hash_password(password): + return hashlib.sha256(password.encode('utf-8')).hexdigest() diff --git a/kfet/models.py b/kfet/models.py index 9aefb782..e547d248 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -12,7 +12,6 @@ from django.db import transaction from django.db.models import F from datetime import date import re -import hashlib from .auth import KFET_GENERIC_TRIGRAMME from .auth.models import GenericTeamToken # noqa @@ -42,6 +41,17 @@ class AccountManager(models.Manager): """ return self.get(trigramme=KFET_GENERIC_TRIGRAMME) + def get_by_password(self, password): + """ + Get a kfet generic account by clear password. + + Raises Account.DoesNotExist if no Account has this password. + """ + from .auth.utils import hash_password + if password is None: + raise self.model.DoesNotExist + return self.get(password=hash_password(password)) + class Account(models.Model): objects = AccountManager() @@ -245,10 +255,9 @@ class Account(models.Model): self.cofprofile = cof super(Account, self).save(*args, **kwargs) - def change_pwd(self, pwd): - pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\ - .hexdigest() - self.password = pwd_sha256 + def change_pwd(self, clear_password): + from .auth.utils import hash_password + self.password = hash_password(clear_password) # Surcharge de delete # Pas de suppression possible diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py new file mode 100644 index 00000000..ea132acd --- /dev/null +++ b/kfet/tests/test_models.py @@ -0,0 +1,25 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from kfet.models import Account + +User = get_user_model() + + +class AccountTests(TestCase): + + def setUp(self): + self.account = Account(trigramme='000') + self.account.save({'username': 'user'}) + + def test_password(self): + self.account.change_pwd('anna') + self.account.save() + + self.assertEqual(Account.objects.get_by_password('anna'), self.account) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password(None) + + with self.assertRaises(Account.DoesNotExist): + Account.objects.get_by_password('bernard') From 3fa7754ff4d31e300e9241d8a2122340a3a8de89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 23 Sep 2017 20:48:28 +0200 Subject: [PATCH 46/75] KFet Backends inherit from BaseKFetBackend Users who authenticate via a KFetBackend got extra select related. It should save 2 db queries on each request for these users. --- kfet/auth/backends.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index 1c9290d6..b2f1cb03 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -7,7 +7,22 @@ from .utils import get_kfet_generic_user User = get_user_model() -class KFetBackend(object): +class BaseKFetBackend: + def get_user(self, user_id): + """ + Add extra select related up to Account. + """ + try: + return ( + User.objects + .select_related('profile__account_kfet') + .get(pk=user_id) + ) + except User.DoesNotExist: + return None + + +class KFetBackend(BaseKFetBackend): def authenticate(self, request): password = request.POST.get('KFETPASSWORD', '') password = request.META.get('HTTP_KFETPASSWORD', password) @@ -20,7 +35,7 @@ class KFetBackend(object): return None -class GenericTeamBackend(object): +class GenericTeamBackend(BaseKFetBackend): def authenticate(self, username=None, token=None): valid_token = GenericTeamToken.objects.get(token=token) if username == 'kfet_genericteam' and valid_token: From db512a97f6a8086c7d25d45e0d5fbca8eebe1d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 25 Sep 2017 14:22:46 +0200 Subject: [PATCH 47/75] In /admin: displays "given" when it's relevant --- bda/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/admin.py b/bda/admin.py index 0cc66d43..83c89ea5 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -61,12 +61,12 @@ class AttributionInline(admin.TabularInline): class WithListingAttributionInline(AttributionInline): + exclude = ('given', ) form = WithListingAttributionTabularAdminForm listing = True class WithoutListingAttributionInline(AttributionInline): - exclude = ('given', ) form = WithoutListingAttributionTabularAdminForm listing = False From b42452080f36d2a669b19c5c93ea9194a2703afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 25 Sep 2017 17:16:19 +0200 Subject: [PATCH 48/75] Mass cleaning of kfet' authentication machinery AccountBackend - Should now work if used in AUTHENTICATION_BACKENDS settings. - It does not retieve itself the password, as it should not be used this way. GenericBackend - Delete useless 'username' arg of its 'authenticate()' method. - Now delete the token in DB. TemporaryAuthMiddleware - New name of the middleware is more meaningful. - Is now responsible to retrieve the password from the request, instead of the AccountBackend. GenericTeamToken model - Add a manager' method to create token, avoiding possible error due to unicity constraint. GenericLoginView (authentication with the kfet generic user) - Replace obscure system with a 100% HTTP handling. - See comments for more information. Misc - More docstrings! - More tests! - Add some i18n. - Add kfet/confirm_form.html template: Ask user to confirm sth via a form (which will send a POST request). Context variables: * title: the page title * confirm_url: action attribute for
    * text: displayed confirmation text - kfet.js : Add functions allowing to emit POST request from tag. - Non-link nav items from kfet navbar also get a 'title'. - A utility has been found for the 'sunglasses' glyphicon! --- cof/settings/common.py | 6 +- kfet/apps.py | 1 - kfet/auth/apps.py | 1 + kfet/auth/backends.py | 36 +-- kfet/auth/context_processors.py | 2 +- kfet/auth/middleware.py | 25 +- kfet/auth/models.py | 12 + kfet/auth/signals.py | 40 +++ .../templates/kfet/login_genericteam.html | 7 - kfet/auth/tests.py | 299 +++++++++++++++++- kfet/auth/views.py | 111 +++++-- kfet/signals.py | 21 -- kfet/static/kfet/js/kfet.js | 66 +++- kfet/templates/kfet/base_nav.html | 21 +- kfet/templates/kfet/confirm_form.html | 20 ++ kfet/templates/kfet/nav_item.html | 4 + kfet/urls.py | 4 +- kfet/views.py | 2 +- 18 files changed, 559 insertions(+), 119 deletions(-) create mode 100644 kfet/auth/signals.py delete mode 100644 kfet/auth/templates/kfet/login_genericteam.html delete mode 100644 kfet/signals.py create mode 100644 kfet/templates/kfet/confirm_form.html diff --git a/cof/settings/common.py b/cof/settings/common.py index 92759d21..0437f5db 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -100,7 +100,7 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'kfet.auth.middleware.KFetAuthenticationMiddleware', + 'kfet.auth.middleware.TemporaryAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -128,7 +128,7 @@ TEMPLATES = [ 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', - 'kfet.auth.context_processors.auth', + 'kfet.auth.context_processors.temporary_auth', 'kfet.context_processors.config', ], }, @@ -191,7 +191,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'gestioncof.shared.COFCASBackend', - 'kfet.auth.backends.GenericTeamBackend', + 'kfet.auth.backends.GenericBackend', ) RECAPTCHA_USE_SSL = True diff --git a/kfet/apps.py b/kfet/apps.py index 3dd2c0e8..4f114c37 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -11,7 +11,6 @@ class KFetConfig(AppConfig): verbose_name = "Application K-Fêt" def ready(self): - import kfet.signals self.register_config() def register_config(self): diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index 03742843..d91931f5 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -9,5 +9,6 @@ class KFetAuthConfig(AppConfig): verbose_name = _("K-Fêt - Authentification et Autorisation") def ready(self): + from . import signals # noqa from .utils import setup_kfet_generic_user post_migrate.connect(setup_kfet_generic_user, sender=self) diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index b2f1cb03..c6ad21b2 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -22,32 +22,22 @@ class BaseKFetBackend: return None -class KFetBackend(BaseKFetBackend): - def authenticate(self, request): - password = request.POST.get('KFETPASSWORD', '') - password = request.META.get('HTTP_KFETPASSWORD', password) - if not password: - return None - +class AccountBackend(BaseKFetBackend): + def authenticate(self, request, kfet_password=None): try: - return Account.objects.get_by_password(password).user + return Account.objects.get_by_password(kfet_password).user except Account.DoesNotExist: return None -class GenericTeamBackend(BaseKFetBackend): - def authenticate(self, username=None, token=None): - valid_token = GenericTeamToken.objects.get(token=token) - if username == 'kfet_genericteam' and valid_token: - return get_kfet_generic_user() - return None - - def get_user(self, user_id): +class GenericBackend(BaseKFetBackend): + def authenticate(self, request, kfet_token=None): try: - return ( - User.objects - .select_related('profile__account_kfet') - .get(pk=user_id) - ) - except User.DoesNotExist: - return None + team_token = GenericTeamToken.objects.get(token=kfet_token) + except GenericTeamToken.DoesNotExist: + return + + # No need to keep the token. + team_token.delete() + + return get_kfet_generic_user() diff --git a/kfet/auth/context_processors.py b/kfet/auth/context_processors.py index 07c9537f..7b59b88b 100644 --- a/kfet/auth/context_processors.py +++ b/kfet/auth/context_processors.py @@ -1,7 +1,7 @@ from django.contrib.auth.context_processors import PermWrapper -def auth(request): +def temporary_auth(request): if hasattr(request, 'real_user'): return { 'user': request.real_user, diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 1a930c3b..748ce4dd 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- +from django.contrib.auth import get_user_model -from django.contrib.auth.models import User +from .backends import AccountBackend -from .backends import KFetBackend +User = get_user_model() -class KFetAuthenticationMiddleware(object): - """Authenticate another user for this request if KFetBackend succeeds. +class TemporaryAuthMiddleware: + """Authenticate another user for this request if AccountBackend succeeds. By the way, if a user is authenticated, we refresh its from db to add values from CofProfile and Account of this user. @@ -15,15 +16,23 @@ class KFetAuthenticationMiddleware(object): def process_request(self, request): if request.user.is_authenticated(): # avoid multiple db accesses in views and templates - user_pk = request.user.pk request.user = ( User.objects .select_related('profile__account_kfet') - .get(pk=user_pk) + .get(pk=request.user.pk) ) - kfet_backend = KFetBackend() - temp_request_user = kfet_backend.authenticate(request) + temp_request_user = AccountBackend().authenticate( + request, + kfet_password=self.get_kfet_password(request), + ) + if temp_request_user: request.real_user = request.user request.user = temp_request_user + + def get_kfet_password(self, request): + return ( + request.META.get('HTTP_KFETPASSWORD') or + request.POST.get('KFETPASSWORD') + ) diff --git a/kfet/auth/models.py b/kfet/auth/models.py index 53aef6c9..ecd40091 100644 --- a/kfet/auth/models.py +++ b/kfet/auth/models.py @@ -1,5 +1,17 @@ from django.db import models +from django.utils.crypto import get_random_string + + +class GenericTeamTokenManager(models.Manager): + + def create_token(self): + token = get_random_string(50) + while self.filter(token=token).exists(): + token = get_random_string(50) + return self.create(token=token) class GenericTeamToken(models.Model): token = models.CharField(max_length=50, unique=True) + + objects = GenericTeamTokenManager() diff --git a/kfet/auth/signals.py b/kfet/auth/signals.py new file mode 100644 index 00000000..3d7af18b --- /dev/null +++ b/kfet/auth/signals.py @@ -0,0 +1,40 @@ +from django.contrib import messages +from django.contrib.auth.signals import user_logged_in +from django.core.urlresolvers import reverse +from django.dispatch import receiver +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + +from .utils import get_kfet_generic_user + + +@receiver(user_logged_in) +def suggest_auth_generic(sender, request, user, **kwargs): + """ + Suggest logged in user to continue as the kfet generic user. + + Message is only added if the following conditions are met: + - the next page (where user is going to be redirected due to successful + authentication) is related to kfet, i.e. 'k-fet' is in its url. + - logged in user is a kfet staff member (except the generic user). + """ + # Filter against the next page. + if not(hasattr(request, 'GET') and 'next' in request.GET): + return + + next_page = request.GET['next'] + generic_url = reverse('kfet.login.generic') + + if not('k-fet' in next_page and not next_page.startswith(generic_url)): + return + + # Filter against the logged in user. + if not(user.has_perm('kfet.is_team') and user != get_kfet_generic_user()): + return + + # Seems legit to add message. + text = _("K-Fêt — Ouvrir une session partagée ?") + messages.info(request, mark_safe( + '{}' + .format(generic_url, text) + )) diff --git a/kfet/auth/templates/kfet/login_genericteam.html b/kfet/auth/templates/kfet/login_genericteam.html deleted file mode 100644 index d2f8eca0..00000000 --- a/kfet/auth/templates/kfet/login_genericteam.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'kfet/base.html' %} - -{% block extra_head %} - -{% endblock %} diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 47cc2376..c2f183cd 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -1,15 +1,26 @@ # -*- coding: utf-8 -*- +from unittest import mock -from django.test import TestCase -from django.contrib.auth.models import User, Group +from django.core import signing +from django.core.urlresolvers import reverse +from django.contrib.auth.models import AnonymousUser, Group, Permission, User +from django.test import RequestFactory, TestCase from kfet.forms import UserGroupForm from kfet.models import Account from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME +from .backends import AccountBackend, GenericBackend +from .middleware import TemporaryAuthMiddleware +from .models import GenericTeamToken from .utils import get_kfet_generic_user +from .views import GenericLoginView +## +# Forms +## + class UserGroupFormTests(TestCase): """Test suite for UserGroupForm.""" @@ -70,3 +81,287 @@ class KFetGenericUserTests(TestCase): self.assertEqual(generic.trigramme, KFET_GENERIC_TRIGRAMME) self.assertEqual(generic.user.username, KFET_GENERIC_USERNAME) self.assertEqual(get_kfet_generic_user(), generic.user) + + +## +# Backends +## + +class AccountBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + acc = Account(trigramme='000') + acc.change_pwd('valid') + acc.save({'username': 'user'}) + + auth = AccountBackend().authenticate( + self.request, kfet_password='valid') + + self.assertEqual(auth, acc.user) + + def test_invalid(self): + auth = AccountBackend().authenticate( + self.request, kfet_password='invalid') + self.assertIsNone(auth) + + +class GenericBackendTests(TestCase): + + def setUp(self): + self.request = RequestFactory().get('/') + + def test_valid(self): + token = GenericTeamToken.objects.create_token() + + auth = GenericBackend().authenticate( + self.request, kfet_token=token.token) + + self.assertEqual(auth, get_kfet_generic_user()) + self.assertEqual(GenericTeamToken.objects.all().count(), 0) + + def test_invalid(self): + auth = GenericBackend().authenticate( + self.request, kfet_token='invalid') + self.assertIsNone(auth) + + +## +# Views +## + +class GenericLoginViewTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + user_acc = Account(trigramme='000') + user_acc.save({'username': 'user'}) + self.user = user_acc.user + self.user.set_password('user') + self.user.save() + + team_acc = Account(trigramme='100') + team_acc.save({'username': 'team'}) + self.team = team_acc.user + self.team.set_password('team') + self.team.save() + self.team.user_permissions.add( + Permission.objects.get( + content_type__app_label='kfet', codename='is_team'), + ) + + self.url = reverse('kfet.login.generic') + self.generic_user = get_kfet_generic_user() + + def test_url(self): + self.assertEqual(self.url, '/k-fet/login/generic') + + def test_notoken_get(self): + """ + Send confirmation for user to emit POST request, instead of GET. + """ + self.client.login(username='team', password='team') + + r = self.client.get(self.url) + + self.assertEqual(r.status_code, 200) + self.assertTemplateUsed(r, 'kfet/confirm_form.html') + + def test_notoken_post(self): + """ + POST request without token in COOKIES sets a token and redirects to + logout url. + """ + self.client.login(username='team', password='team') + + r = self.client.post(self.url) + + self.assertRedirects( + r, '/logout?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def test_notoken_not_team(self): + """ + Logged in user must be a team user to initiate login as generic user. + """ + self.client.login(username='user', password='user') + + # With GET. + r = self.client.get(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + # Also with POST. + r = self.client.post(self.url) + self.assertRedirects( + r, '/login?next={}'.format(self.url), + fetch_redirect_response=False, + ) + + def _set_signed_cookie(self, client, key, value): + signed_value = signing.get_cookie_signer(salt=key).sign(value) + client.cookies.load({key: signed_value}) + + def _is_cookie_deleted(self, client, key): + try: + self.assertNotIn(key, client.cookies) + except AssertionError: + try: + cookie = client.cookies[key] + # It also can be emptied. + self.assertEqual(cookie.value, '') + self.assertEqual( + cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') + self.assertEqual(cookie['max-age'], 0) + except AssertionError: + raise AssertionError("The cookie '%s' still exists." % key) + + def test_withtoken_valid(self): + """ + The kfet generic user is logged in. + """ + token = GenericTeamToken.objects.create(token='valid') + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'valid') + + r = self.client.get(self.url) + + self.assertRedirects(r, reverse('kfet.kpsul')) + self.assertEqual(r.wsgi_request.user, self.generic_user) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + with self.assertRaises(GenericTeamToken.DoesNotExist): + token.refresh_from_db() + + def test_withtoken_invalid(self): + """ + If token is invalid, delete it and try again. + """ + self._set_signed_cookie( + self.client, GenericLoginView.TOKEN_COOKIE_NAME, 'invalid') + + r = self.client.get(self.url) + + self.assertRedirects(r, self.url, fetch_redirect_response=False) + self.assertEqual(r.wsgi_request.user, AnonymousUser()) + self._is_cookie_deleted( + self.client, GenericLoginView.TOKEN_COOKIE_NAME) + + def test_flow_ok(self): + """ + A team user is logged in as the kfet generic user. + """ + self.client.login(username='team', password='team') + next_url = '/k-fet/' + + r = self.client.post( + '{}?next={}'.format(self.url, next_url), follow=True) + + self.assertEqual(r.wsgi_request.user, self.generic_user) + self.assertEqual(r.wsgi_request.path, '/k-fet/') + + +## +# Temporary authentication +# +# Includes: +# - TemporaryAuthMiddleware +# - temporary_auth context processor +## + +class TemporaryAuthTests(TestCase): + + def setUp(self): + patcher_messages = mock.patch('gestioncof.signals.messages') + patcher_messages.start() + self.addCleanup(patcher_messages.stop) + + self.factory = RequestFactory() + + user1_acc = Account(trigramme='000') + user1_acc.change_pwd('kfet_user1') + user1_acc.save({'username': 'user1'}) + self.user1 = user1_acc.user + self.user1.set_password('user1') + self.user1.save() + + user2_acc = Account(trigramme='100') + user2_acc.change_pwd('kfet_user2') + user2_acc.save({'username': 'user2'}) + self.user2 = user2_acc.user + self.user2.set_password('user2') + self.user2.save() + + self.perm = Permission.objects.get( + content_type__app_label='kfet', codename='is_team') + self.user2.user_permissions.add(self.perm) + + def test_middleware_header(self): + """ + A user can be authenticated if ``HTTP_KFETPASSWORD`` header of a + request contains a valid kfet password. + """ + request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_post(self): + """ + A user can be authenticated if ``KFETPASSWORD`` of POST data contains + a valid kfet password. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user2) + self.assertEqual(request.real_user, self.user1) + + def test_middleware_invalid(self): + """ + The given password must be a password of an Account. + """ + request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) + request.user = self.user1 + + TemporaryAuthMiddleware().process_request(request) + + self.assertEqual(request.user, self.user1) + self.assertFalse(hasattr(request, 'real_user')) + + def test_context_processor(self): + """ + Context variables give the real authenticated user and his permissions. + """ + self.client.login(username='user1', password='user1') + + r = self.client.get('/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + + self.assertEqual(r.context['user'], self.user1) + self.assertNotIn('kfet.is_team', r.context['perms']) + + def test_auth_not_persistent(self): + """ + The authentication is temporary, i.e. for one request. + """ + self.client.login(username='user1', password='user1') + + r1 = self.client.get( + '/k-fet/accounts/', HTTP_KFETPASSWORD='kfet_user2') + self.assertEqual(r1.wsgi_request.user, self.user2) + + r2 = self.client.get('/k-fet/accounts/') + self.assertEqual(r2.wsgi_request.user, self.user1) diff --git a/kfet/auth/views.py b/kfet/auth/views.py index ce44b007..7b9f4099 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -3,38 +3,105 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import Group, User -from django.core.urlresolvers import reverse_lazy +from django.contrib.auth.views import redirect_to_login +from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Prefetch -from django.shortcuts import render -from django.utils.crypto import get_random_string +from django.http import QueryDict +from django.shortcuts import redirect, render +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import View +from django.views.decorators.http import require_http_methods from django.views.generic.edit import CreateView, UpdateView -from django_cas_ng.views import logout as cas_logout_view - -from kfet.decorators import teamkfet_required - from .forms import GroupForm from .models import GenericTeamToken -@teamkfet_required -def login_genericteam(request): - # Check si besoin de déconnecter l'utilisateur de CAS - cas_logout = None - if request.user.profile.login_clipper: - # Récupèration de la vue de déconnexion de CAS - # Ici, car request sera modifié après - next_page = request.META.get('HTTP_REFERER', None) - cas_logout = cas_logout_view(request, next_page=next_page) +class GenericLoginView(View): + """ + View to authenticate as kfet generic user. - # Authentification du compte générique - token = GenericTeamToken.objects.create(token=get_random_string(50)) - user = authenticate(username="kfet_genericteam", token=token.token) - login(request, user) + It is a 2-step view. First, issue a token if user is a team member and send + him to the logout view (for proper disconnect) with callback url to here. + Then authenticate the token to log in as the kfet generic user. - messages.success(request, "Connecté en utilisateur partagé") + Token is stored in COOKIES to avoid share it with the authentication + provider, which can be external. Session is unusable as it will be cleared + on logout. + """ + TOKEN_COOKIE_NAME = 'kfettoken' - return cas_logout or render(request, "kfet/login_genericteam.html") + @method_decorator(require_http_methods(['GET', 'POST'])) + def dispatch(self, request, *args, **kwargs): + token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) + if not token: + if not request.user.has_perm('kfet.is_team'): + return redirect_to_login(request.get_full_path()) + + if request.method == 'POST': + # Step 1: set token and logout user. + return self.prepare_auth() + else: + # GET request should not change server/client states. Send a + # confirmation template to emit a POST request. + return render(request, 'kfet/confirm_form.html', { + 'title': _("Ouvrir une session partagée"), + 'text': _( + "Êtes-vous sûr·e de vouloir ouvrir une session " + "partagée ?" + ), + }) + else: + # Step 2: validate token. + return self.validate_auth(token) + + def prepare_auth(self): + # Issue token. + token = GenericTeamToken.objects.create_token() + + # Prepare callback of logout. + here_url = reverse(login_generic) + if 'next' in self.request.GET: + # Keep given next page. + here_qd = QueryDict(mutable=True) + here_qd['next'] = self.request.GET['next'] + here_url += '?{}'.format(here_qd.urlencode()) + + logout_url = reverse('cof-logout') + logout_qd = QueryDict(mutable=True) + logout_qd['next'] = here_url + logout_url += '?{}'.format(logout_qd.urlencode(safe='/')) + + resp = redirect(logout_url) + resp.set_signed_cookie( + self.TOKEN_COOKIE_NAME, token.token, httponly=True) + return resp + + def validate_auth(self, token): + # Authenticate with GenericBackend. + user = authenticate(request=self.request, kfet_token=token) + + if user: + # Log in generic user. + login(self.request, user) + messages.success(self.request, _( + "K-Fêt — Ouverture d'une session partagée." + )) + resp = redirect(self.get_next_url()) + else: + # Try again. + resp = redirect(self.request.get_full_path()) + + # Prevents blocking due to an invalid COOKIE. + resp.delete_cookie(self.TOKEN_COOKIE_NAME) + return resp + + def get_next_url(self): + return self.request.GET.get('next', reverse('kfet.kpsul')) + + +login_generic = GenericLoginView.as_view() @permission_required('kfet.manage_perms') diff --git a/kfet/signals.py b/kfet/signals.py deleted file mode 100644 index c677ac9c..00000000 --- a/kfet/signals.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.contrib import messages -from django.contrib.auth.signals import user_logged_in -from django.core.urlresolvers import reverse -from django.dispatch import receiver -from django.utils.safestring import mark_safe - - -@receiver(user_logged_in) -def messages_on_login(sender, request, user, **kwargs): - if (not user.username == 'kfet_genericteam' and - user.has_perm('kfet.is_team') and - hasattr(request, 'GET') and - 'k-fet' in request.GET.get('next', '')): - messages.info(request, mark_safe( - '' - ' Connexion en utilisateur partagé ?' - '' - .format(reverse('kfet.login.genericteam')) - )) diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index b34f2005..75b80b04 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,22 +1,33 @@ -$(document).ready(function() { - if (typeof Cookies !== 'undefined') { - // Retrieving csrf token - csrftoken = Cookies.get('csrftoken'); - // Appending csrf token to ajax post requests - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +/** + * CSRF Token + */ + +var csrftoken = ''; +if (typeof Cookies !== 'undefined') + csrftoken = Cookies.get('csrftoken'); + +// Add CSRF token in header of AJAX requests. + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); } - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - } - }); } }); +function add_csrf_form($form) { + $form.append( + $('', {'name': 'csrfmiddlewaretoken', 'value': csrftoken}) + ); +} + + /* * Generic Websocket class and k-psul ws instanciation */ @@ -199,3 +210,28 @@ jconfirm.defaults = { confirmButton: '', cancelButton: '' }; + + +/** + * Create form node, given an url used as 'action', with csrftoken set. + */ +function create_form(url) { + let $form = $('', { + 'action': url, + 'method': 'post', + }); + add_csrf_form($form); + return $form; +} + + +/** + * Emit a POST request from tag. + * + * Usage: + * {…} + */ +function submit_url(el) { + let url = $(el).data('url'); + create_form(url).appendTo($('body')).submit(); +} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index abcb8e18..dda6c1ef 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,4 +1,4 @@ -{% load static %} +{% load i18n static %} {% load wagtailcore_tags %} - - diff --git a/kfet/templates/kfet/confirm_form.html b/kfet/templates/kfet/confirm_form.html new file mode 100644 index 00000000..1cffd171 --- /dev/null +++ b/kfet/templates/kfet/confirm_form.html @@ -0,0 +1,20 @@ +{% extends "kfet/base_form.html" %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} +{% block header %}{% endblock %} + +{% block main-class %}main-bg main-padding text-center{% endblock %} +{% block main-size %}col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3{% endblock %} + +{% block main %} + + +

    + {{ text }} +

    + + {% csrf_token %} +
    + +{% endblock %} diff --git a/kfet/templates/kfet/nav_item.html b/kfet/templates/kfet/nav_item.html index 8bc311b8..b5981266 100644 --- a/kfet/templates/kfet/nav_item.html +++ b/kfet/templates/kfet/nav_item.html @@ -4,6 +4,8 @@
  • {% if href %} + {% else %} + {% endif %} {% if href %} + {% else %} + {% endif %}
  • diff --git a/kfet/urls.py b/kfet/urls.py index c3499b18..eb4f8311 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -8,8 +8,8 @@ from kfet.decorators import teamkfet_required urlpatterns = [ - url(r'^login/genericteam$', views.login_genericteam, - name='kfet.login.genericteam'), + url(r'^login/generic$', views.login_generic, + name='kfet.login.generic'), url(r'^history$', views.history, name='kfet.history'), diff --git a/kfet/views.py b/kfet/views.py index 386eddb6..79fe184d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -51,7 +51,7 @@ import statistics from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale from .auth.views import ( # noqa - account_group, login_genericteam, AccountGroupCreate, AccountGroupUpdate, + account_group, login_generic, AccountGroupCreate, AccountGroupUpdate, ) From d18fb86a98b5044a169c154961da10b6001e1507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 25 Sep 2017 18:26:54 +0200 Subject: [PATCH 49/75] Fix attribution inlines of participant in admin --- bda/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/admin.py b/bda/admin.py index 83c89ea5..60d3c1ba 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -56,7 +56,7 @@ class AttributionInline(admin.TabularInline): def get_queryset(self, request): qs = super().get_queryset(request) if self.listing is not None: - qs.filter(spectacle__listing=self.listing) + qs = qs.filter(spectacle__listing=self.listing) return qs From 596868f5b6762629ac4d6e4f0347aa7b9b77d785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 30 Sep 2017 02:39:45 +0200 Subject: [PATCH 50/75] plop --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5080ef32..e2c36d8d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,9 @@ variables: POSTGRES_USER: "cof_gestion" POSTGRES_DB: "cof_gestion" + # psql password authentication + PGPASSWORD: $POSTGRES_PASSWORD + cache: paths: @@ -28,11 +31,10 @@ before_script: - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py # Remove the old test database if it has not been done yet - - psql --username=cof_gestion --password="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" --host="$DBHOST" - -e "DROP DATABASE test_$DBNAME" || true + - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt test: stage: test script: - - python manage.py test + - python manage.py test -v3 From 435e211b3d064204eb7d77a624cdbb1f00a687a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 2 Oct 2017 13:58:52 +0200 Subject: [PATCH 51/75] Add a "PEI" status + "Gratis" subscription fees --- gestioncof/migrations/0013_pei.py | 47 ++++++++++++++++++++++++++++ gestioncof/models.py | 51 ++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 gestioncof/migrations/0013_pei.py diff --git a/gestioncof/migrations/0013_pei.py b/gestioncof/migrations/0013_pei.py new file mode 100644 index 00000000..2fbddf1f --- /dev/null +++ b/gestioncof/migrations/0013_pei.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gestioncof', '0012_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='cofprofile', + name='occupation', + field=models.CharField( + verbose_name='Occupation', + max_length=9, + default='1A', + choices=[ + ('exterieur', 'Extérieur'), + ('1A', '1A'), + ('2A', '2A'), + ('3A', '3A'), + ('4A', '4A'), + ('archicube', 'Archicube'), + ('doctorant', 'Doctorant'), + ('CST', 'CST'), + ('PEI', 'PEI') + ]), + ), + migrations.AlterField( + model_name='cofprofile', + name='type_cotiz', + field=models.CharField( + verbose_name='Type de cotisation', + max_length=9, + default='normalien', + choices=[ + ('etudiant', 'Normalien étudiant'), + ('normalien', 'Normalien élève'), + ('exterieur', 'Extérieur'), + ('gratis', 'Gratuit') + ]), + ), + ] diff --git a/gestioncof/models.py b/gestioncof/models.py index 6aa6f9cd..ea2cacc4 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -8,23 +8,6 @@ from gestioncof.petits_cours_models import choices_length from bda.models import Spectacle -OCCUPATION_CHOICES = ( - ('exterieur', _("Extérieur")), - ('1A', _("1A")), - ('2A', _("2A")), - ('3A', _("3A")), - ('4A', _("4A")), - ('archicube', _("Archicube")), - ('doctorant', _("Doctorant")), - ('CST', _("CST")), -) - -TYPE_COTIZ_CHOICES = ( - ('etudiant', _("Normalien étudiant")), - ('normalien', _("Normalien élève")), - ('exterieur', _("Extérieur")), -) - TYPE_COMMENT_FIELD = ( ('text', _("Texte long")), ('char', _("Texte court")), @@ -32,6 +15,40 @@ TYPE_COMMENT_FIELD = ( class CofProfile(models.Model): + STATUS_EXTE = "exterieur" + STATUS_1A = "1A" + STATUS_2A = "2A" + STATUS_3A = "3A" + STATUS_4A = "4A" + STATUS_ARCHI = "archicube" + STATUS_DOCTORANT = "doctorant" + STATUS_CST = "CST" + STATUS_PEI = "PEI" + + OCCUPATION_CHOICES = ( + (STATUS_EXTE, _("Extérieur")), + (STATUS_1A, _("1A")), + (STATUS_2A, _("2A")), + (STATUS_3A, _("3A")), + (STATUS_4A, _("4A")), + (STATUS_ARCHI, _("Archicube")), + (STATUS_DOCTORANT, _("Doctorant")), + (STATUS_CST, _("CST")), + (STATUS_PEI, _("PEI")), + ) + + COTIZ_ETUDIANT = "etudiant" + COTIZ_NORMALIEN = "normalien" + COTIZ_EXTE = "exterieur" + COTIZ_GRATIS = "gratis" + + TYPE_COTIZ_CHOICES = ( + (COTIZ_ETUDIANT, _("Normalien étudiant")), + (COTIZ_NORMALIEN, _("Normalien élève")), + (COTIZ_EXTE, _("Extérieur")), + (COTIZ_GRATIS, _("Gratuit")), + ) + user = models.OneToOneField(User, related_name="profile") login_clipper = models.CharField( "Login clipper", max_length=32, blank=True From 4d1cb3c2d7032fe2a481610bf5e68393ac44365e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 15:26:14 +0200 Subject: [PATCH 52/75] Set password for redis in CI --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2c36d8d..85be668b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,7 @@ variables: DJANGO_SETTINGS_MODULE: "cof.settings.prod" DBHOST: "postgres" REDIS_HOST: "redis" + REDIS_PASSWD: "dummy" # Cached packages PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" @@ -33,6 +34,7 @@ before_script: # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt + - redis-cli config set requirepass $REDIS_PASSWD || true test: stage: test From 503b305299fe66f9b84195312049b48962b6337e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 18:34:18 +0200 Subject: [PATCH 53/75] djangorestframework 3.7 breaks with Django 1.8 JSONField doesn't exist in Django 1.8 --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index f3964212..1591656d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,5 +26,8 @@ python-dateutil wagtail==1.10.* wagtailmenus==2.2.* +# Remove this when we switch to Django 1.11 +djangorestframework==3.6.4 + # Production tools wheel From 29ef297b2a83177820d5cb087a5869f0c4996eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 15:48:38 +0200 Subject: [PATCH 54/75] =?UTF-8?q?try=20to=20set=20the=20redis=20password?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85be668b..f297cf40 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,6 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD - cache: paths: - vendor/python @@ -31,10 +30,10 @@ before_script: - mkdir -p vendor/{python,pip,apt} - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py + - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt - - redis-cli config set requirepass $REDIS_PASSWD || true test: stage: test From e0ab7f5f94b5812bb69551a84ff5569305beb8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 21:21:28 +0200 Subject: [PATCH 55/75] Fix migration conflict --- kfet/migrations/0057_merge.py | 3 ++- kfet/migrations/0058_amend_supplier.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py index 48f63399..e3cadb23 100644 --- a/kfet/migrations/0057_merge.py +++ b/kfet/migrations/0057_merge.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): dependencies = [ + ('kfet', '0057_add_perms_config'), ('kfet', '0056_change_account_meta'), ('kfet', '0054_update_promos'), ] diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0058_amend_supplier.py index 0b45dade..764322b0 100644 --- a/kfet/migrations/0058_amend_supplier.py +++ b/kfet/migrations/0058_amend_supplier.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0057_add_perms_config'), + ('kfet', '0057_merge'), ] operations = [ From 3f6c5be74836e325a5f6d8ee16e9499ba358c305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 10 Oct 2017 21:27:15 +0200 Subject: [PATCH 56/75] Upgrade python packages before testing --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f297cf40..b00817de 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ before_script: - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt + - pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt test: stage: test From f4a7e9dbf1c7075f13c7673dba698af76ac42320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 11 Oct 2017 23:34:43 +0200 Subject: [PATCH 57/75] Verbosity should stay calm. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b00817de..19bcc736 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,4 +38,4 @@ before_script: test: stage: test script: - - python manage.py test -v3 + - python manage.py test From 85657591f5f69f19fec15d94f6e1d3d7a72ebc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 12 Oct 2017 11:10:30 +0200 Subject: [PATCH 58/75] =?UTF-8?q?Fix=20fields=20cleaning=20with=20unreacha?= =?UTF-8?q?ble=20items=20when=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … object is being created. --- kfet/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kfet/forms.py b/kfet/forms.py index 6ef3aefb..7bafbdd7 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -136,6 +136,8 @@ class UserGroupForm(forms.ModelForm): def clean_groups(self): kfet_groups = self.cleaned_data.get('groups') + if self.instance.pk is None: + return kfet_groups other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') return list(kfet_groups) + list(other_groups) @@ -173,6 +175,8 @@ class GroupForm(forms.ModelForm): # other_groups = self.instance.permissions.difference( # self.fields['permissions'].queryset # ) + if self.instance.pk is None: + return kfet_perms other_perms = self.instance.permissions.exclude( pk__in=[p.pk for p in self.fields['permissions'].queryset], ) From fccad5edee83df4726cf54aa19dee5b02661db1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 16 Oct 2017 14:31:02 +0200 Subject: [PATCH 59/75] rename root -> kfet_genericteam in fixtures --- kfet/cms/fixtures/kfet_wagtail_17_05.json | 86 +++++++++++------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/kfet/cms/fixtures/kfet_wagtail_17_05.json b/kfet/cms/fixtures/kfet_wagtail_17_05.json index f6a46c30..66ac7040 100644 --- a/kfet/cms/fixtures/kfet_wagtail_17_05.json +++ b/kfet/cms/fixtures/kfet_wagtail_17_05.json @@ -53,7 +53,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -83,7 +83,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -113,7 +113,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -143,7 +143,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -173,7 +173,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -203,7 +203,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -233,7 +233,7 @@ "page" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -263,7 +263,7 @@ "kfetpage" ], "owner": [ - "root" + "kfet_genericteam" ], "expired": false, "first_published_at": "2017-05-28T04:20:00.000Z", @@ -681,7 +681,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Plan d'acc\u00e8s", @@ -694,7 +694,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Demande d'autorisation", @@ -707,7 +707,7 @@ "fields": { "created_at": "2017-05-30T04:20:00.000Z", "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "collection": 2, "title": "K-F\u00eat - Trait\u00e9 de Flipper Th\u00e9orique", @@ -730,7 +730,7 @@ "title": "K-F\u00eat - Amazon Hunt", "width": 200, "uploaded_by_user": [ - "root" + "kfet_genericteam" ] } }, @@ -750,7 +750,7 @@ "title": "K-F\u00eat - Fun Machine", "width": 200, "uploaded_by_user": [ - "root" + "kfet_genericteam" ] } }, @@ -767,7 +767,7 @@ "title": "Hugo Manet", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -787,7 +787,7 @@ "title": "Lisa Gourdon", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -807,7 +807,7 @@ "title": "Pierre Quesselaire", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -827,7 +827,7 @@ "title": "Thibault Scoquard", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -847,7 +847,7 @@ "title": "Arnaud Fanthomme", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -867,7 +867,7 @@ "title": "Vincent Balerdi", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -887,7 +887,7 @@ "title": "Nathana\u00ebl Willaime", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -907,7 +907,7 @@ "title": "\u00c9lisabeth Miller", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -927,7 +927,7 @@ "title": "Arthur Lesage", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -947,7 +947,7 @@ "title": "Sarah Asset", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -967,7 +967,7 @@ "title": "Alexandre Legrand", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -987,7 +987,7 @@ "title": "\u00c9tienne Baudel", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1007,7 +1007,7 @@ "title": "Marine Snape", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1027,7 +1027,7 @@ "title": "Anatole Gosset", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1047,7 +1047,7 @@ "title": "Jacko Rastikian", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1067,7 +1067,7 @@ "title": "Alexandre Jannaud", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1087,7 +1087,7 @@ "title": "Aur\u00e9lien Delobelle", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1107,7 +1107,7 @@ "title": "Sylvain Douteau", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1127,7 +1127,7 @@ "title": "Rapha\u00ebl Lescanne", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1147,7 +1147,7 @@ "title": "Romain Gourvil", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1167,7 +1167,7 @@ "title": "Marie Labeye", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1187,7 +1187,7 @@ "title": "Oscar Blumberg", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1207,7 +1207,7 @@ "title": "Za\u00efd Allybokus", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1227,7 +1227,7 @@ "title": "Damien Garreau", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1247,7 +1247,7 @@ "title": "Andr\u00e9a Londono-Lopez", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1267,7 +1267,7 @@ "title": "Tristan Roussel", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1287,7 +1287,7 @@ "title": "Guillaume Vernade", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1307,7 +1307,7 @@ "title": "Lucas Mercier", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1327,7 +1327,7 @@ "title": "Fran\u00e7ois Maillot", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, @@ -1347,7 +1347,7 @@ "title": "Fabrice Catoire", "collection": 2, "uploaded_by_user": [ - "root" + "kfet_genericteam" ], "created_at": "2017-05-30T04:20:00.000Z" }, From 46187659ed7a91c11030308cdfb2879413c14526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 17 Oct 2017 14:41:53 +0200 Subject: [PATCH 60/75] Fix tirage pk conflicts with postgres --- bda/migrations/0002_add_tirage.py | 58 +++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index 1956a4a4..22c387a0 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -5,17 +5,34 @@ from django.db import migrations, models from django.conf import settings from django.utils import timezone -def forwards_func(apps, schema_editor): + +def fill_tirage_fields(apps, schema_editor): + """ + Create a `Tirage` to fill new field `tirage` of `Participant` + and `Spectacle` already existing. + """ + Participant = apps.get_model("bda", "Participant") + Spectacle = apps.get_model("bda", "Spectacle") Tirage = apps.get_model("bda", "Tirage") - db_alias = schema_editor.connection.alias - Tirage.objects.using(db_alias).bulk_create([ - Tirage( - id=1, - title="Tirage de test (migration)", - active=False, - ouverture=timezone.now(), - fermeture=timezone.now()), - ]) + + # These querysets only contains instances not linked to any `Tirage`. + participants = Participant.objects.filter(tirage=None) + spectacles = Spectacle.objects.filter(tirage=None) + + if not participants.count() and not spectacles.count(): + # No need to create a "trash" tirage. + return + + tirage = Tirage.objects.create( + title="Tirage de test (migration)", + active=False, + ouverture=timezone.now(), + fermeture=timezone.now(), + ) + + participants.update(tirage=tirage) + spectacles.update(tirage=tirage) + class Migration(migrations.Migration): @@ -35,22 +52,33 @@ class Migration(migrations.Migration): ('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')), ], ), - migrations.RunPython(forwards_func, migrations.RunPython.noop), migrations.AlterField( model_name='participant', name='user', field=models.ForeignKey(to=settings.AUTH_USER_MODEL), ), + # Create fields `spectacle` for `Participant` and `Spectacle` models. + # These fields are not nullable, but we first create them as nullable + # to give a default value for existing instances of these models. migrations.AddField( model_name='participant', name='tirage', - field=models.ForeignKey(default=1, to='bda.Tirage'), - preserve_default=False, + field=models.ForeignKey(to='bda.Tirage', null=True), ), migrations.AddField( model_name='spectacle', name='tirage', - field=models.ForeignKey(default=1, to='bda.Tirage'), - preserve_default=False, + field=models.ForeignKey(to='bda.Tirage', null=True), + ), + migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), + migrations.AlterField( + model_name='participant', + name='tirage', + field=models.ForeignKey(to='bda.Tirage'), + ), + migrations.AlterField( + model_name='spectacle', + name='tirage', + field=models.ForeignKey(to='bda.Tirage'), ), ] From 8b1f174b13f98c0bccb6a04d4e8bbb2f7d71b329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 24 Oct 2017 16:46:15 +0200 Subject: [PATCH 61/75] manage.py is executable --- manage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 manage.py diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 From 8673da18742fea41d8902f45170089cdc6fd5a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 24 Oct 2017 16:51:54 +0200 Subject: [PATCH 62/75] Fix migration conflict --- .../{0058_amend_supplier.py => 0060_amend_supplier.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename kfet/migrations/{0058_amend_supplier.py => 0060_amend_supplier.py} (96%) diff --git a/kfet/migrations/0058_amend_supplier.py b/kfet/migrations/0060_amend_supplier.py similarity index 96% rename from kfet/migrations/0058_amend_supplier.py rename to kfet/migrations/0060_amend_supplier.py index 764322b0..4eb569f8 100644 --- a/kfet/migrations/0058_amend_supplier.py +++ b/kfet/migrations/0060_amend_supplier.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0057_merge'), + ('kfet', '0059_create_generic'), ] operations = [ From 1cc51f17a390d88a75895bb09691e9244aa8989e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 24 Oct 2017 17:55:02 +0200 Subject: [PATCH 63/75] Prevent connection to LDAP when settings is None --- gestioncof/autocomplete.py | 2 +- kfet/autocomplete.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 3532525d..968398fd 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -58,7 +58,7 @@ def autocomplete(request): ) # Fetching data from the SPI - if hasattr(settings, 'LDAP_SERVER_URL'): + if getattr(settings, 'LDAP_SERVER_URL', None): # Fetching ldap_query = '(&{:s})'.format(''.join( '(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit) diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 0a9bb42c..c4886180 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -76,7 +76,7 @@ def account_create(request): queries['users_notcof'].values_list('username', flat=True)) # Fetching data from the SPI - if hasattr(settings, 'LDAP_SERVER_URL'): + if getattr(settings, 'LDAP_SERVER_URL', None): # Fetching ldap_query = '(&{:s})'.format(''.join( '(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=word) From af3a7cf6971bfead6541f76048f61dc0407fb5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 24 Oct 2017 17:56:14 +0200 Subject: [PATCH 64/75] =?UTF-8?q?Reapply=20fix=20to=20kfetauth=20(?= =?UTF-8?q?=E2=80=A6)=20and=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/auth/forms.py | 4 ++++ kfet/tests/test_views.py | 14 -------------- kfet/views.py | 2 +- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py index 0c9fa53b..876e8814 100644 --- a/kfet/auth/forms.py +++ b/kfet/auth/forms.py @@ -18,6 +18,8 @@ class GroupForm(forms.ModelForm): # other_groups = self.instance.permissions.difference( # self.fields['permissions'].queryset # ) + if self.instance.pk is None: + return kfet_perms other_perms = self.instance.permissions.exclude( pk__in=[p.pk for p in self.fields['permissions'].queryset], ) @@ -36,6 +38,8 @@ class UserGroupForm(forms.ModelForm): def clean_groups(self): kfet_groups = self.cleaned_data.get('groups') + if self.instance.pk is None: + return kfet_groups other_groups = self.instance.groups.exclude(name__icontains='K-Fêt') return list(kfet_groups) + list(other_groups) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index ff9803c9..c7ed5dda 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -18,20 +18,6 @@ from .testcases import ViewTestCaseMixin from .utils import create_team, create_user, get_perms -class LoginGenericTeamViewTests(ViewTestCaseMixin, TestCase): - url_name = 'kfet.login.genericteam' - url_expected = '/k-fet/login/genericteam' - - auth_user = 'team' - auth_forbidden = [None, 'user'] - - def test_ok(self): - r = self.client.get(self.url) - self.assertEqual(r.status_code, 200) - logged_in = r.wsgi_request.user - self.assertEqual(logged_in.username, 'kfet_genericteam') - - class AccountListViewTests(ViewTestCaseMixin, TestCase): url_name = 'kfet.account' url_expected = '/k-fet/accounts/' diff --git a/kfet/views.py b/kfet/views.py index 7a28819e..f1dd6834 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -12,7 +12,7 @@ from django.views.generic.edit import CreateView, UpdateView from django.core.urlresolvers import reverse, reverse_lazy from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User, Permission from django.http import JsonResponse, Http404 from django.forms import formset_factory From 8e8e9aa0764b911c8eb1e6726e4a120e6fb40d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 24 Oct 2017 19:19:57 +0200 Subject: [PATCH 65/75] Fix migration history --- kfet/migrations/0057_merge.py | 3 +-- .../{0057_add_perms_config.py => 0061_add_perms_config.py} | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename kfet/migrations/{0057_add_perms_config.py => 0061_add_perms_config.py} (94%) diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py index e3cadb23..48f63399 100644 --- a/kfet/migrations/0057_merge.py +++ b/kfet/migrations/0057_merge.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0057_add_perms_config'), ('kfet', '0056_change_account_meta'), ('kfet', '0054_update_promos'), ] diff --git a/kfet/migrations/0057_add_perms_config.py b/kfet/migrations/0061_add_perms_config.py similarity index 94% rename from kfet/migrations/0057_add_perms_config.py rename to kfet/migrations/0061_add_perms_config.py index 1300665f..01bdf51d 100644 --- a/kfet/migrations/0057_add_perms_config.py +++ b/kfet/migrations/0061_add_perms_config.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('kfet', '0056_change_account_meta'), + ('kfet', '0060_amend_supplier'), ] operations = [ From a07b5308a322ac03b3f252b4a74b025ed1f0c1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:01:58 +0200 Subject: [PATCH 66/75] PetitCoursAttributionCounter defaults to 0 --- gestioncof/petits_cours_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py index 753e8674..d9ea9668 100644 --- a/gestioncof/petits_cours_models.py +++ b/gestioncof/petits_cours_models.py @@ -157,14 +157,16 @@ class PetitCoursAttributionCounter(models.Model): compteurs de tout le monde. """ counter, created = cls.objects.get_or_create( - user=user, matiere=matiere) + user=user, + matiere=matiere, + ) if created: mincount = ( cls.objects.filter(matiere=matiere).exclude(user=user) .aggregate(Min('count')) ['count__min'] ) - counter.count = mincount + counter.count = mincount or 0 counter.save() return counter From 40abe27e8185f843ed2d313c978701832fe0543b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:05:14 +0200 Subject: [PATCH 67/75] EMAIL_HOST needs to be set but as a secret --- cof/settings/common.py | 1 + cof/settings/secret_example.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index f92dc83b..a2ea3f5e 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -31,6 +31,7 @@ def import_secret(name): SECRET_KEY = import_secret("SECRET_KEY") ADMINS = import_secret("ADMINS") SERVER_EMAIL = import_secret("SERVER_EMAIL") +EMAIL_HOST = import_secret("EMAIL_HOST") DBNAME = import_secret("DBNAME") DBUSER = import_secret("DBUSER") diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index e9c0e63c..e966565a 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,6 +1,7 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' ADMINS = None SERVER_EMAIL = "root@vagrant" +EMAIL_HOST = "localhost" DBUSER = "cof_gestion" DBNAME = "cof_gestion" From 1a136088bfa8892eebde77cb5aa8619dd342b3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:08:29 +0200 Subject: [PATCH 68/75] Add missing type in custommail (dev only) --- gestioncof/management/data/custommail.json | 655 +++++++++++---------- 1 file changed, 334 insertions(+), 321 deletions(-) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index 9ee9b1ea..590ebf18 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -1,587 +1,600 @@ [ { - "model": "custommail.variabletype", - "pk": 1, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "auth", "user" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 1 }, { - "model": "custommail.variabletype", - "pk": 2, + "model": "custommail.type", "fields": { + "kind": "int", "content_type": null, "inner1": null, - "kind": "int", "inner2": null - } + }, + "pk": 2 }, { - "model": "custommail.variabletype", - "pk": 3, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "bda", "spectacle" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 3 }, { - "model": "custommail.variabletype", - "pk": 4, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "bda", "spectaclerevente" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 4 }, { - "model": "custommail.variabletype", - "pk": 5, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "sites", "site" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 5 }, { - "model": "custommail.variabletype", - "pk": 6, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "gestioncof", "petitcoursdemande" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 6 }, { - "model": "custommail.variabletype", - "pk": 7, + "model": "custommail.type", "fields": { - "content_type": null, - "inner1": null, "kind": "list", + "content_type": null, + "inner1": 12, "inner2": null - } + }, + "pk": 7 }, { - "model": "custommail.variabletype", - "pk": 8, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 1, - "kind": "list", "inner2": null - } + }, + "pk": 8 }, { - "model": "custommail.variabletype", - "pk": 9, + "model": "custommail.type", "fields": { - "content_type": null, - "inner1": null, "kind": "pair", + "content_type": null, + "inner1": 12, "inner2": 8 - } + }, + "pk": 9 }, { - "model": "custommail.variabletype", - "pk": 10, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 9, - "kind": "list", "inner2": null - } + }, + "pk": 10 }, { - "model": "custommail.variabletype", - "pk": 11, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 3, - "kind": "list", "inner2": null - } + }, + "pk": 11 +}, +{ + "model": "custommail.type", + "fields": { + "kind": "model", + "content_type": [ + "gestioncof", + "petitcourssubject" + ], + "inner1": null, + "inner2": null + }, + "pk": 12 }, { "model": "custommail.custommail", - "pk": 1, "fields": { "shortname": "welcome", "subject": "Bienvenue au COF", - "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre", - "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime." - } + "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime.", + "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre" + }, + "pk": 1 }, { "model": "custommail.custommail", - "pk": 2, "fields": { "shortname": "bda-rappel", "subject": "{{ show }}", - "description": "Mail de rappel pour les spectacles BdA", - "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts", + "description": "Mail de rappel pour les spectacles BdA" + }, + "pk": 2 }, { "model": "custommail.custommail", - "pk": 3, "fields": { "shortname": "bda-revente", "subject": "{{ show }}", - "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente." + }, + "pk": 3 }, { "model": "custommail.custommail", - "pk": 4, "fields": { "shortname": "bda-shotgun", "subject": "{{ show }}", - "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es." + }, + "pk": 4 }, { "model": "custommail.custommail", - "pk": 5, "fields": { "shortname": "bda-revente-winner", "subject": "BdA-Revente : {{ show.title }}", - "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente", - "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente" + }, + "pk": 5 }, { "model": "custommail.custommail", - "pk": 6, "fields": { "shortname": "bda-revente-loser", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.", - "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts", + "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente." + }, + "pk": 6 }, { "model": "custommail.custommail", - "pk": 7, "fields": { "shortname": "bda-revente-seller", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e", - "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e" + }, + "pk": 7 }, { "model": "custommail.custommail", - "pk": 8, "fields": { "shortname": "bda-revente-new", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.", - "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts", + "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires." + }, + "pk": 8 }, { "model": "custommail.custommail", - "pk": 9, "fields": { "shortname": "bda-buy-shotgun", "subject": "BdA-Revente : {{ show.title }}", - "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.", - "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})" - } + "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})", + "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun." + }, + "pk": 9 }, { "model": "custommail.custommail", - "pk": 10, "fields": { "shortname": "petit-cours-mail-eleve", "subject": "Petits cours ENS par le COF", - "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours", - "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours" - } + "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours", + "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours" + }, + "pk": 10 }, { "model": "custommail.custommail", - "pk": 11, "fields": { "shortname": "petits-cours-mail-demandeur", "subject": "Cours particuliers ENS", - "description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})", - "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS" - } + "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS", + "description": "Mail envoy\u00e9 aux personnes qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})" + }, + "pk": 11 }, { "model": "custommail.custommail", - "pk": 12, "fields": { "shortname": "bda-attributions", "subject": "R\u00e9sultats du tirage au sort", - "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places", - "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts" - } + "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts", + "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places" + }, + "pk": 12 }, { "model": "custommail.custommail", - "pk": 13, "fields": { "shortname": "bda-attributions-decus", "subject": "R\u00e9sultats du tirage au sort", - "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place", - "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts" - } + "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts", + "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place" + }, + "pk": 13 }, { - "model": "custommail.custommailvariable", - "pk": 1, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Utilisateur de GestioCOF", "custommail": 1, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 2, - "fields": { + "type": 1, "name": "member", - "description": "Utilisateur ayant eu une place pour ce spectacle", - "custommail": 2, - "type": 1 - } + "description": "Utilisateur de GestioCOF" + }, + "pk": 1 }, { - "model": "custommail.custommailvariable", - "pk": 3, + "model": "custommail.variable", "fields": { + "custommail": 2, + "type": 1, + "name": "member", + "description": "Utilisateur ayant eu une place pour ce spectacle" + }, + "pk": 2 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 2, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 2, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 3 }, { - "model": "custommail.custommailvariable", - "pk": 4, + "model": "custommail.variable", "fields": { + "custommail": 2, + "type": 2, "name": "nb_attr", - "description": "Nombre de places obtenues", - "custommail": 2, - "type": 2 - } + "description": "Nombre de places obtenues" + }, + "pk": 4 }, { - "model": "custommail.custommailvariable", - "pk": 5, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 4, "name": "revente", - "description": "Revente mentionn\u00e9e dans le mail", - "custommail": 3, - "type": 4 - } + "description": "Revente mentionn\u00e9e dans le mail" + }, + "pk": 5 }, { - "model": "custommail.custommailvariable", - "pk": 6, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 1, "name": "member", - "description": "Personne int\u00e9ress\u00e9e par la place", - "custommail": 3, - "type": 1 - } + "description": "Personne int\u00e9ress\u00e9e par la place" + }, + "pk": 6 }, { - "model": "custommail.custommailvariable", - "pk": 7, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 3, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 7 }, { - "model": "custommail.custommailvariable", - "pk": 8, + "model": "custommail.variable", "fields": { - "name": "site", - "description": "Site web (gestioCOF)", "custommail": 3, - "type": 5 - } + "type": 5, + "name": "site", + "description": "Site web (gestioCOF)" + }, + "pk": 8 }, { - "model": "custommail.custommailvariable", - "pk": 9, + "model": "custommail.variable", "fields": { - "name": "site", - "description": "Site web (gestioCOF)", "custommail": 4, - "type": 5 - } + "type": 5, + "name": "site", + "description": "Site web (gestioCOF)" + }, + "pk": 9 }, { - "model": "custommail.custommailvariable", - "pk": 10, + "model": "custommail.variable", "fields": { + "custommail": 4, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 4, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 10 }, { - "model": "custommail.custommailvariable", - "pk": 11, + "model": "custommail.variable", "fields": { + "custommail": 4, + "type": 1, "name": "member", - "description": "Personne int\u00e9ress\u00e9e par la place", - "custommail": 4, - "type": 1 - } + "description": "Personne int\u00e9ress\u00e9e par la place" + }, + "pk": 11 }, { - "model": "custommail.custommailvariable", - "pk": 12, + "model": "custommail.variable", "fields": { - "name": "acheteur", - "description": "Gagnant-e du tirage", "custommail": 5, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 13, - "fields": { - "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 5, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 14, - "fields": { - "name": "show", - "description": "Spectacle", - "custommail": 5, - "type": 3 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 15, - "fields": { - "name": "show", - "description": "Spectacle", - "custommail": 6, - "type": 3 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 16, - "fields": { - "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 6, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 17, - "fields": { + "type": 1, "name": "acheteur", - "description": "Personne inscrite au tirage qui n'a pas eu la place", - "custommail": 6, - "type": 1 - } + "description": "Gagnant-e du tirage" + }, + "pk": 12 }, { - "model": "custommail.custommailvariable", - "pk": 18, - "fields": { - "name": "acheteur", - "description": "Gagnant-e du tirage", - "custommail": 7, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 19, + "model": "custommail.variable", "fields": { + "custommail": 5, + "type": 1, "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 7, - "type": 1 - } + "description": "Personne qui vend une place" + }, + "pk": 13 }, { - "model": "custommail.custommailvariable", - "pk": 20, + "model": "custommail.variable", "fields": { + "custommail": 5, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 7, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 14 }, { - "model": "custommail.custommailvariable", - "pk": 21, + "model": "custommail.variable", "fields": { + "custommail": 6, + "type": 3, "name": "show", - "description": "Spectacle", + "description": "Spectacle" + }, + "pk": 15 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 6, + "type": 1, + "name": "vendeur", + "description": "Personne qui vend une place" + }, + "pk": 16 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 6, + "type": 1, + "name": "acheteur", + "description": "Personne inscrite au tirage qui n'a pas eu la place" + }, + "pk": 17 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 1, + "name": "acheteur", + "description": "Gagnant-e du tirage" + }, + "pk": 18 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 1, + "name": "vendeur", + "description": "Personne qui vend une place" + }, + "pk": 19 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 3, + "name": "show", + "description": "Spectacle" + }, + "pk": 20 +}, +{ + "model": "custommail.variable", + "fields": { "custommail": 8, - "type": 3 - } + "type": 3, + "name": "show", + "description": "Spectacle" + }, + "pk": 21 }, { - "model": "custommail.custommailvariable", - "pk": 22, + "model": "custommail.variable", "fields": { - "name": "vendeur", - "description": "Personne qui vend la place", "custommail": 8, - "type": 1 - } + "type": 1, + "name": "vendeur", + "description": "Personne qui vend la place" + }, + "pk": 22 }, { - "model": "custommail.custommailvariable", - "pk": 23, + "model": "custommail.variable", "fields": { + "custommail": 8, + "type": 4, "name": "revente", - "description": "Revente mentionn\u00e9e dans le mail", - "custommail": 8, - "type": 4 - } + "description": "Revente mentionn\u00e9e dans le mail" + }, + "pk": 23 }, { - "model": "custommail.custommailvariable", - "pk": 24, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 1, "name": "vendeur", - "description": "Personne qui vend la place", - "custommail": 9, - "type": 1 - } + "description": "Personne qui vend la place" + }, + "pk": 24 }, { - "model": "custommail.custommailvariable", - "pk": 25, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 9, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 25 }, { - "model": "custommail.custommailvariable", - "pk": 26, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 1, "name": "acheteur", - "description": "Personne qui prend la place au shotgun", - "custommail": 9, - "type": 1 - } + "description": "Personne qui prend la place au shotgun" + }, + "pk": 26 }, { - "model": "custommail.custommailvariable", - "pk": 27, + "model": "custommail.variable", "fields": { + "custommail": 10, + "type": 6, "name": "demande", - "description": "Demande de petit cours", - "custommail": 10, - "type": 6 - } + "description": "Demande de petit cours" + }, + "pk": 27 }, { - "model": "custommail.custommailvariable", - "pk": 28, + "model": "custommail.variable", "fields": { + "custommail": 10, + "type": 7, "name": "matieres", - "description": "Liste des mati\u00e8res concern\u00e9es par la demande", - "custommail": 10, - "type": 7 - } + "description": "Liste des mati\u00e8res concern\u00e9es par la demande" + }, + "pk": 28 }, { - "model": "custommail.custommailvariable", - "pk": 29, + "model": "custommail.variable", "fields": { + "custommail": 11, + "type": 10, "name": "proposals", - "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re", - "custommail": 11, - "type": 10 - } + "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re" + }, + "pk": 29 }, { - "model": "custommail.custommailvariable", - "pk": 30, + "model": "custommail.variable", "fields": { + "custommail": 11, + "type": 7, "name": "unsatisfied", - "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer", - "custommail": 11, - "type": 7 - } + "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer" + }, + "pk": 30 }, { - "model": "custommail.custommailvariable", - "pk": 31, + "model": "custommail.variable", "fields": { + "custommail": 12, + "type": 11, "name": "places", - "description": "Places de spectacle du participant", - "custommail": 12, - "type": 11 - } + "description": "Places de spectacle du participant" + }, + "pk": 31 }, { - "model": "custommail.custommailvariable", - "pk": 32, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Participant du tirage au sort", "custommail": 12, - "type": 1 - } + "type": 1, + "name": "member", + "description": "Participant du tirage au sort" + }, + "pk": 32 }, { - "model": "custommail.custommailvariable", - "pk": 33, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Participant du tirage au sort", "custommail": 13, - "type": 1 - } + "type": 1, + "name": "member", + "description": "Participant du tirage au sort" + }, + "pk": 33 } ] From 1c90d067fa38542ca0877e8ba22dcd2a6108ad8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 26 Oct 2017 18:13:09 +0200 Subject: [PATCH 69/75] Make cof.settings a module --- cof/settings/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cof/settings/__init__.py diff --git a/cof/settings/__init__.py b/cof/settings/__init__.py new file mode 100644 index 00000000..e69de29b From 895f7e062cc9da5d590f18c6ed36fa1f1e6738cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 27 Oct 2017 03:00:33 +0200 Subject: [PATCH 70/75] Delete GlobalPermissions model (migrations) It is an old model which doesn't exist anymore in kfet.models module. This adds its missing DeleteModel in migrations. --- kfet/migrations/0062_delete_globalpermissions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 kfet/migrations/0062_delete_globalpermissions.py diff --git a/kfet/migrations/0062_delete_globalpermissions.py b/kfet/migrations/0062_delete_globalpermissions.py new file mode 100644 index 00000000..ee245412 --- /dev/null +++ b/kfet/migrations/0062_delete_globalpermissions.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('kfet', '0061_add_perms_config'), + ] + + operations = [ + migrations.DeleteModel( + name='GlobalPermissions', + ), + ] From 93fa79128cd4688e8fbe1adb07db49409cc2d9d8 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 31 Oct 2017 15:10:21 +0100 Subject: [PATCH 71/75] order table striped --- kfet/templates/kfet/order_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index 6ff9b824..d95cafe3 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -11,7 +11,7 @@
    {% csrf_token %}
    - +
    From 273e6374ef072c6b589ddb46268a8085a2085359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 11:09:16 +0100 Subject: [PATCH 72/75] Pluralization in bda -> participant list --- bda/templates/bda/participants.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bda/templates/bda/participants.html b/bda/templates/bda/participants.html index 85af4a2e..c3ff31d6 100644 --- a/bda/templates/bda/participants.html +++ b/bda/templates/bda/participants.html @@ -47,11 +47,11 @@
    - + From b0b0542407384fe7988162119228edf286eecb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 19 Nov 2017 18:41:39 +0100 Subject: [PATCH 73/75] Upgrade to Django 1.11 - Deprecation warnings using Django 1.8 are resolved. - Deprecation warnings using Django 1.11 are resolved. - Admin: grappelli is no longer used. - Upgrade to django-autocomplete-light v3 (v2 is not 1.11 compatible). * autocomplete.modelform_factory being dropped, code uses dal Select2 views and widgets. --- bda/admin.py | 15 +++- bda/autocomplete_light_registry.py | 18 ----- bda/migrations/0001_initial.py | 12 +-- bda/migrations/0002_add_tirage.py | 10 +-- bda/migrations/0007_extends_spectacle.py | 4 +- bda/migrations/0009_revente.py | 3 + bda/models.py | 50 ++++++++---- bda/urls.py | 6 ++ bda/views.py | 25 ++++++ cof/settings/common.py | 17 ++-- cof/settings/dev.py | 11 +-- cof/urls.py | 25 +++--- gestioncof/admin.py | 12 ++- gestioncof/autocomplete_light_registry.py | 10 --- gestioncof/migrations/0001_initial.py | 40 +++++----- gestioncof/migrations/0006_add_calendar.py | 3 +- gestioncof/models.py | 51 ++++++++---- gestioncof/petits_cours_models.py | 31 ++++++-- gestioncof/petits_cours_views.py | 4 +- gestioncof/templates/admin/base_site.html | 6 -- gestioncof/templates/admin/index.html | 78 ------------------- .../templates/gestioncof/base_header.html | 4 +- gestioncof/templates/gestioncof/event.html | 2 +- gestioncof/templates/home.html | 20 ++--- gestioncof/templates/login.html | 2 +- gestioncof/templates/login_switch.html | 4 +- .../registration/password_change_done.html | 2 +- .../registration/password_change_form.html | 2 +- gestioncof/urls.py | 18 +++-- gestioncof/views.py | 21 ++++- kfet/auth/middleware.py | 9 ++- kfet/auth/tests.py | 8 +- kfet/cms/migrations/0001_initial.py | 2 +- kfet/models.py | 2 +- kfet/templates/kfet/base_nav.html | 10 ++- kfet/tests/test_views.py | 2 +- kfet/tests/testcases.py | 4 +- kfet/tests/utils.py | 2 +- kfet/views.py | 28 ++++--- requirements-devel.txt | 2 +- requirements.txt | 18 ++--- utils/__init__.py | 0 utils/views/__init__.py | 0 utils/views/autocomplete.py | 26 +++++++ 44 files changed, 341 insertions(+), 278 deletions(-) delete mode 100644 bda/autocomplete_light_registry.py delete mode 100644 gestioncof/autocomplete_light_registry.py delete mode 100644 gestioncof/templates/admin/base_site.html delete mode 100644 gestioncof/templates/admin/index.html create mode 100644 utils/__init__.py create mode 100644 utils/views/__init__.py create mode 100644 utils/views/autocomplete.py diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..6638ad45 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import autocomplete_light from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail @@ -9,6 +8,9 @@ from django.db.models import Sum, Count from django.template.defaultfilters import pluralize from django.utils import timezone from django import forms + +from dal.autocomplete import ModelSelect2 + from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente @@ -24,8 +26,17 @@ class ReadOnlyMixin(object): return readonly_fields + self.readonly_fields_update +class ChoixSpectacleAdminForm(forms.ModelForm): + class Meta: + widgets = { + 'participant': ModelSelect2(url='bda-participant-autocomplete'), + 'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'), + } + + class ChoixSpectacleInline(admin.TabularInline): model = ChoixSpectacle + form = ChoixSpectacleAdminForm sortable_field_name = "priority" @@ -180,7 +191,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin): - form = autocomplete_light.modelform_factory(ChoixSpectacle, exclude=[]) + form = ChoixSpectacleAdminForm def tirage(self, obj): return obj.participant.tirage diff --git a/bda/autocomplete_light_registry.py b/bda/autocomplete_light_registry.py deleted file mode 100644 index 6c2f3ea6..00000000 --- a/bda/autocomplete_light_registry.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import autocomplete_light - -from bda.models import Participant, Spectacle - -autocomplete_light.register( - Participant, search_fields=('user__username', 'user__first_name', - 'user__last_name'), - autocomplete_js_attributes={'placeholder': 'participant...'}) - -autocomplete_light.register( - Spectacle, search_fields=('title', ), - autocomplete_js_attributes={'placeholder': 'spectacle...'}) diff --git a/bda/migrations/0001_initial.py b/bda/migrations/0001_initial.py index aa2cb252..c4494413 100644 --- a/bda/migrations/0001_initial.py +++ b/bda/migrations/0001_initial.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): ('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)), ('slots', models.IntegerField(verbose_name=b'Places')), ('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')), - ('location', models.ForeignKey(to='bda.Salle')), + ('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)), ], options={ 'ordering': ('priority', 'date', 'title'), @@ -79,27 +79,27 @@ class Migration(migrations.Migration): migrations.AddField( model_name='participant', name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL), + field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='choixspectacle', name='participant', - field=models.ForeignKey(to='bda.Participant'), + field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), ), migrations.AddField( model_name='choixspectacle', name='spectacle', - field=models.ForeignKey(related_name='participants', to='bda.Spectacle'), + field=models.ForeignKey(related_name='participants', to='bda.Spectacle', on_delete=models.CASCADE), ), migrations.AddField( model_name='attribution', name='participant', - field=models.ForeignKey(to='bda.Participant'), + field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE), ), migrations.AddField( model_name='attribution', name='spectacle', - field=models.ForeignKey(related_name='attribues', to='bda.Spectacle'), + field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE), ), migrations.AlterUniqueTogether( name='choixspectacle', diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index 22c387a0..79f79a57 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -55,7 +55,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='participant', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), # Create fields `spectacle` for `Participant` and `Spectacle` models. # These fields are not nullable, but we first create them as nullable @@ -63,22 +63,22 @@ class Migration(migrations.Migration): migrations.AddField( model_name='participant', name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True), + field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='spectacle', name='tirage', - field=models.ForeignKey(to='bda.Tirage', null=True), + field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE), ), migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop), migrations.AlterField( model_name='participant', name='tirage', - field=models.ForeignKey(to='bda.Tirage'), + field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), ), migrations.AlterField( model_name='spectacle', name='tirage', - field=models.ForeignKey(to='bda.Tirage'), + field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py index b95c18de..6ea11dc0 100644 --- a/bda/migrations/0007_extends_spectacle.py +++ b/bda/migrations/0007_extends_spectacle.py @@ -73,6 +73,7 @@ class Migration(migrations.Migration): model_name='spectacle', name='category', field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle', + on_delete=models.CASCADE, null=True), ), migrations.AddField( @@ -84,6 +85,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='quote', name='spectacle', - field=models.ForeignKey(to='bda.Spectacle'), + field=models.ForeignKey(to='bda.Spectacle', + on_delete=models.CASCADE), ), ] diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py index 1cca4e86..70d6f338 100644 --- a/bda/migrations/0009_revente.py +++ b/bda/migrations/0009_revente.py @@ -47,12 +47,14 @@ class Migration(migrations.Migration): model_name='spectaclerevente', name='attribution', field=models.OneToOneField(to='bda.Attribution', + on_delete=models.CASCADE, related_name='revente'), ), migrations.AddField( model_name='spectaclerevente', name='seller', field=models.ForeignKey(to='bda.Participant', + on_delete=models.CASCADE, verbose_name='Vendeur', related_name='original_shows'), ), @@ -60,6 +62,7 @@ class Migration(migrations.Migration): model_name='spectaclerevente', name='soldTo', field=models.ForeignKey(to='bda.Participant', + on_delete=models.CASCADE, verbose_name='Vendue à', null=True, blank=True), ), diff --git a/bda/models.py b/bda/models.py index 41462d70..ba9ccf4a 100644 --- a/bda/models.py +++ b/bda/models.py @@ -59,9 +59,12 @@ class CategorieSpectacle(models.Model): class Spectacle(models.Model): title = models.CharField("Titre", max_length=300) - category = models.ForeignKey(CategorieSpectacle, blank=True, null=True) + category = models.ForeignKey( + CategorieSpectacle, on_delete=models.CASCADE, + blank=True, null=True, + ) date = models.DateTimeField("Date & heure") - location = models.ForeignKey(Salle) + location = models.ForeignKey(Salle, on_delete=models.CASCADE) vips = models.TextField('Personnalités', blank=True) description = models.TextField("Description", blank=True) slots_description = models.TextField("Description des places", blank=True) @@ -71,7 +74,7 @@ class Spectacle(models.Model): max_length=500) price = models.FloatField("Prix d'une place") slots = models.IntegerField("Places") - tirage = models.ForeignKey(Tirage) + tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) listing = models.BooleanField("Les places sont sur listing") rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True) @@ -135,7 +138,7 @@ class Spectacle(models.Model): class Quote(models.Model): - spectacle = models.ForeignKey(Spectacle) + spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE) text = models.TextField('Citation') author = models.CharField('Auteur', max_length=200) @@ -149,7 +152,7 @@ PAYMENT_TYPES = ( class Participant(models.Model): - user = models.ForeignKey(User) + user = models.ForeignKey(User, on_delete=models.CASCADE) choices = models.ManyToManyField(Spectacle, through="ChoixSpectacle", related_name="chosen_by") @@ -160,7 +163,7 @@ class Participant(models.Model): paymenttype = models.CharField("Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True) - tirage = models.ForeignKey(Tirage) + tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) choicesrevente = models.ManyToManyField(Spectacle, related_name="subscribed", blank=True) @@ -176,8 +179,11 @@ DOUBLE_CHOICES = ( class ChoixSpectacle(models.Model): - participant = models.ForeignKey(Participant) - spectacle = models.ForeignKey(Spectacle, related_name="participants") + participant = models.ForeignKey(Participant, on_delete=models.CASCADE) + spectacle = models.ForeignKey( + Spectacle, on_delete=models.CASCADE, + related_name="participants", + ) priority = models.PositiveIntegerField("Priorité") double_choice = models.CharField("Nombre de places", default="1", choices=DOUBLE_CHOICES, @@ -204,8 +210,11 @@ class ChoixSpectacle(models.Model): class Attribution(models.Model): - participant = models.ForeignKey(Participant) - spectacle = models.ForeignKey(Spectacle, related_name="attribues") + participant = models.ForeignKey(Participant, on_delete=models.CASCADE) + spectacle = models.ForeignKey( + Spectacle, on_delete=models.CASCADE, + related_name="attribues", + ) given = models.BooleanField("Donnée", default=False) def __str__(self): @@ -214,18 +223,25 @@ class Attribution(models.Model): class SpectacleRevente(models.Model): - attribution = models.OneToOneField(Attribution, - related_name="revente") + attribution = models.OneToOneField( + Attribution, on_delete=models.CASCADE, + related_name="revente", + ) date = models.DateTimeField("Date de mise en vente", default=timezone.now) answered_mail = models.ManyToManyField(Participant, related_name="wanted", blank=True) - seller = models.ForeignKey(Participant, - related_name="original_shows", - verbose_name="Vendeur") - soldTo = models.ForeignKey(Participant, blank=True, null=True, - verbose_name="Vendue à") + seller = models.ForeignKey( + Participant, on_delete=models.CASCADE, + verbose_name="Vendeur", + related_name="original_shows", + ) + soldTo = models.ForeignKey( + Participant, on_delete=models.CASCADE, + verbose_name="Vendue à", + blank=True, null=True, + ) notif_sent = models.BooleanField("Notification envoyée", default=False) diff --git a/bda/urls.py b/bda/urls.py index 876c84ea..6acc1746 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -32,6 +32,12 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), + url(r'^spectacles/autocomplete$', + views.spectacle_autocomplete, + name="bda-spectacle-autocomplete"), + url(r'^participants/autocomplete$', + views.participant_autocomplete, + name="bda-participant-autocomplete"), url(r'^liste-revente/(?P\d+)$', views.list_revente, name="bda-liste-revente"), diff --git a/bda/views.py b/bda/views.py index 84b6c9d3..6c4edff8 100644 --- a/bda/views.py +++ b/bda/views.py @@ -33,6 +33,8 @@ from bda.forms import ( InscriptionInlineFormSet, ) +from utils.views.autocomplete import Select2QuerySetView + @cof_required def etat_places(request, tirage_id): @@ -813,3 +815,26 @@ def catalogue(request, request_type): return JsonResponse(data_return, safe=False) # Si la requête n'est pas de la forme attendue, on quitte avec une erreur return HttpResponseBadRequest() + + +## +# Autocomplete views +# +# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view +## + + +class ParticipantAutocomplete(Select2QuerySetView): + model = Participant + search_fields = ('user__username', 'user__first_name', 'user__last_name') + + +participant_autocomplete = buro_required(ParticipantAutocomplete.as_view()) + + +class SpectacleAutocomplete(Select2QuerySetView): + model = Spectacle + search_fields = ('title',) + + +spectacle_autocomplete = buro_required(SpectacleAutocomplete.as_view()) diff --git a/cof/settings/common.py b/cof/settings/common.py index a2ea3f5e..48242fc3 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -57,17 +57,22 @@ BASE_DIR = os.path.dirname( # Application definition INSTALLED_APPS = [ 'gestioncof', + + # Must be before 'django.contrib.admin'. + # https://django-autocomplete-light.readthedocs.io/en/master/install.html + 'dal', + 'dal_select2', + 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', - 'grappelli', 'django.contrib.admin', 'django.contrib.admindocs', + 'bda', - 'autocomplete_light', 'captcha', 'django_cas_ng', 'bootstrapform', @@ -96,7 +101,7 @@ INSTALLED_APPS = [ 'kfet.cms', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -124,9 +129,9 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.core.context_processors.static', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 9c622063..6e1f6b11 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -4,7 +4,7 @@ The settings that are not listed here are imported from .common """ from .common import * # NOQA -from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES +from .common import INSTALLED_APPS, MIDDLEWARE EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -37,10 +37,11 @@ def show_toolbar(request): return DEBUG INSTALLED_APPS += ["debug_toolbar", "debug_panel"] -MIDDLEWARE_CLASSES = ( - ["debug_panel.middleware.DebugPanelMiddleware"] - + MIDDLEWARE_CLASSES -) + +MIDDLEWARE = [ + "debug_panel.middleware.DebugPanelMiddleware" +] + MIDDLEWARE + DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': show_toolbar, } diff --git a/cof/urls.py b/cof/urls.py index f62d5f01..e6e5d313 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -4,8 +4,6 @@ Fichier principal de configuration des urls du projet GestioCOF """ -import autocomplete_light - from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static @@ -24,7 +22,6 @@ from gestioncof.urls import export_patterns, petitcours_patterns, \ clubs_patterns from gestioncof.autocomplete import autocomplete -autocomplete_light.autodiscover() admin.autodiscover() urlpatterns = [ @@ -49,18 +46,22 @@ urlpatterns = [ name="cof-denied"), url(r'^cas/login$', django_cas_views.login, name="cas_login_view"), url(r'^cas/logout$', django_cas_views.logout), - url(r'^outsider/login$', gestioncof_views.login_ext), + url(r'^outsider/login$', gestioncof_views.login_ext, + name="ext_login_view"), url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), url(r'^login$', gestioncof_views.login, name="cof-login"), url(r'^logout$', gestioncof_views.logout, name="cof-logout"), # Infos persos - url(r'^profile$', gestioncof_views.profile), - url(r'^outsider/password-change$', django_views.password_change), + url(r'^profile$', gestioncof_views.profile, + name='profile'), + url(r'^outsider/password-change$', django_views.password_change, + name='password_change'), url(r'^outsider/password-change-done$', django_views.password_change_done, name='password_change_done'), # Inscription d'un nouveau membre - url(r'^registration$', gestioncof_views.registration), + url(r'^registration$', gestioncof_views.registration, + name='registration'), url(r'^registration/clipper/(?P[\w-]+)/' r'(?P.*)$', gestioncof_views.registration_form2, name="clipper-registration"), @@ -70,7 +71,8 @@ urlpatterns = [ name="empty-registration"), # Autocompletion url(r'^autocomplete/registration$', autocomplete), - url(r'^autocomplete/', include('autocomplete_light.urls')), + url(r'^user/autocomplete$', gestioncof_views.user_autocomplete, + name='cof-user-autocomplete'), # Interface admin url(r'^admin/logout/', gestioncof_views.logout), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), @@ -78,10 +80,11 @@ urlpatterns = [ csv_views.admin_list_export, {'fields': ['username', ]}), url(r'^admin/', include(admin.site.urls)), - url(r'^grappelli/', include('grappelli.urls')), # Liens utiles du COF et du BdA - url(r'^utile_cof$', gestioncof_views.utile_cof), - url(r'^utile_bda$', gestioncof_views.utile_bda), + url(r'^utile_cof$', gestioncof_views.utile_cof, + name='utile_cof'), + url(r'^utile_bda$', gestioncof_views.utile_bda, + name='utile_bda'), url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 0d7d7143..51969822 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from django.db.models import Q -import autocomplete_light +from dal.autocomplete import ModelSelect2 def add_link_field(target_model='', field='', link_text=str, @@ -217,8 +217,16 @@ def user_str(self): User.__str__ = user_str +class EventRegistrationAdminForm(forms.ModelForm): + class Meta: + widgets = { + 'user': ModelSelect2(url='cof-user-autocomplete'), + } + + class EventRegistrationAdmin(admin.ModelAdmin): - form = autocomplete_light.modelform_factory(EventRegistration, exclude=[]) + form = EventRegistrationAdminForm + list_display = ('__str__', 'event', 'user', 'paid') list_filter = ('paid',) search_fields = ('user__username', 'user__first_name', 'user__last_name', diff --git a/gestioncof/autocomplete_light_registry.py b/gestioncof/autocomplete_light_registry.py deleted file mode 100644 index 4c62d995..00000000 --- a/gestioncof/autocomplete_light_registry.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -import autocomplete_light - -from django.contrib.auth.models import User - -autocomplete_light.register( - User, search_fields=('username', 'first_name', 'last_name'), - attrs={'placeholder': 'membre...'} -) diff --git a/gestioncof/migrations/0001_initial.py b/gestioncof/migrations/0001_initial.py index b942645d..c6bb6151 100644 --- a/gestioncof/migrations/0001_initial.py +++ b/gestioncof/migrations/0001_initial.py @@ -48,7 +48,7 @@ class Migration(migrations.Migration): ('is_buro', models.BooleanField(default=False, verbose_name=b'Membre du Bur\xc3\xb4')), ('petits_cours_accept', models.BooleanField(default=False, verbose_name=b'Recevoir des petits cours')), ('petits_cours_remarques', models.TextField(default=b'', verbose_name='Remarques et pr\xe9cisions pour les petits cours', blank=True)), - ('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Profil COF', @@ -91,7 +91,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=200, verbose_name=b'Champ')), ('fieldtype', models.CharField(default=b'text', max_length=10, verbose_name=b'Type', choices=[(b'text', 'Texte long'), (b'char', 'Texte court')])), ('default', models.TextField(verbose_name=b'Valeur par d\xc3\xa9faut', blank=True)), - ('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event')), + ('event', models.ForeignKey(related_name='commentfields', to='gestioncof.Event', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Champ', @@ -102,7 +102,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('content', models.TextField(null=True, verbose_name=b'Contenu', blank=True)), - ('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField')), + ('commentfield', models.ForeignKey(related_name='values', to='gestioncof.EventCommentField', on_delete=models.CASCADE)), ], ), migrations.CreateModel( @@ -111,7 +111,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=200, verbose_name=b'Option')), ('multi_choices', models.BooleanField(default=False, verbose_name=b'Choix multiples')), - ('event', models.ForeignKey(related_name='options', to='gestioncof.Event')), + ('event', models.ForeignKey(related_name='options', to='gestioncof.Event', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Option', @@ -122,7 +122,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('value', models.CharField(max_length=200, verbose_name=b'Valeur')), - ('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption')), + ('event_option', models.ForeignKey(related_name='choices', to='gestioncof.EventOption', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Choix', @@ -133,10 +133,10 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('paid', models.BooleanField(default=False, verbose_name=b'A pay\xc3\xa9')), - ('event', models.ForeignKey(to='gestioncof.Event')), + ('event', models.ForeignKey(to='gestioncof.Event', on_delete=models.CASCADE)), ('filledcomments', models.ManyToManyField(to='gestioncof.EventCommentField', through='gestioncof.EventCommentValue')), ('options', models.ManyToManyField(to='gestioncof.EventOptionChoice')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Inscription', @@ -240,7 +240,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('question', models.CharField(max_length=200, verbose_name=b'Question')), ('multi_answers', models.BooleanField(default=False, verbose_name=b'Choix multiples')), - ('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey')), + ('survey', models.ForeignKey(related_name='questions', to='gestioncof.Survey', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'Question', @@ -251,7 +251,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('answer', models.CharField(max_length=200, verbose_name=b'R\xc3\xa9ponse')), - ('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion')), + ('survey_question', models.ForeignKey(related_name='answers', to='gestioncof.SurveyQuestion', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'R\xe9ponse', @@ -265,12 +265,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='surveyanswer', name='survey', - field=models.ForeignKey(to='gestioncof.Survey'), + field=models.ForeignKey(to='gestioncof.Survey', on_delete=models.CASCADE), ), migrations.AddField( model_name='surveyanswer', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursdemande', @@ -280,47 +280,47 @@ class Migration(migrations.Migration): migrations.AddField( model_name='petitcoursdemande', name='traitee_par', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursattributioncounter', name='matiere', - field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject'), + field=models.ForeignKey(verbose_name='Matiere', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursattributioncounter', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursattribution', name='demande', - field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande'), + field=models.ForeignKey(verbose_name='Demande', to='gestioncof.PetitCoursDemande', on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursattribution', name='matiere', - field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'), + field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursattribution', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursability', name='matiere', - field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject'), + field=models.ForeignKey(verbose_name='Mati\xe8re', to='gestioncof.PetitCoursSubject', on_delete=models.CASCADE), ), migrations.AddField( model_name='petitcoursability', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), migrations.AddField( model_name='eventcommentvalue', name='registration', - field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration'), + field=models.ForeignKey(related_name='comments', to='gestioncof.EventRegistration', on_delete=models.CASCADE), ), migrations.AlterUniqueTogether( name='surveyanswer', diff --git a/gestioncof/migrations/0006_add_calendar.py b/gestioncof/migrations/0006_add_calendar.py index 004d3602..27852f61 100644 --- a/gestioncof/migrations/0006_add_calendar.py +++ b/gestioncof/migrations/0006_add_calendar.py @@ -23,7 +23,8 @@ class Migration(migrations.Migration): ('subscribe_to_events', models.BooleanField(default=True)), ('subscribe_to_my_shows', models.BooleanField(default=True)), ('other_shows', models.ManyToManyField(to='bda.Spectacle')), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE)), ], ), migrations.AlterModelOptions( diff --git a/gestioncof/models.py b/gestioncof/models.py index ea2cacc4..0d816155 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -49,7 +49,10 @@ class CofProfile(models.Model): (COTIZ_GRATIS, _("Gratuit")), ) - user = models.OneToOneField(User, related_name="profile") + user = models.OneToOneField( + User, on_delete=models.CASCADE, + related_name="profile", + ) login_clipper = models.CharField( "Login clipper", max_length=32, blank=True ) @@ -130,7 +133,10 @@ class Event(models.Model): class EventCommentField(models.Model): - event = models.ForeignKey(Event, related_name="commentfields") + event = models.ForeignKey( + Event, on_delete=models.CASCADE, + related_name="commentfields", + ) name = models.CharField("Champ", max_length=200) fieldtype = models.CharField("Type", max_length=10, choices=TYPE_COMMENT_FIELD, default="text") @@ -144,9 +150,14 @@ class EventCommentField(models.Model): class EventCommentValue(models.Model): - commentfield = models.ForeignKey(EventCommentField, related_name="values") - registration = models.ForeignKey("EventRegistration", - related_name="comments") + commentfield = models.ForeignKey( + EventCommentField, on_delete=models.CASCADE, + related_name="values", + ) + registration = models.ForeignKey( + "EventRegistration", on_delete=models.CASCADE, + related_name="comments", + ) content = models.TextField("Contenu", blank=True, null=True) def __str__(self): @@ -154,7 +165,10 @@ class EventCommentValue(models.Model): class EventOption(models.Model): - event = models.ForeignKey(Event, related_name="options") + event = models.ForeignKey( + Event, on_delete=models.CASCADE, + related_name="options", + ) name = models.CharField("Option", max_length=200) multi_choices = models.BooleanField("Choix multiples", default=False) @@ -166,7 +180,10 @@ class EventOption(models.Model): class EventOptionChoice(models.Model): - event_option = models.ForeignKey(EventOption, related_name="choices") + event_option = models.ForeignKey( + EventOption, on_delete=models.CASCADE, + related_name="choices", + ) value = models.CharField("Valeur", max_length=200) class Meta: @@ -178,8 +195,8 @@ class EventOptionChoice(models.Model): class EventRegistration(models.Model): - user = models.ForeignKey(User) - event = models.ForeignKey(Event) + user = models.ForeignKey(User, on_delete=models.CASCADE) + event = models.ForeignKey(Event, on_delete=models.CASCADE) options = models.ManyToManyField(EventOptionChoice) filledcomments = models.ManyToManyField(EventCommentField, through=EventCommentValue) @@ -207,7 +224,10 @@ class Survey(models.Model): class SurveyQuestion(models.Model): - survey = models.ForeignKey(Survey, related_name="questions") + survey = models.ForeignKey( + Survey, on_delete=models.CASCADE, + related_name="questions", + ) question = models.CharField("Question", max_length=200) multi_answers = models.BooleanField("Choix multiples", default=False) @@ -219,7 +239,10 @@ class SurveyQuestion(models.Model): class SurveyQuestionAnswer(models.Model): - survey_question = models.ForeignKey(SurveyQuestion, related_name="answers") + survey_question = models.ForeignKey( + SurveyQuestion, on_delete=models.CASCADE, + related_name="answers", + ) answer = models.CharField("Réponse", max_length=200) class Meta: @@ -230,8 +253,8 @@ class SurveyQuestionAnswer(models.Model): class SurveyAnswer(models.Model): - user = models.ForeignKey(User) - survey = models.ForeignKey(Survey) + user = models.ForeignKey(User, on_delete=models.CASCADE) + survey = models.ForeignKey(Survey, on_delete=models.CASCADE) answers = models.ManyToManyField(SurveyQuestionAnswer, related_name="selected_by") @@ -247,7 +270,7 @@ class SurveyAnswer(models.Model): class CalendarSubscription(models.Model): token = models.UUIDField() - user = models.OneToOneField(User) + user = models.OneToOneField(User, on_delete=models.CASCADE) other_shows = models.ManyToManyField(Spectacle) subscribe_to_events = models.BooleanField(default=True) subscribe_to_my_shows = models.BooleanField(default=True) diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py index d9ea9668..bf2cfde9 100644 --- a/gestioncof/petits_cours_models.py +++ b/gestioncof/petits_cours_models.py @@ -35,8 +35,11 @@ class PetitCoursSubject(models.Model): class PetitCoursAbility(models.Model): - user = models.ForeignKey(User) - matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière")) + user = models.ForeignKey(User, on_delete=models.CASCADE) + matiere = models.ForeignKey( + PetitCoursSubject, on_delete=models.CASCADE, + verbose_name=_("Matière"), + ) niveau = models.CharField(_("Niveau"), choices=LEVELS_CHOICES, max_length=choices_length(LEVELS_CHOICES)) @@ -84,7 +87,10 @@ class PetitCoursDemande(models.Model): remarques = models.TextField(_("Remarques et précisions"), blank=True) traitee = models.BooleanField(_("Traitée"), default=False) - traitee_par = models.ForeignKey(User, blank=True, null=True) + traitee_par = models.ForeignKey( + User, on_delete=models.CASCADE, + blank=True, null=True, + ) processed = models.DateTimeField(_("Date de traitement"), blank=True, null=True) created = models.DateTimeField(_("Date de création"), auto_now_add=True) @@ -126,9 +132,15 @@ class PetitCoursDemande(models.Model): class PetitCoursAttribution(models.Model): - user = models.ForeignKey(User) - demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande")) - matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière")) + user = models.ForeignKey(User, on_delete=models.CASCADE) + demande = models.ForeignKey( + PetitCoursDemande, on_delete=models.CASCADE, + verbose_name=_("Demande"), + ) + matiere = models.ForeignKey( + PetitCoursSubject, on_delete=models.CASCADE, + verbose_name=_("Matière"), + ) date = models.DateTimeField(_("Date d'attribution"), auto_now_add=True) rank = models.IntegerField("Rang dans l'email") selected = models.BooleanField(_("Sélectionné par le demandeur"), @@ -145,8 +157,11 @@ class PetitCoursAttribution(models.Model): class PetitCoursAttributionCounter(models.Model): - user = models.ForeignKey(User) - matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere")) + user = models.ForeignKey(User, on_delete=models.CASCADE) + matiere = models.ForeignKey( + PetitCoursSubject, on_delete=models.CASCADE, + verbose_name=_("Matiere"), + ) count = models.IntegerField("Nombre d'envois", default=0) @classmethod diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 087c9cef..52a29883 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import json -from datetime import datetime from custommail.shortcuts import render_custom_mail from django.shortcuts import render, get_object_or_404, redirect @@ -13,6 +12,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db import transaction +from django.utils import timezone from gestioncof.models import CofProfile from gestioncof.petits_cours_models import ( @@ -287,7 +287,7 @@ def _traitement_post(request, demande): attrib.save() demande.traitee = True demande.traitee_par = request.user - demande.processed = datetime.now() + demande.processed = timezone.now() demande.save() return render(request, "gestioncof/traitement_demande_petit_cours_success.html", diff --git a/gestioncof/templates/admin/base_site.html b/gestioncof/templates/admin/base_site.html deleted file mode 100644 index 03fd0d17..00000000 --- a/gestioncof/templates/admin/base_site.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "admin/base.html" %} - -{% block extrahead %} - - {% include 'autocomplete_light/static.html' %} -{% endblock %} diff --git a/gestioncof/templates/admin/index.html b/gestioncof/templates/admin/index.html deleted file mode 100644 index 965c71fa..00000000 --- a/gestioncof/templates/admin/index.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "admin/base_site.html" %} - - -{% load i18n grp_tags log %} - - -{% block javascripts %} - {{ block.super }} -{% endblock %} - - -{% block breadcrumbs %} -
      -
    • {% trans "Home" %}
    • -
    -{% endblock %} -{% block content_title %} - {% if title %} -

    {{ title }}

    - {% endif %} -{% endblock %} - - -{% block content %} -
    -
    - - {% for app in app_list %} -
    -

    {% trans app.name %}

    - {% for model in app.models %} -
    - {% if model.perms.change %}{{ model.name }}{% else %}{{ model.name }}{% endif %} - {% if model.perms.add or model.perms.change %} - - {% endif %} -
    - {% endfor %} -
    - {% empty %} -

    {% trans "You don´t have permission to edit anything." %}

    - {% endfor %} -
    -
    -
    -

    {% trans 'Recent Actions' %}

    -
    -

    {% trans 'My Actions' %}

    - {% get_admin_log 20 as admin_log for_user user %} - {% if not admin_log %} -

    {% trans 'None available' %}

    - {% else %} -
      - {% for entry in admin_log %} -
    • - {% if entry.is_deletion %} - {{ entry.object_repr }} - {% else %} - {{ entry.object_repr }} - {% endif %} - {% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %} -
    • - {% endfor %} -
    - {% endif %} -
    -
    -
    -
    -{% endblock %} - diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index 21441875..e5f757a7 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -3,7 +3,7 @@ {% block content %}
    diff --git a/gestioncof/templates/gestioncof/event.html b/gestioncof/templates/gestioncof/event.html index 52f893db..f388bc25 100644 --- a/gestioncof/templates/gestioncof/event.html +++ b/gestioncof/templates/gestioncof/event.html @@ -5,7 +5,7 @@ {% if event.details %}

    {{ event.details }}

    {% endif %} - + {% csrf_token %} {{ form.as_p }} diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index acc04f30..65c4ba5e 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -14,7 +14,7 @@
    @@ -24,7 +24,7 @@
    @@ -69,11 +69,11 @@

    Divers

    {% endif %} @@ -86,16 +86,16 @@

    Général

  • Administration générale
  • Demandes de petits cours
  • -
  • Inscription d'un nouveau membre
  • +
  • Inscription d'un nouveau membre
  • Gestion des clubs
  • @@ -120,8 +120,8 @@

    Liens utiles

    diff --git a/gestioncof/templates/login.html b/gestioncof/templates/login.html index 1cd1d25d..bfc2dbb8 100644 --- a/gestioncof/templates/login.html +++ b/gestioncof/templates/login.html @@ -15,7 +15,7 @@

    Identifiants incorrects.

    {% endif %} + action="{% url 'ext_login_view' %}?next={{ next|urlencode }}"> {% csrf_token %}
    diff --git a/gestioncof/templates/login_switch.html b/gestioncof/templates/login_switch.html index aa8a68c6..d361493b 100644 --- a/gestioncof/templates/login_switch.html +++ b/gestioncof/templates/login_switch.html @@ -12,13 +12,13 @@
    + href="{% url 'cas_login_view' %}?next={{ next|urlencode }}">
    Compte clipper
    + href="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
    Extérieur
    diff --git a/gestioncof/templates/registration/password_change_done.html b/gestioncof/templates/registration/password_change_done.html index f83a781b..9f2c4a60 100644 --- a/gestioncof/templates/registration/password_change_done.html +++ b/gestioncof/templates/registration/password_change_done.html @@ -5,5 +5,5 @@ {% block realcontent %}

    Mot de passe modifié avec succès !

    -

    Retour au menu principal

    +

    Retour au menu principal

    {% endblock %} diff --git a/gestioncof/templates/registration/password_change_form.html b/gestioncof/templates/registration/password_change_form.html index f579fb31..d9a3f66a 100644 --- a/gestioncof/templates/registration/password_change_form.html +++ b/gestioncof/templates/registration/password_change_form.html @@ -5,7 +5,7 @@ {% block realcontent %}

    Changement de mot de passe

    - + {% csrf_token %} {{ form | bootstrap }} diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 57c2e8f2..2be609b3 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -36,19 +36,23 @@ petitcours_patterns = [ ] surveys_patterns = [ - url(r'^(?P\d+)/status$', views.survey_status), - url(r'^(?P\d+)$', views.survey), + url(r'^(?P\d+)/status$', views.survey_status, + name='survey.details.status'), + url(r'^(?P\d+)$', views.survey, + name='survey.details'), ] events_patterns = [ - url(r'^(?P\d+)$', views.event), - url(r'^(?P\d+)/status$', views.event_status), + url(r'^(?P\d+)$', views.event, + name='event.details'), + url(r'^(?P\d+)/status$', views.event_status, + name='event.details.status'), ] calendar_patterns = [ - url(r'^subscription$', 'gestioncof.views.calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', - 'gestioncof.views.calendar_ics') + url(r'^subscription$', views.calendar, + name='calendar'), + url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics) ] clubs_patterns = [ diff --git a/gestioncof/views.py b/gestioncof/views.py index ec9f6efd..5dfee83f 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -20,6 +20,8 @@ from django.contrib import messages from django_cas_ng.views import logout as cas_logout_view +from utils.views.autocomplete import Select2QuerySetView + from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ SurveyQuestionAnswer from gestioncof.models import Event, EventRegistration, EventOption, \ @@ -54,8 +56,8 @@ def home(request): def login(request): - if request.user.is_authenticated(): - return redirect("gestioncof.views.home") + if request.user.is_authenticated: + return redirect("home") context = {} if request.method == "GET" and 'next' in request.GET: context['next'] = request.GET['next'] @@ -786,3 +788,18 @@ class ConfigUpdate(FormView): def form_valid(self, form): form.save() return super().form_valid(form) + + +## +# Autocomplete views +# +# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view +## + + +class UserAutocomplete(Select2QuerySetView): + model = User + search_fields = ('username', 'first_name', 'last_name') + + +user_autocomplete = buro_required(UserAutocomplete.as_view()) diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 748ce4dd..48d9c4ee 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -13,8 +13,11 @@ class TemporaryAuthMiddleware: values from CofProfile and Account of this user. """ - def process_request(self, request): - if request.user.is_authenticated(): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: # avoid multiple db accesses in views and templates request.user = ( User.objects @@ -31,6 +34,8 @@ class TemporaryAuthMiddleware: request.real_user = request.user request.user = temp_request_user + return self.get_response(request) + def get_kfet_password(self, request): return ( request.META.get('HTTP_KFETPASSWORD') or diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index c2f183cd..0c8b25d3 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -286,6 +286,8 @@ class TemporaryAuthTests(TestCase): self.factory = RequestFactory() + self.middleware = TemporaryAuthMiddleware(mock.Mock()) + user1_acc = Account(trigramme='000') user1_acc.change_pwd('kfet_user1') user1_acc.save({'username': 'user1'}) @@ -312,7 +314,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -325,7 +327,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -337,7 +339,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user1) self.assertFalse(hasattr(request, 'real_user')) diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py index 951637c7..ed0b0948 100644 --- a/kfet/cms/migrations/0001_initial.py +++ b/kfet/cms/migrations/0001_initial.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='KFetPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page', on_delete=models.CASCADE)), ('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)), ('content', wagtail.wagtailcore.fields.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')), ('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')), diff --git a/kfet/models.py b/kfet/models.py index b1e351d5..deee76eb 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from django.db import models -from django.core.urlresolvers import reverse from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile +from django.urls import reverse from django.utils.six.moves import reduce from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index dda6c1ef..f4c07e05 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,10 +1,12 @@ {% load i18n static %} {% load wagtailcore_tags %} +{% slugurl "kfet" as kfet_home_url %} +
    Article