Compare commits

...

14 Commits

Author SHA1 Message Date
Yohann D'ANELLO 25e26fe8cf Don't test LaTeX pages if LaTeX is not installed 2020-08-10 23:29:11 +02:00
Yohann D'ANELLO 0fae5b3e62 Create tests for the WEI app 2020-08-10 23:18:40 +02:00
Yohann D'ANELLO 3784e97d60 Hide the credit interface when editing a WEI registration 2020-08-10 20:09:49 +02:00
Yohann D'ANELLO 6567d2f8cc When an user is registering to the WEI, it doesn't pay the membership + the credit amount. The credit amount is deducted instead 2020-08-10 19:59:01 +02:00
Yohann D'ANELLO 999cc0a6b2 Tesdt login page 2020-08-10 19:36:04 +02:00
Alexandre Iooss 60de58b78a Execute tests from apps/ 2020-08-10 18:43:50 +02:00
Yohann D'ANELLO 9c816a288d Stronger alias normalisation, ensure that normalized strings are encoded in ASCII. Closes #52 2020-08-10 18:36:47 +02:00
Alexandre Iooss c277d8bccd Fix CI broken link in README 2020-08-10 18:27:45 +02:00
Alexandre Iooss 4a4c3d33b0 Format README and fix link 2020-08-10 18:26:45 +02:00
Alexandre Iooss 9c679d5bc9 Regen locales 2020-08-10 18:12:59 +02:00
Alexandre Iooss 3b49b7f4c1 Add how to translate in README 2020-08-10 18:12:50 +02:00
Alexandre Iooss 747a878cca Do not hover table when not clickable 2020-08-10 18:01:39 +02:00
Yohann D'ANELLO c612e159cf See user information does not imply see the note balance 2020-08-10 16:32:45 +02:00
Yohann D'ANELLO 1b84c8c603 🐛 The balance must be greater than the *total* amount of a transaction, not the unit price 2020-08-10 16:05:50 +02:00
17 changed files with 2820 additions and 2234 deletions

200
README.md
View File

@ -1,96 +1,99 @@
# NoteKfet 2020
[![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)
## 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
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
### Installation avec Debian/Ubuntu
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/
$ 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
2. **Clonage du dépot** dans `/var/www/note_kfet`,
À 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
$ 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
3. **Création d'un environment de travail Python décorrélé du système.**
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):
$ 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/
```bash
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/
```
Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`.
5. Base de données
5. **Base de données.** En production on utilise PostgreSQL.
En prod on utilise postgresql.
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
(env)$ pip3 install psycopg2
La config de la base de donnée se fait comme suit:
a. On se connecte au shell de psql
$ sudo su - postgres
$ psql
b. On sécurise l'utilisateur postgres
postgres=# \password
Enter new password:
Conservez ce mot de passe de la meme manière que tous les autres.
c. On créer la basse de donnée, et l'utilisateur associé
postgres=# CREATE USER note WITH PASSWORD 'un_mot_de_passe_sur';
CREATE ROLE
postgres=# CREATE DATABASE note_db OWNER note;
CREATE DATABASE
Si tout va bien :
postgres=#\list
List of databases
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
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | =c/postgres +postgres=CTc/postgres
(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
et on renseigne des secrets et des paramètres :
DJANGO_APP_STAGE=dev # ou "prod"
DJANGO_DEV_STORE_METHOD=sqlite # ou "postgres"
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 migrate
7. Enjoy
7. *Enjoy \o/*
## Installer avec Docker
### Installation avec 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
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
et mettez à jour vos variables d'environnement
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
et mettez à jour vos variables d'environnement
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
nk20:
build: /chemin/vers/nk20
volumes:
- /chemin/vers/nk20:/code/
env_file: /chemin/vers/nk20/.env
restart: always
labels:
- traefik.domain=ndd.example.com
- traefik.frontend.rule=Host:ndd.example.com
- traefik.port=8000
nk20:
build: /chemin/vers/nk20
volumes:
- /chemin/vers/nk20:/code/
env_file: /chemin/vers/nk20/.env
restart: always
labels:
- traefik.domain=ndd.example.com
- traefik.frontend.rule=Host:ndd.example.com
- traefik.port=8000
3. Enjoy :
4. Enjoy :
$ 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
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
2. Créer un environnement Python isolé
pour ne pas interférer avec les versions de paquets systèmes :
2. Créer un environnement Python isolé
pour ne pas interférer avec les versions de paquets systèmes :
$ python3 -m venv venv
$ source venv/bin/activate
(env)$ pip install -r requirements/base.txt
$ python3 -m venv venv
$ source venv/bin/activate
(env)$ pip install -r requirements/base.txt
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
ce qu'il faut
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
ce qu'il faut
4. Migrations et chargement des données initiales :
4. Migrations et chargement des données initiales :
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial
5. Créer un super-utilisateur :
5. Créer un super-utilisateur :
(env)$ ./manage.py createsuperuser
6. Enjoy :
6. Enjoy :
(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
de la note sur un téléphone !
## Cahier des Charges
Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
## Documentation
La documentation est générée par django et son module admindocs.
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 !**
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
```

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
import django_tables2 as tables
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.utils.html import format_html
@ -44,8 +44,9 @@ class UserTable(tables.Table):
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
def render_balance(self, value):
return pretty_money(value)
def render_balance(self, record, value):
return pretty_money(value)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
class Meta:
attrs = {
@ -105,8 +106,8 @@ class MembershipTable(tables.Table):
empty_membership = Membership(
club=record.club,
user=record.user,
date_start=datetime.now().date(),
date_end=datetime.now().date(),
date_start=timezone.now().date(),
date_end=timezone.now().date(),
fee=0,
)
if PermissionBackend.check_perm(get_current_authenticated_user(),
@ -127,7 +128,7 @@ class MembershipTable(tables.Table):
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover',
'class': 'table table-condensed table-striped',
'style': 'table-layout: fixed;'
}
template_name = 'django_tables2/bootstrap4.html'

View File

@ -1,4 +1,4 @@
{% load i18n static pretty_money %}
{% load i18n static pretty_money perms %}
<div class="card bg-light shadow">
<div class="card-header text-center" >
@ -32,8 +32,10 @@
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
{% if "note.view_note"|has_perm:user_object.note %}
<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>
<dd class="col-xl-6 text-truncate">{{ user_object.note.alias_set.all|join:", " }}</dd>

View File

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

View File

@ -244,12 +244,12 @@ class Alias(models.Model):
@staticmethod
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(
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)
for cat in {'M', 'P', 'Z', 'C'})).casefold()
for cat in {'M', 'P', 'Z', 'C'})).casefold().encode('ascii', 'ignore').decode('ascii')
def clean(self):
normalized_name = self.normalize(self.name)

