mirror of https://gitlab.crans.org/bde/nk20
Compare commits
14 Commits
3a52af33a2
...
25e26fe8cf
Author | SHA1 | Date |
---|---|---|
Yohann D'ANELLO | 25e26fe8cf | |
Yohann D'ANELLO | 0fae5b3e62 | |
Yohann D'ANELLO | 3784e97d60 | |
Yohann D'ANELLO | 6567d2f8cc | |
Yohann D'ANELLO | 999cc0a6b2 | |
Alexandre Iooss | 60de58b78a | |
Yohann D'ANELLO | 9c816a288d | |
Alexandre Iooss | c277d8bccd | |
Alexandre Iooss | 4a4c3d33b0 | |
Alexandre Iooss | 9c679d5bc9 | |
Alexandre Iooss | 3b49b7f4c1 | |
Alexandre Iooss | 747a878cca | |
Yohann D'ANELLO | c612e159cf | |
Yohann D'ANELLO | 1b84c8c603 |
200
README.md
200
README.md
|
@ -1,96 +1,99 @@
|
||||||
# NoteKfet 2020
|
# NoteKfet 2020
|
||||||
|
|
||||||
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
|
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/nk20/commits/master)
|
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||||
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
|
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||||
|
|
||||||
## Installation sur un serveur
|
## Installation sur un serveur
|
||||||
|
|
||||||
On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré.
|
On supposera pour la suite que vous utilisez une installation de Debian Buster ou Ubuntu 20.04 fraîche ou bien configuré.
|
||||||
|
|
||||||
1. Paquets nécessaires
|
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
|
||||||
|
Sinon vous pouvez suivre les étapes ici.
|
||||||
|
|
||||||
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi
|
### Installation avec Debian/Ubuntu
|
||||||
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
|
|
||||||
|
|
||||||
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante :
|
1. **Installation des dépendances APT.**
|
||||||
|
|
||||||
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
|
```bash
|
||||||
|
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi uwsgi-plugin-python3 python3-venv git acl
|
||||||
|
```
|
||||||
|
|
||||||
2. Clonage du dépot
|
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante,
|
||||||
|
|
||||||
on se met au bon endroit :
|
```bash
|
||||||
|
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
|
||||||
|
```
|
||||||
|
|
||||||
$ cd /var/www/
|
2. **Clonage du dépot** dans `/var/www/note_kfet`,
|
||||||
$ mkdir note_kfet
|
|
||||||
$ sudo chown www-data:www-data note_kfet
|
|
||||||
$ sudo usermod -a -G www-data $USER
|
|
||||||
$ sudo chmod g+ws note_kfet
|
|
||||||
$ sudo setfacl -d -m "g::rwx" note_kfet
|
|
||||||
$ cd note_kfet
|
|
||||||
$ git clone git@gitlab.crans.org:bde/nk20.git .
|
|
||||||
3. Environment Virtuel
|
|
||||||
|
|
||||||
À la racine du projet:
|
```bash
|
||||||
|
$ mkdir -p /var/www/note_kfet && cd /var/www/note_kfet
|
||||||
|
$ sudo chown www-data:www-data .
|
||||||
|
$ sudo chmod g+rwx .
|
||||||
|
$ sudo -u www-data git clone git@gitlab.crans.org:bde/nk20.git .
|
||||||
|
```
|
||||||
|
|
||||||
$ python3 -m venv env
|
3. **Création d'un environment de travail Python décorrélé du système.**
|
||||||
$ source env/bin/activate
|
|
||||||
(env)$ pip3 install -r requirements/base.txt
|
|
||||||
(env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite un base postgres
|
|
||||||
(env)$ deactivate
|
|
||||||
|
|
||||||
4. uwsgi et Nginx
|
```bash
|
||||||
|
$ python3 -m venv env
|
||||||
|
$ source env/bin/activate
|
||||||
|
(env)$ pip3 install -r requirements/base.txt
|
||||||
|
(env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite une base postgres
|
||||||
|
(env)$ deactivate # sortir de l'environnement
|
||||||
|
```
|
||||||
|
|
||||||
Un exemple de conf est disponible :
|
4. **Pour configurer UWSGI et NGINX**, des exemples de conf sont disponibles.
|
||||||
|
**_Modifier le fichier pour être en accord avec le reste de votre config_**
|
||||||
|
|
||||||
$ cp nginx_note.conf_example nginx_note.conf
|
```bash
|
||||||
|
$ cp nginx_note.conf_example nginx_note.conf
|
||||||
|
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
|
||||||
|
```
|
||||||
|
|
||||||
***Modifier le fichier pour être en accord avec le reste de votre config***
|
Si l'on a un emperor (plusieurs instance uwsgi):
|
||||||
|
|
||||||
On utilise uwsgi et Nginx pour gérer le coté serveur :
|
```bash
|
||||||
|
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
|
||||||
|
```
|
||||||
|
|
||||||
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
|
Sinon si on est dans le cas habituel :
|
||||||
|
|
||||||
Si l'on a un emperor (plusieurs instance uwsgi):
|
```bash
|
||||||
|
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/
|
||||||
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
|
```
|
||||||
|
|
||||||
Sinon:
|
|
||||||
|
|
||||||
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/
|
|
||||||
|
|
||||||
Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`.
|
Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`.
|
||||||
|
|
||||||
5. Base de données
|
5. **Base de données.** En production on utilise PostgreSQL.
|
||||||
|
|
||||||
En prod on utilise postgresql.
|
|
||||||
|
|
||||||
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
|
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
|
||||||
(env)$ pip3 install psycopg2
|
(env)$ pip3 install psycopg2
|
||||||
|
|
||||||
La config de la base de donnée se fait comme suit:
|
La config de la base de donnée se fait comme suit:
|
||||||
|
|
||||||
a. On se connecte au shell de psql
|
a. On se connecte au shell de psql
|
||||||
|
|
||||||
$ sudo su - postgres
|
$ sudo su - postgres
|
||||||
$ psql
|
$ psql
|
||||||
|
|
||||||
b. On sécurise l'utilisateur postgres
|
b. On sécurise l'utilisateur postgres
|
||||||
|
|
||||||
postgres=# \password
|
postgres=# \password
|
||||||
Enter new password:
|
Enter new password:
|
||||||
|
|
||||||
Conservez ce mot de passe de la meme manière que tous les autres.
|
Conservez ce mot de passe de la meme manière que tous les autres.
|
||||||
|
|
||||||
c. On créer la basse de donnée, et l'utilisateur associé
|
c. On créer la basse de donnée, et l'utilisateur associé
|
||||||
|
|
||||||
postgres=# CREATE USER note WITH PASSWORD 'un_mot_de_passe_sur';
|
postgres=# CREATE USER note WITH PASSWORD 'un_mot_de_passe_sur';
|
||||||
CREATE ROLE
|
CREATE ROLE
|
||||||
postgres=# CREATE DATABASE note_db OWNER note;
|
postgres=# CREATE DATABASE note_db OWNER note;
|
||||||
CREATE DATABASE
|
CREATE DATABASE
|
||||||
|
|
||||||
Si tout va bien :
|
Si tout va bien :
|
||||||
|
|
||||||
postgres=#\list
|
postgres=#\list
|
||||||
List of databases
|
List of databases
|
||||||
Name | Owner | Encoding | Collate | Ctype | Access privileges
|
Name | Owner | Encoding | Collate | Ctype | Access privileges
|
||||||
|
@ -100,12 +103,12 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
|
||||||
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres
|
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres+postgres=CTc/postgres
|
||||||
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres
|
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres
|
||||||
(4 rows)
|
(4 rows)
|
||||||
|
|
||||||
6. Variable d'environnement et Migrations
|
6. Variable d'environnement et Migrations
|
||||||
|
|
||||||
On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet
|
On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet
|
||||||
et on renseigne des secrets et des paramètres :
|
et on renseigne des secrets et des paramètres :
|
||||||
|
|
||||||
DJANGO_APP_STAGE=dev # ou "prod"
|
DJANGO_APP_STAGE=dev # ou "prod"
|
||||||
DJANGO_DEV_STORE_METHOD=sqlite # ou "postgres"
|
DJANGO_DEV_STORE_METHOD=sqlite # ou "postgres"
|
||||||
DJANGO_DB_HOST=localhost
|
DJANGO_DB_HOST=localhost
|
||||||
|
@ -135,68 +138,67 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
|
||||||
(env)$ ./manage.py makemigrations
|
(env)$ ./manage.py makemigrations
|
||||||
(env)$ ./manage.py migrate
|
(env)$ ./manage.py migrate
|
||||||
|
|
||||||
7. Enjoy
|
7. *Enjoy \o/*
|
||||||
|
|
||||||
|
### Installation avec Docker
|
||||||
## Installer avec Docker
|
|
||||||
|
|
||||||
Il est possible de travailler sur une instance Docker.
|
Il est possible de travailler sur une instance Docker.
|
||||||
|
|
||||||
1. Cloner le dépôt là où vous voulez :
|
1. Cloner le dépôt là où vous voulez :
|
||||||
|
|
||||||
$ git clone git@gitlab.crans.org:bde/nk20.git
|
$ git clone git@gitlab.crans.org:bde/nk20.git
|
||||||
|
|
||||||
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
|
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
|
||||||
et mettez à jour vos variables d'environnement
|
et mettez à jour vos variables d'environnement
|
||||||
|
|
||||||
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
|
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
|
||||||
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
|
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
|
||||||
|
|
||||||
nk20:
|
nk20:
|
||||||
build: /chemin/vers/nk20
|
build: /chemin/vers/nk20
|
||||||
volumes:
|
volumes:
|
||||||
- /chemin/vers/nk20:/code/
|
- /chemin/vers/nk20:/code/
|
||||||
env_file: /chemin/vers/nk20/.env
|
env_file: /chemin/vers/nk20/.env
|
||||||
restart: always
|
restart: always
|
||||||
labels:
|
labels:
|
||||||
- traefik.domain=ndd.example.com
|
- traefik.domain=ndd.example.com
|
||||||
- traefik.frontend.rule=Host:ndd.example.com
|
- traefik.frontend.rule=Host:ndd.example.com
|
||||||
- traefik.port=8000
|
- traefik.port=8000
|
||||||
|
|
||||||
3. Enjoy :
|
4. Enjoy :
|
||||||
|
|
||||||
$ docker-compose up -d nk20
|
$ docker-compose up -d nk20
|
||||||
|
|
||||||
## Installer un serveur de développement
|
### Lancer un serveur de développement
|
||||||
|
|
||||||
Avec `./manage.py runserver` il est très rapide de mettre en place
|
Avec `./manage.py runserver` il est très rapide de mettre en place
|
||||||
un serveur de développement par exemple sur son ordinateur.
|
un serveur de développement par exemple sur son ordinateur.
|
||||||
|
|
||||||
1. Cloner le dépôt là où vous voulez :
|
1. Cloner le dépôt là où vous voulez :
|
||||||
|
|
||||||
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
|
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
|
||||||
|
|
||||||
2. Créer un environnement Python isolé
|
2. Créer un environnement Python isolé
|
||||||
pour ne pas interférer avec les versions de paquets systèmes :
|
pour ne pas interférer avec les versions de paquets systèmes :
|
||||||
|
|
||||||
$ python3 -m venv venv
|
$ python3 -m venv venv
|
||||||
$ source venv/bin/activate
|
$ source venv/bin/activate
|
||||||
(env)$ pip install -r requirements/base.txt
|
(env)$ pip install -r requirements/base.txt
|
||||||
|
|
||||||
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
|
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
|
||||||
ce qu'il faut
|
ce qu'il faut
|
||||||
|
|
||||||
4. Migrations et chargement des données initiales :
|
4. Migrations et chargement des données initiales :
|
||||||
|
|
||||||
(env)$ ./manage.py makemigrations
|
(env)$ ./manage.py makemigrations
|
||||||
(env)$ ./manage.py migrate
|
(env)$ ./manage.py migrate
|
||||||
(env)$ ./manage.py loaddata initial
|
(env)$ ./manage.py loaddata initial
|
||||||
|
|
||||||
5. Créer un super-utilisateur :
|
5. Créer un super-utilisateur :
|
||||||
|
|
||||||
(env)$ ./manage.py createsuperuser
|
(env)$ ./manage.py createsuperuser
|
||||||
|
|
||||||
6. Enjoy :
|
6. Enjoy :
|
||||||
|
|
||||||
(env)$ ./manage.py runserver 0.0.0.0:8000
|
(env)$ ./manage.py runserver 0.0.0.0:8000
|
||||||
|
|
||||||
|
@ -204,11 +206,27 @@ En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
|
||||||
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
||||||
de la note sur un téléphone !
|
de la note sur un téléphone !
|
||||||
|
|
||||||
## Cahier des Charges
|
|
||||||
|
|
||||||
Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
La documentation est générée par django et son module admindocs.
|
Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
|
||||||
|
|
||||||
|
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
|
||||||
**Commentez votre code !**
|
**Commentez votre code !**
|
||||||
|
|
||||||
|
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Regénérer les fichiers de traduction
|
||||||
|
|
||||||
|
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
django-admin makemessages -i env
|
||||||
|
```
|
||||||
|
|
||||||
|
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
|
||||||
|
|
||||||
|
```bash
|
||||||
|
django-admin compilemessages
|
||||||
|
```
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
@ -44,8 +44,9 @@ class UserTable(tables.Table):
|
||||||
|
|
||||||
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
|
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
|
||||||
|
|
||||||
def render_balance(self, value):
|
def render_balance(self, record, value):
|
||||||
return pretty_money(value)
|
return pretty_money(value)\
|
||||||
|
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
|
@ -105,8 +106,8 @@ class MembershipTable(tables.Table):
|
||||||
empty_membership = Membership(
|
empty_membership = Membership(
|
||||||
club=record.club,
|
club=record.club,
|
||||||
user=record.user,
|
user=record.user,
|
||||||
date_start=datetime.now().date(),
|
date_start=timezone.now().date(),
|
||||||
date_end=datetime.now().date(),
|
date_end=timezone.now().date(),
|
||||||
fee=0,
|
fee=0,
|
||||||
)
|
)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||||
|
@ -127,7 +128,7 @@ class MembershipTable(tables.Table):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'table table-condensed table-striped table-hover',
|
'class': 'table table-condensed table-striped',
|
||||||
'style': 'table-layout: fixed;'
|
'style': 'table-layout: fixed;'
|
||||||
}
|
}
|
||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load i18n static pretty_money %}
|
{% load i18n static pretty_money perms %}
|
||||||
|
|
||||||
<div class="card bg-light shadow">
|
<div class="card bg-light shadow">
|
||||||
<div class="card-header text-center" >
|
<div class="card-header text-center" >
|
||||||
|
@ -32,8 +32,10 @@
|
||||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
{% if "note.view_note"|has_perm:user_object.note %}
|
||||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-xl-6"> <a href="{% url 'member:user_alias' user_object.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
|
<dt class="col-xl-6"> <a href="{% url 'member:user_alias' user_object.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
|
||||||
<dd class="col-xl-6 text-truncate">{{ user_object.note.alias_set.all|join:", " }}</dd>
|
<dd class="col-xl-6 text-truncate">{{ user_object.note.alias_set.all|join:", " }}</dd>
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
"""
|
||||||
|
Test that login page still works
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateLoggedOutTests(TestCase):
|
||||||
|
def test_login_page(self):
|
||||||
|
response = self.client.get('/accounts/login/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateLoggedInTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admin",
|
||||||
|
password="adminadmin",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
def test_login_page(self):
|
||||||
|
response = self.client.get('/accounts/login/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_admin_index(self):
|
||||||
|
response = self.client.get('/admin/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_accounts_password_reset(self):
|
||||||
|
response = self.client.get('/accounts/password_reset/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_logout_page(self):
|
||||||
|
response = self.client.get('/accounts/logout/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_transfer_page(self):
|
||||||
|
response = self.client.get('/note/transfer/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_consos_page(self):
|
||||||
|
response = self.client.get('/note/consos/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
|
@ -244,12 +244,12 @@ class Alias(models.Model):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(string):
|
def normalize(string):
|
||||||
"""
|
"""
|
||||||
Normalizes a string: removes most diacritics and does casefolding
|
Normalizes a string: removes most diacritics, does casefolding and ignore non-ASCII characters
|
||||||
"""
|
"""
|
||||||
return ''.join(
|
return ''.join(
|
||||||
char for char in unicodedata.normalize('NFKD', string.casefold())
|
char for char in unicodedata.normalize('NFKD', string.casefold().replace('æ', 'ae').replace('œ', 'oe'))
|
||||||
if all(not unicodedata.category(char).startswith(cat)
|
if all(not unicodedata.category(char).startswith(cat)
|
||||||
for cat in {'M', 'P', 'Z', 'C'})).casefold()
|
for cat in {'M', 'P', 'Z', 'C'})).casefold().encode('ascii', 'ignore').decode('ascii')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
normalized_name = self.normalize(self.name)
|
normalized_name = self.normalize(self.name)
|
||||||
|
|
|
@ -19,8 +19,7 @@ from .templatetags.pretty_money import pretty_money
|
||||||
class HistoryTable(tables.Table):
|
class HistoryTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
'class':
|
'class': 'table table-condensed table-striped'
|
||||||
'table table-condensed table-striped table-hover'
|
|
||||||
}
|
}
|
||||||
model = Transaction
|
model = Transaction
|
||||||
exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
|
exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
|
||||||
|
@ -123,7 +122,7 @@ DELETE_TEMPLATE = """
|
||||||
class AliasTable(tables.Table):
|
class AliasTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'table table condensed table-striped table-hover',
|
'class': 'table table condensed table-striped',
|
||||||
'id': "alias_table"
|
'id': "alias_table"
|
||||||
}
|
}
|
||||||
model = Alias
|
model = Alias
|
||||||
|
@ -142,8 +141,7 @@ class AliasTable(tables.Table):
|
||||||
class ButtonTable(tables.Table):
|
class ButtonTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
'class':
|
'class': 'table table-bordered condensed'
|
||||||
'table table-bordered condensed table-hover'
|
|
||||||
}
|
}
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
||||||
|
|
|
@ -287,7 +287,7 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]",
|
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"source__balance__gte\": {\"F\": [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]]}}, {\"valid\": false}]]",
|
||||||
"type": "add",
|
"type": "add",
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
|
@ -319,7 +319,7 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
|
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]]",
|
||||||
"type": "add",
|
"type": "add",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
|
@ -335,7 +335,7 @@
|
||||||
"note",
|
"note",
|
||||||
"recurrenttransaction"
|
"recurrenttransaction"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
|
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]]",
|
||||||
"type": "add",
|
"type": "add",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
|
@ -1572,7 +1572,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "emergency_contact_phone",
|
"field": "emergency_contact_phone",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier le nom du contact en cas d'urgence de mon inscription WEI"
|
"description": "Modifier le téléphone du contact en cas d'urgence de mon inscription WEI"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1983,7 +1983,7 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]",
|
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]]",
|
||||||
"type": "change",
|
"type": "change",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "valid",
|
"field": "valid",
|
||||||
|
@ -2079,9 +2079,9 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]",
|
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]]",
|
||||||
"type": "change",
|
"type": "change",
|
||||||
"mask": 1,
|
"mask": 2,
|
||||||
"field": "invalidity_reason",
|
"field": "invalidity_reason",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier la raison d'invalidité d'une transaction de club"
|
"description": "Modifier la raison d'invalidité d'une transaction de club"
|
||||||
|
@ -2910,7 +2910,8 @@
|
||||||
140,
|
140,
|
||||||
145,
|
145,
|
||||||
146,
|
146,
|
||||||
147
|
147,
|
||||||
|
150
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -129,7 +129,8 @@ class WEIMembershipForm(forms.ModelForm):
|
||||||
attrs={
|
attrs={
|
||||||
'api_url': '/api/wei/team/',
|
'api_url': '/api/wei/team/',
|
||||||
'placeholder': 'Équipe ...',
|
'placeholder': 'Équipe ...',
|
||||||
}
|
},
|
||||||
|
resetable=True,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ class WEISurveyForm2020(forms.Form):
|
||||||
words = [choice(WORDS) for _ in range(10)]
|
words = [choice(WORDS) for _ in range(10)]
|
||||||
words = [(w, w) for w in words]
|
words = [(w, w) for w in words]
|
||||||
if self.data:
|
if self.data:
|
||||||
self.fields["word"].choices = WORDS
|
self.fields["word"].choices = [(w, w) for w in WORDS]
|
||||||
if self.is_valid():
|
if self.is_valid():
|
||||||
return
|
return
|
||||||
self.fields["word"].choices = words
|
self.fields["word"].choices = words
|
||||||
|
|
|
@ -160,7 +160,8 @@
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
{% with pretty_fee=fee|pretty_money %}
|
{% with pretty_fee=fee|pretty_money %}
|
||||||
{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
|
{% blocktrans trimmed with balance=registration.user.note.balance|pretty_money %}
|
||||||
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required). The registration may fail.
|
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required).
|
||||||
|
The registration may fail if you don't credit the note now.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,27 +16,29 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script>
|
{% if not object.membership %}
|
||||||
$(document).ready(function() {
|
<script>
|
||||||
function refreshTeams() {
|
$(document).ready(function() {
|
||||||
let buses = [];
|
function refreshTeams() {
|
||||||
$("input[name='bus']:checked").each(function(ignored) {
|
let buses = [];
|
||||||
buses.push($(this).parent().text().trim());
|
$("input[name='bus']:checked").each(function(ignored) {
|
||||||
});
|
buses.push($(this).parent().text().trim());
|
||||||
console.log(buses);
|
|
||||||
$("input[name='team']").each(function() {
|
|
||||||
let label = $(this).parent();
|
|
||||||
$(this).parent().addClass('d-none');
|
|
||||||
buses.forEach(function(bus) {
|
|
||||||
if (label.text().includes(bus))
|
|
||||||
label.removeClass('d-none');
|
|
||||||
});
|
});
|
||||||
});
|
console.log(buses);
|
||||||
}
|
$("input[name='team']").each(function() {
|
||||||
|
let label = $(this).parent();
|
||||||
|
$(this).parent().addClass('d-none');
|
||||||
|
buses.forEach(function(bus) {
|
||||||
|
if (label.text().includes(bus))
|
||||||
|
label.removeClass('d-none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$("input[name='bus']").change(refreshTeams);
|
$("input[name='bus']").change(refreshTeams);
|
||||||
|
|
||||||
refreshTeams();
|
refreshTeams();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,419 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from member.models import Membership
|
||||||
|
from note.models import NoteClub
|
||||||
|
|
||||||
|
from ..forms import CurrentSurvey
|
||||||
|
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
|
||||||
|
|
||||||
|
|
||||||
|
class TestWEIRegistration(TestCase):
|
||||||
|
"""
|
||||||
|
Test the whole WEI app
|
||||||
|
"""
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Setup the database with initial data
|
||||||
|
Create a new user, a new WEI, bus, team, registration
|
||||||
|
"""
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="weiadmin",
|
||||||
|
password="admin",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.year = timezone.now().year
|
||||||
|
self.wei = WEIClub.objects.create(
|
||||||
|
name="Test WEI",
|
||||||
|
email="gc.wei@example.com",
|
||||||
|
parent_club_id=2,
|
||||||
|
membership_fee_paid=12500,
|
||||||
|
membership_fee_unpaid=5500,
|
||||||
|
membership_start=str(self.year) + "-08-01",
|
||||||
|
membership_end=str(self.year) + "-12-31",
|
||||||
|
year=self.year,
|
||||||
|
date_start=str(self.year) + "-09-01",
|
||||||
|
date_end=str(self.year) + "-09-03",
|
||||||
|
)
|
||||||
|
NoteClub.objects.create(club=self.wei)
|
||||||
|
self.bus = Bus.objects.create(
|
||||||
|
name="Test Bus",
|
||||||
|
wei=self.wei,
|
||||||
|
description="Test Bus",
|
||||||
|
)
|
||||||
|
self.team = BusTeam.objects.create(
|
||||||
|
name="Test Team",
|
||||||
|
bus=self.bus,
|
||||||
|
color=0xFFFFFF,
|
||||||
|
description="Test Team",
|
||||||
|
)
|
||||||
|
self.registration = WEIRegistration.objects.create(
|
||||||
|
user_id=self.user.id,
|
||||||
|
wei_id=self.wei.id,
|
||||||
|
soge_credit=True,
|
||||||
|
caution_check=True,
|
||||||
|
birth_date="2000-01-01",
|
||||||
|
gender="nonbinary",
|
||||||
|
clothing_cut="male",
|
||||||
|
clothing_size="XL",
|
||||||
|
health_issues="I am a bot",
|
||||||
|
emergency_contact_name="Pikachu",
|
||||||
|
emergency_contact_phone="+33123456789",
|
||||||
|
ml_events_registration=True,
|
||||||
|
ml_sport_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
first_year=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_wei(self):
|
||||||
|
"""
|
||||||
|
Test creating a new WEI club.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:wei_create"), dict(
|
||||||
|
name="Create WEI Test",
|
||||||
|
email="gc.wei@example.com",
|
||||||
|
membership_fee_paid=12500,
|
||||||
|
membership_fee_unpaid=5500,
|
||||||
|
membership_start=str(self.year + 1) + "-08-01",
|
||||||
|
membership_end=str(self.year + 1) + "-09-30",
|
||||||
|
year=self.year + 1,
|
||||||
|
date_start=str(self.year + 1) + "-09-01",
|
||||||
|
date_end=str(self.year + 1) + "-09-03",
|
||||||
|
))
|
||||||
|
qs = WEIClub.objects.filter(name="Create WEI Test", year=self.year + 1)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
wei = qs.get()
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=wei.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_wei_detail(self):
|
||||||
|
"""
|
||||||
|
Test display the information about the default WEI.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_current_wei_detail(self):
|
||||||
|
"""
|
||||||
|
Test display the information about the current WEI.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:current_wei_detail"))
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_update_wei(self):
|
||||||
|
"""
|
||||||
|
Test update the information about the default WEI.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:wei_update", kwargs=dict(pk=self.wei.pk)), dict(
|
||||||
|
name="Update WEI Test",
|
||||||
|
year=2000,
|
||||||
|
email="wei-updated@example.com",
|
||||||
|
membership_fee_paid=0,
|
||||||
|
membership_fee_unpaid=0,
|
||||||
|
membership_start="2000-08-01",
|
||||||
|
membership_end="2000-09-30",
|
||||||
|
date_start="2000-09-01",
|
||||||
|
date_end="2000-09-03",
|
||||||
|
))
|
||||||
|
qs = WEIClub.objects.filter(name="Update WEI Test", id=self.wei.id)
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
|
||||||
|
def test_wei_closed(self):
|
||||||
|
"""
|
||||||
|
Test display the page when a WEI is closed.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_closed", kwargs=dict(pk=self.wei.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_wei_list(self):
|
||||||
|
"""
|
||||||
|
Test display the list of all WEI.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_add_bus(self):
|
||||||
|
"""
|
||||||
|
Test create a new bus.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:add_bus", kwargs=dict(pk=self.wei.pk)), dict(
|
||||||
|
wei=self.wei.id,
|
||||||
|
name="Create Bus Test",
|
||||||
|
description="This bus was created.",
|
||||||
|
))
|
||||||
|
qs = Bus.objects.filter(name="Create Bus Test")
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
bus = qs.get()
|
||||||
|
self.assertRedirects(response, reverse("wei:manage_bus", kwargs=dict(pk=bus.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_detail_bus(self):
|
||||||
|
"""
|
||||||
|
Test display the information about a bus.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:manage_bus", kwargs=dict(pk=self.bus.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_update_bus(self):
|
||||||
|
"""
|
||||||
|
Test update a bus.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:update_bus", kwargs=dict(pk=self.bus.pk)), dict(
|
||||||
|
name="Update Bus Test",
|
||||||
|
description="This bus was updated.",
|
||||||
|
))
|
||||||
|
qs = Bus.objects.filter(name="Update Bus Test", id=self.bus.id)
|
||||||
|
self.assertRedirects(response, reverse("wei:manage_bus", kwargs=dict(pk=self.bus.pk)), 302, 200)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
|
||||||
|
def test_add_team(self):
|
||||||
|
"""
|
||||||
|
Test create a new team.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:add_team", kwargs=dict(pk=self.bus.pk)), dict(
|
||||||
|
bus=self.bus.id,
|
||||||
|
name="Create Team Test",
|
||||||
|
color="#2A",
|
||||||
|
description="This team was created.",
|
||||||
|
))
|
||||||
|
qs = BusTeam.objects.filter(name="Create Team Test", color=42)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
team = qs.get()
|
||||||
|
self.assertRedirects(response, reverse("wei:manage_bus_team", kwargs=dict(pk=team.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_detail_team(self):
|
||||||
|
"""
|
||||||
|
Test display the detail about a team.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:manage_bus_team", kwargs=dict(pk=self.team.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_update_team(self):
|
||||||
|
"""
|
||||||
|
Test update a team.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:update_bus_team", kwargs=dict(pk=self.team.pk)), dict(
|
||||||
|
name="Update Team Test",
|
||||||
|
color="#A6AA",
|
||||||
|
description="This team was updated.",
|
||||||
|
))
|
||||||
|
qs = BusTeam.objects.filter(name="Update Team Test", color=42666, id=self.team.id)
|
||||||
|
self.assertRedirects(response, reverse("wei:manage_bus_team", kwargs=dict(pk=self.team.pk)), 302, 200)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
|
||||||
|
def test_register_2a(self):
|
||||||
|
"""
|
||||||
|
Test register a new 2A+ to the WEI.
|
||||||
|
"""
|
||||||
|
user = User.objects.create(username="toto", email="toto@example.com")
|
||||||
|
response = self.client.post(reverse("wei:wei_register_2A", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||||
|
user=user.id,
|
||||||
|
soge_credit=True,
|
||||||
|
birth_date='2000-01-01',
|
||||||
|
gender='nonbinary',
|
||||||
|
clothing_cut='female',
|
||||||
|
clothing_size='XS',
|
||||||
|
health_issues='I am a bot',
|
||||||
|
emergency_contact_name='NoteKfet2020',
|
||||||
|
emergency_contact_phone='+33123456789',
|
||||||
|
bus=[self.bus.id],
|
||||||
|
team=[self.team.id],
|
||||||
|
roles=[role.id for role in WEIRole.objects.filter(~Q(name="1A")).all()],
|
||||||
|
))
|
||||||
|
qs = WEIRegistration.objects.filter(user_id=user.id)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=qs.get().pk)), 302, 302)
|
||||||
|
|
||||||
|
def test_register_1a(self):
|
||||||
|
"""
|
||||||
|
Test register a first year member to the WEI and complete the survey.
|
||||||
|
"""
|
||||||
|
user = User.objects.create(username="toto", email="toto@example.com")
|
||||||
|
response = self.client.post(reverse("wei:wei_register_1A_myself", kwargs=dict(wei_pk=self.wei.pk)), dict(
|
||||||
|
user=user.id,
|
||||||
|
soge_credit=True,
|
||||||
|
birth_date='2000-01-01',
|
||||||
|
gender='nonbinary',
|
||||||
|
clothing_cut='female',
|
||||||
|
clothing_size='XS',
|
||||||
|
health_issues='I am a bot',
|
||||||
|
emergency_contact_name='NoteKfet2020',
|
||||||
|
emergency_contact_phone='+33123456789',
|
||||||
|
ml_events_registration=True,
|
||||||
|
ml_sport_registration=False,
|
||||||
|
ml_art_registration=False,
|
||||||
|
))
|
||||||
|
qs = WEIRegistration.objects.filter(user_id=user.id)
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
registration = qs.get()
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
|
||||||
|
for i in range(1, 21):
|
||||||
|
# Fill 1A Survey, 20 pages
|
||||||
|
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), dict(
|
||||||
|
word="Jus de fruit",
|
||||||
|
))
|
||||||
|
registration.refresh_from_db()
|
||||||
|
survey = CurrentSurvey(registration)
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
|
||||||
|
302 if survey.is_complete() else 200)
|
||||||
|
self.assertIsNotNone(getattr(survey.information, "word" + str(i)), "Survey page #" + str(i) + " failed")
|
||||||
|
survey = CurrentSurvey(registration)
|
||||||
|
self.assertTrue(survey.is_complete())
|
||||||
|
|
||||||
|
def test_wei_survey_ended(self):
|
||||||
|
"""
|
||||||
|
Test display the end page of a survey.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_survey_end", kwargs=dict(pk=self.registration.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_update_registration(self):
|
||||||
|
self.registration.information = dict(
|
||||||
|
preferred_bus_pk=[],
|
||||||
|
preferred_team_pk=[],
|
||||||
|
preferred_roles_pk=[]
|
||||||
|
)
|
||||||
|
self.registration.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)),
|
||||||
|
dict(
|
||||||
|
user=self.user.id,
|
||||||
|
soge_credit=False,
|
||||||
|
birth_date='2020-01-01',
|
||||||
|
gender='female',
|
||||||
|
clothing_cut='male',
|
||||||
|
clothing_size='M',
|
||||||
|
health_issues='I am really a bot',
|
||||||
|
emergency_contact_name='Note Kfet 2020',
|
||||||
|
emergency_contact_phone='+33600000000',
|
||||||
|
bus=[self.bus.id],
|
||||||
|
team=[self.team.id],
|
||||||
|
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent WEI").all()],
|
||||||
|
information_json=self.registration.information_json,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qs = WEIRegistration.objects.filter(user_id=self.user.id, soge_credit=False, clothing_size="M")
|
||||||
|
self.assertTrue(qs.exists())
|
||||||
|
self.assertRedirects(response, reverse("wei:validate_registration", kwargs=dict(pk=qs.get().pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_delete_registration(self):
|
||||||
|
"""
|
||||||
|
Test delete a WEI registration.
|
||||||
|
"""
|
||||||
|
response = self.client.delete(reverse("wei:wei_delete_registration", kwargs=dict(pk=self.registration.pk)))
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.wei.pk)), 302, 200)
|
||||||
|
|
||||||
|
def test_validate_membership(self):
|
||||||
|
"""
|
||||||
|
Test validate a membership.
|
||||||
|
"""
|
||||||
|
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
||||||
|
roles=[WEIRole.objects.get(name="GC WEI").id],
|
||||||
|
bus=self.bus.pk,
|
||||||
|
team=self.team.pk,
|
||||||
|
credit_type=4, # Bank transfer
|
||||||
|
credit_amount=420,
|
||||||
|
last_name="admin",
|
||||||
|
first_name="admin",
|
||||||
|
bank="Société générale",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
|
||||||
|
# Check if the membership is successfully created
|
||||||
|
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
|
||||||
|
self.assertTrue(membership.exists())
|
||||||
|
membership = membership.get()
|
||||||
|
# Check if the user is member of the Kfet club and the BDE
|
||||||
|
kfet_membership = Membership.objects.filter(user_id=self.user.id, club__name="Kfet")
|
||||||
|
self.assertTrue(kfet_membership.exists())
|
||||||
|
kfet_membership = kfet_membership.get()
|
||||||
|
bde_membership = Membership.objects.filter(user_id=self.user.id, club__name="BDE")
|
||||||
|
self.assertTrue(bde_membership.exists())
|
||||||
|
bde_membership = bde_membership.get()
|
||||||
|
|
||||||
|
if "treasury" in settings.INSTALLED_APPS:
|
||||||
|
# The registration is made with the Société générale. Ensure that all is fine
|
||||||
|
from treasury.models import SogeCredit
|
||||||
|
soge_credit = SogeCredit.objects.filter(user_id=self.user.id)
|
||||||
|
self.assertTrue(soge_credit.exists())
|
||||||
|
soge_credit = soge_credit.get()
|
||||||
|
self.assertTrue(membership.transaction in soge_credit.transactions.all())
|
||||||
|
self.assertTrue(kfet_membership.transaction in soge_credit.transactions.all())
|
||||||
|
self.assertTrue(bde_membership.transaction in soge_credit.transactions.all())
|
||||||
|
self.assertFalse(membership.transaction.valid)
|
||||||
|
self.assertFalse(kfet_membership.transaction.valid)
|
||||||
|
self.assertFalse(bde_membership.transaction.valid)
|
||||||
|
|
||||||
|
def test_registrations_list(self):
|
||||||
|
"""
|
||||||
|
Test display the registration list
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_registrations", kwargs=dict(pk=self.wei.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_memberships_list(self):
|
||||||
|
"""
|
||||||
|
Test display the memberships list
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("wei:wei_memberships", kwargs=dict(pk=self.wei.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def is_latex_installed(self):
|
||||||
|
"""
|
||||||
|
Check if LaTeX is installed in the machine. Don't check pages that generate a PDF file if LaTeX is not
|
||||||
|
installed, like in Gitlab.
|
||||||
|
"""
|
||||||
|
return subprocess.call(
|
||||||
|
["which", "pdflatex"],
|
||||||
|
stdout=open('/dev/null', 'wb'),
|
||||||
|
stderr=open('/dev/null', 'wb'),
|
||||||
|
) == 0
|
||||||
|
|
||||||
|
def test_memberships_pdf_list(self):
|
||||||
|
"""
|
||||||
|
Test display the membership list as a PDF file
|
||||||
|
"""
|
||||||
|
if not self.is_latex_installed():
|
||||||
|
return
|
||||||
|
|
||||||
|
response = self.client.get(reverse("wei:wei_memberships_pdf", kwargs=dict(wei_pk=self.wei.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response["content-type"], "application/pdf")
|
||||||
|
|
||||||
|
def test_bus_memberships_pdf_list(self):
|
||||||
|
"""
|
||||||
|
Test display the membership list of a bus as a PDF file
|
||||||
|
"""
|
||||||
|
if not self.is_latex_installed():
|
||||||
|
return
|
||||||
|
|
||||||
|
response = self.client.get(reverse("wei:wei_memberships_bus_pdf", kwargs=dict(wei_pk=self.wei.pk,
|
||||||
|
bus_pk=self.bus.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response["content-type"], "application/pdf")
|
||||||
|
|
||||||
|
def test_team_memberships_pdf_list(self):
|
||||||
|
"""
|
||||||
|
Test display the membership list of a bus team as a PDF file
|
||||||
|
"""
|
||||||
|
if not self.is_latex_installed():
|
||||||
|
return
|
||||||
|
|
||||||
|
response = self.client.get(reverse("wei:wei_memberships_team_pdf", kwargs=dict(wei_pk=self.wei.pk,
|
||||||
|
bus_pk=self.bus.pk,
|
||||||
|
team_pk=self.team.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response["content-type"], "application/pdf")
|
|
@ -391,7 +391,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
self.object.refresh_from_db()
|
self.object.refresh_from_db()
|
||||||
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.pk})
|
return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
@ -526,7 +526,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
if "myself" in self.request.path:
|
if "myself" in self.request.path:
|
||||||
context["form"].fields["user"].disabled = True
|
context["form"].fields["user"].disabled = True
|
||||||
|
|
||||||
choose_bus_form = WEIChooseBusForm()
|
choose_bus_form = WEIChooseBusForm(self.request.POST if self.request.POST else None)
|
||||||
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]).order_by('name')
|
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]).order_by('name')
|
||||||
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\
|
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\
|
||||||
.order_by('bus__name', 'name')
|
.order_by('bus__name', 'name')
|
||||||
|
@ -602,16 +602,22 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||||
context["club"] = self.object.wei
|
context["club"] = self.object.wei
|
||||||
|
|
||||||
if self.object.is_validated:
|
if self.object.is_validated:
|
||||||
membership_form = WEIMembershipForm(instance=self.object.membership)
|
membership_form = WEIMembershipForm(instance=self.object.membership,
|
||||||
|
data=self.request.POST if self.request.POST else None)
|
||||||
for field_name, field in membership_form.fields.items():
|
for field_name, field in membership_form.fields.items():
|
||||||
if not PermissionBackend.check_perm(
|
if not PermissionBackend.check_perm(
|
||||||
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
|
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
|
||||||
field.widget = HiddenInput()
|
field.widget = HiddenInput()
|
||||||
|
del membership_form.fields["credit_type"]
|
||||||
|
del membership_form.fields["credit_amount"]
|
||||||
|
del membership_form.fields["first_name"]
|
||||||
|
del membership_form.fields["last_name"]
|
||||||
|
del membership_form.fields["bank"]
|
||||||
context["membership_form"] = membership_form
|
context["membership_form"] = membership_form
|
||||||
elif not self.object.first_year and PermissionBackend.check_perm(
|
elif not self.object.first_year and PermissionBackend.check_perm(
|
||||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
||||||
choose_bus_form = WEIChooseBusForm(
|
choose_bus_form = WEIChooseBusForm(
|
||||||
dict(
|
self.request.POST if self.request.POST else dict(
|
||||||
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
||||||
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
|
team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(),
|
||||||
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
|
roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(),
|
||||||
|
@ -855,7 +861,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea
|
||||||
form.add_error('bus', _("This user didn't give her/his caution check."))
|
form.add_error('bus', _("This user didn't give her/his caution check."))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
if not registration.soge_credit and user.note.balance < fee + credit_amount:
|
if not registration.soge_credit and user.note.balance + credit_amount < fee:
|
||||||
# Users must have money before registering to the WEI.
|
# Users must have money before registering to the WEI.
|
||||||
form.add_error('bus',
|
form.add_error('bus',
|
||||||
_("This user don't have enough money to join this club, and can't have a negative balance."))
|
_("This user don't have enough money to join this club, and can't have a negative balance."))
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2
tox.ini
2
tox.ini
|
@ -19,7 +19,7 @@ deps =
|
||||||
coverage
|
coverage
|
||||||
commands =
|
commands =
|
||||||
./manage.py makemigrations
|
./manage.py makemigrations
|
||||||
coverage run --omit='*migrations*' --source=apps,note_kfet ./manage.py test
|
coverage run --omit='*migrations*' --source=apps,note_kfet ./manage.py test apps/
|
||||||
coverage report -m
|
coverage report -m
|
||||||
|
|
||||||
[testenv:linters]
|
[testenv:linters]
|
||||||
|
|
Loading…
Reference in New Issue