Compare commits

..

No commits in common. "25e26fe8cf25dd71f56ec65c2a144014bed83437" and "3a52af33a263b7a647eaef02f823b7071a38e355" have entirely different histories.

17 changed files with 2232 additions and 2818 deletions

108
README.md
View File

@ -1,72 +1,69 @@
# 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/commits/master)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/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 une installation de Debian Buster ou Ubuntu 20.04 fraîche ou bien configuré.
On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré.
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.
1. Paquets nécessaires
### Installation avec Debian/Ubuntu
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
1. **Installation des dépendances APT.**
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante :
```bash
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi 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,
```bash
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
```
2. **Clonage du dépot** dans `/var/www/note_kfet`,
2. Clonage du dépot
```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 .
```
on se met au bon endroit :
3. **Création d'un environment de travail Python décorrélé du système.**
$ 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
À la racine du projet:
```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
```
(env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite un base postgres
(env)$ deactivate
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_**
4. uwsgi et Nginx
Un exemple de conf est disponible :
```bash
$ cp nginx_note.conf_example nginx_note.conf
***Modifier le fichier pour être en accord avec le reste de votre config***
On utilise uwsgi et Nginx pour gérer le coté serveur :
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
```
Si l'on a un emperor (plusieurs instance uwsgi):
```bash
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
```
Sinon si on est dans le cas habituel :
Sinon:
```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.** En production on utilise PostgreSQL.
5. Base de données
En prod on utilise postgresql.
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
(env)$ pip3 install psycopg2
@ -138,9 +135,10 @@ Sinon vous pouvez suivre les étapes ici.
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
7. *Enjoy \o/*
7. Enjoy
### Installation avec Docker
## Installer avec Docker
Il est possible de travailler sur une instance Docker.
@ -149,7 +147,7 @@ Il est possible de travailler sur une instance Docker.
$ 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
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 :
@ -165,11 +163,11 @@ Il est possible de travailler sur une instance Docker.
- traefik.frontend.rule=Host:ndd.example.com
- traefik.port=8000
4. Enjoy :
3. Enjoy :
$ docker-compose up -d nk20
### Lancer un serveur de développement
## Installer un serveur de développement
Avec `./manage.py runserver` il est très rapide de mettre en place
un serveur de développement par exemple sur son ordinateur.
@ -186,7 +184,7 @@ un serveur de développement par exemple sur son ordinateur.
(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
ce qu'il faut
4. Migrations et chargement des données initiales :
@ -206,27 +204,11 @@ 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
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.
La documentation est générée par django et son module admindocs.
**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,9 +44,8 @@ class UserTable(tables.Table):
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
def render_balance(self, record, value):
return pretty_money(value)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
def render_balance(self, value):
return pretty_money(value)
class Meta:
attrs = {
@ -106,8 +105,8 @@ class MembershipTable(tables.Table):
empty_membership = Membership(
club=record.club,
user=record.user,
date_start=timezone.now().date(),
date_end=timezone.now().date(),
date_start=datetime.now().date(),
date_end=datetime.now().date(),
fee=0,
)
if PermissionBackend.check_perm(get_current_authenticated_user(),
@ -128,7 +127,7 @@ class MembershipTable(tables.Table):
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
'class': 'table table-condensed table-striped table-hover',
'style': 'table-layout: fixed;'
}
template_name = 'django_tables2/bootstrap4.html'

View File

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

@ -1,52 +0,0 @@
# 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, does casefolding and ignore non-ASCII characters
Normalizes a string: removes most diacritics and does casefolding
"""
return ''.join(
char for char in unicodedata.normalize('NFKD', string.casefold().replace('æ', 'ae').replace('œ', 'oe'))
char for char in unicodedata.normalize('NFKD', string.casefold())
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'})).casefold().encode('ascii', 'ignore').decode('ascii')
for cat in {'M', 'P', 'Z', 'C'})).casefold()
def clean(self):
normalized_name = self.normalize(self.name)

View File

@ -19,7 +19,8 @@ from .templatetags.pretty_money import pretty_money
class HistoryTable(tables.Table):
class Meta:
attrs = {
'class': 'table table-condensed table-striped'
'class':
'table table-condensed table-striped table-hover'
}
model = Transaction
exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
@ -122,7 +123,7 @@ DELETE_TEMPLATE = """
class AliasTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'class': 'table table condensed table-striped table-hover',
'id': "alias_table"
}
model = Alias
@ -141,7 +142,8 @@ class AliasTable(tables.Table):
class ButtonTable(tables.Table):
class Meta:
attrs = {
'class': 'table table-bordered condensed'
'class':
'table table-bordered condensed table-hover'
}
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\", {\"source__balance__gte\": {\"F\": [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]]}}, {\"valid\": false}]]",
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]",
"type": "add",
"mask": 1,
"field": "",
@ -319,7 +319,7 @@
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]]",
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 2,
"field": "",
@ -335,7 +335,7 @@
"note",
"recurrenttransaction"
],
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]]",
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 2,
"field": "",
@ -1572,7 +1572,7 @@
"mask": 1,
"field": "emergency_contact_phone",
"permanent": false,
"description": "Modifier le téléphone du contact en cas d'urgence de mon inscription WEI"
"description": "Modifier le nom 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\", {\"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}]]",
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]",
"type": "change",
"mask": 2,
"field": "valid",
@ -2079,9 +2079,9 @@
"note",
"transaction"
],
"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}]]",
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]",
"type": "change",
"mask": 2,
"mask": 1,
"field": "invalidity_reason",
"permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction de club"
@ -2910,8 +2910,7 @@
140,
145,
146,
147,
150
147
]
}
},

View File

@ -129,8 +129,7 @@ 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 = [(w, w) for w in WORDS]
self.fields["word"].choices = WORDS
if self.is_valid():
return
self.fields["word"].choices = words

View File

@ -160,8 +160,7 @@
<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 if you don't credit the note now.
The note don't have enough money ({{ balance }}, {{ pretty_fee }} required). The registration may fail.
{% endblocktrans %}
{% endwith %}
</div>

View File

@ -16,7 +16,6 @@
{% endblock %}
{% block extrajavascript %}
{% if not object.membership %}
<script>
$(document).ready(function() {
function refreshTeams() {
@ -40,5 +39,4 @@
refreshTeams();
});
</script>
{% endif %}
{% endblock %}

View File

@ -1,419 +0,0 @@
# 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_team", kwargs={"pk": self.object.pk})
return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.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(self.request.POST if self.request.POST else None)
choose_bus_form = WEIChooseBusForm()
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,22 +602,16 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
context["club"] = self.object.wei
if self.object.is_validated:
membership_form = WEIMembershipForm(instance=self.object.membership,
data=self.request.POST if self.request.POST else None)
membership_form = WEIMembershipForm(instance=self.object.membership)
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(
self.request.POST if self.request.POST else dict(
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(),
@ -861,7 +855,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 + credit_amount < fee:
if not registration.soge_credit and user.note.balance < fee + credit_amount:
# 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 apps/
coverage run --omit='*migrations*' --source=apps,note_kfet ./manage.py test
coverage report -m
[testenv:linters]