View File

@ -19,8 +19,7 @@ from .templatetags.pretty_money import pretty_money
class HistoryTable(tables.Table):
class Meta:
attrs = {
'class':
'table table-condensed table-striped table-hover'
'class': 'table table-condensed table-striped'
}
model = Transaction
exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
@ -123,7 +122,7 @@ DELETE_TEMPLATE = """
class AliasTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped table-hover',
'class': 'table table condensed table-striped',
'id': "alias_table"
}
model = Alias
@ -142,8 +141,7 @@ class AliasTable(tables.Table):
class ButtonTable(tables.Table):
class Meta:
attrs = {
'class':
'table table-bordered condensed table-hover'
'class': 'table table-bordered condensed'
}
row_attrs = {
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),

View File

@ -287,7 +287,7 @@
"note",
"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",
"mask": 1,
"field": "",
@ -319,7 +319,7 @@
"note",
"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",
"mask": 2,
"field": "",
@ -335,7 +335,7 @@
"note",
"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",
"mask": 2,
"field": "",
@ -1572,7 +1572,7 @@
"mask": 1,
"field": "emergency_contact_phone",
"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",
"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",
"mask": 2,
"field": "valid",
@ -2079,9 +2079,9 @@
"note",
"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",
"mask": 1,
"mask": 2,
"field": "invalidity_reason",
"permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction de club"
@ -2910,7 +2910,8 @@
140,
145,
146,
147
147,
150
]
}
},

View File

@ -129,7 +129,8 @@ class WEIMembershipForm(forms.ModelForm):
attrs={
'api_url': '/api/wei/team/',
'placeholder': 'Équipe ...',
}
},
resetable=True,
),
}

View File

@ -37,7 +37,7 @@ class WEISurveyForm2020(forms.Form):
words = [choice(WORDS) for _ in range(10)]
words = [(w, w) for w in words]
if self.data:
self.fields["word"].choices = WORDS
self.fields["word"].choices = [(w, w) for w in WORDS]
if self.is_valid():
return
self.fields["word"].choices = words

View File

@ -160,7 +160,8 @@
<div class="alert alert-danger">
{% with pretty_fee=fee|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 %}
{% endwith %}
</div>

View File

@ -16,27 +16,29 @@
{% endblock %}
{% block extrajavascript %}
<script>
$(document).ready(function() {
function refreshTeams() {
let buses = [];
$("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');
{% if not object.membership %}
<script>
$(document).ready(function() {
function refreshTeams() {
let buses = [];
$("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');
});
});
}
$("input[name='bus']").change(refreshTeams);
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
refreshTeams();
});
</script>
{% endif %}
{% endblock %}

View File

View File

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

View File

@ -391,7 +391,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
def get_success_url(self):
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):
@ -526,7 +526,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
if "myself" in self.request.path:
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["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\
.order_by('bus__name', 'name')
@ -602,16 +602,22 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
context["club"] = self.object.wei
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():
if not PermissionBackend.check_perm(
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
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
elif not self.object.first_year and PermissionBackend.check_perm(
self.request.user, "wei.change_weiregistration_information_json", self.object):
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(),
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(),
@ -855,7 +861,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea
form.add_error('bus', _("This user didn't give her/his caution check."))
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.
form.add_error('bus',
_("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

View File

@ -19,7 +19,7 @@ deps =
coverage
commands =
./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
[testenv:linters]