Merge branch 'beta' into 'master'
Beta Closes #59 See merge request bde/nk20!107
|
@ -40,15 +40,7 @@ linters:
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
image: debian:buster-backports
|
image: debian:buster-backports
|
||||||
before_script:
|
before_script:
|
||||||
- >
|
- apt-get update && apt-get install -y tox
|
||||||
apt-get update &&
|
|
||||||
apt-get install --no-install-recommends -t buster-backports -y
|
|
||||||
python3-django python3-django-crispy-forms
|
|
||||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
|
||||||
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
|
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers
|
|
||||||
python3-bs4 python3-setuptools tox
|
|
||||||
texlive-latex-extra texlive-lang-french lmodern texlive-fonts-recommended
|
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
|
||||||
# Be nice to new contributors, but please use `tox`
|
# Be nice to new contributors, but please use `tox`
|
||||||
|
|
115
README.md
|
@ -4,16 +4,82 @@
|
||||||
[![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/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
|
## Table des matières
|
||||||
|
|
||||||
On supposera pour la suite que vous utilisez une installation de Debian Buster ou Ubuntu 20.04 fraîche ou bien configuré.
|
- [Installation d'une instance de développement](#installation-dune-instance-de-développement)
|
||||||
|
- [Installation d'une instance de production](#installation-dune-instance-de-production)
|
||||||
|
|
||||||
|
## Installation d'une instance de développement
|
||||||
|
|
||||||
|
L'instance de développement installe la majorité des dépendances dans un environnement Python isolé.
|
||||||
|
Bien que cela permette de créer une instance sur toutes les distributions,
|
||||||
|
**cela veut dire que vos dépendances ne seront pas mises à jour automatiquement.**
|
||||||
|
|
||||||
|
1. **Installation des dépendances de la distribution.**
|
||||||
|
Il y a quelques dépendances qui ne sont pas trouvable dans PyPI.
|
||||||
|
On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo apt update
|
||||||
|
$ sudo apt install --no-install-recommends -y \
|
||||||
|
ipython3 python3-setuptools python3-venv python3-dev \
|
||||||
|
texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended \
|
||||||
|
gettext libjs-bootstrap4 fonts-font-awesome git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Clonage du dépot** là où vous voulez :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Création d'un environment de travail Python décorrélé du système.**
|
||||||
|
On n'utilise pas `--system-site-packages` ici pour ne pas avoir des clashs de versions de modules avec le système.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python3 -m venv env
|
||||||
|
$ source env/bin/activate # entrer dans l'environnement
|
||||||
|
(env)$ pip3 install -r requirements.txt
|
||||||
|
(env)$ deactivate # sortir de l'environnement
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Variable d'environnement.**
|
||||||
|
Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
|
||||||
|
ce qu'il faut.
|
||||||
|
|
||||||
|
5. **Migrations et chargement des données initiales.**
|
||||||
|
Pour initialiser la base de données avec de quoi travailler.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(env)$ ./manage.py collectstatic --noinput
|
||||||
|
(env)$ ./manage.py compilemessages
|
||||||
|
(env)$ ./manage.py makemigrations
|
||||||
|
(env)$ ./manage.py migrate
|
||||||
|
(env)$ ./manage.py loaddata initial
|
||||||
|
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Enjoy :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(env)$ ./manage.py runserver 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
|
||||||
|
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
||||||
|
de la note sur un téléphone !
|
||||||
|
|
||||||
|
## Installation d'une instance de production
|
||||||
|
|
||||||
|
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
|
||||||
|
Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
|
||||||
|
|
||||||
|
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
|
||||||
|
|
||||||
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
|
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.
|
Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||||
|
|
||||||
### Installation avec Debian/Ubuntu
|
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
||||||
|
|
||||||
0. **Activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ echo "deb http://deb.debian.org/debian buster-backports main" | sudo tee /etc/apt/sources.list.d/deb_debian_org_debian.list
|
$ echo "deb http://deb.debian.org/debian buster-backports main" | sudo tee /etc/apt/sources.list.d/deb_debian_org_debian.list
|
||||||
|
@ -192,43 +258,6 @@ nk20:
|
||||||
- "traefik.http.services.nk20.loadbalancer.server.port=8080"
|
- "traefik.http.services.nk20.loadbalancer.server.port=8080"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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 :
|
|
||||||
|
|
||||||
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
|
|
||||||
|
|
||||||
2. Créer un environnement Python isolé
|
|
||||||
pour ne pas interférer avec les versions de paquets systèmes :
|
|
||||||
|
|
||||||
$ python3 -m venv venv
|
|
||||||
$ source venv/bin/activate
|
|
||||||
(env)$ pip install -r requirements.txt
|
|
||||||
|
|
||||||
3. 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 :
|
|
||||||
|
|
||||||
(env)$ ./manage.py makemigrations
|
|
||||||
(env)$ ./manage.py migrate
|
|
||||||
(env)$ ./manage.py loaddata initial
|
|
||||||
|
|
||||||
5. Créer un super-utilisateur :
|
|
||||||
|
|
||||||
(env)$ ./manage.py createsuperuser
|
|
||||||
|
|
||||||
6. Enjoy :
|
|
||||||
|
|
||||||
(env)$ ./manage.py runserver 0.0.0.0:8000
|
|
||||||
|
|
||||||
En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
|
|
||||||
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
|
||||||
de la note sur un téléphone !
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
|
Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
|
||||||
|
|
|
@ -130,7 +130,7 @@ class Activity(models.Model):
|
||||||
raise ValidationError(_("The end date must be after the start date."))
|
raise ValidationError(_("The end date must be after the start date."))
|
||||||
|
|
||||||
ret = super().save(*args, **kwargs)
|
ret = super().save(*args, **kwargs)
|
||||||
if settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS:
|
if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS:
|
||||||
def refresh_activities():
|
def refresh_activities():
|
||||||
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
|
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
|
||||||
RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name)
|
RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name)
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<h1 class="text-white">{{ title }}</h1>
|
<h1 class="text-white">{{ title }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle bg-light" style="width: 100%" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle bg-light" style="width: 100%">
|
||||||
<a href="{% url "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Transfer" %}
|
{% trans "Transfer" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from activity.models import Activity, ActivityType, Guest, Entry
|
||||||
|
from member.models import Club
|
||||||
|
|
||||||
|
|
||||||
|
class TestActivities(TestCase):
|
||||||
|
"""
|
||||||
|
Test activities
|
||||||
|
"""
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="tototototo",
|
||||||
|
email="toto@example.com"
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.activity = Activity.objects.create(
|
||||||
|
name="Activity",
|
||||||
|
description="This is a test activity\non two very very long lines\nbecause this is very important.",
|
||||||
|
location="Earth",
|
||||||
|
activity_type=ActivityType.objects.get(name="Pot"),
|
||||||
|
creater=self.user,
|
||||||
|
organizer=Club.objects.get(name="Kfet"),
|
||||||
|
attendees_club=Club.objects.get(name="Kfet"),
|
||||||
|
date_start=timezone.now(),
|
||||||
|
date_end=timezone.now() + timedelta(days=2),
|
||||||
|
valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.guest = Guest.objects.create(
|
||||||
|
activity=self.activity,
|
||||||
|
inviter=self.user.note,
|
||||||
|
last_name="GUEST",
|
||||||
|
first_name="Guest",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_activity_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of all activities
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:activity_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_activity_create(self):
|
||||||
|
"""
|
||||||
|
Create a new activity
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:activity_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("activity:activity_create"), data=dict(
|
||||||
|
name="Activity created",
|
||||||
|
description="This activity was successfully created.",
|
||||||
|
location="Earth",
|
||||||
|
activity_type=ActivityType.objects.get(name="Soirée de club").id,
|
||||||
|
creater=self.user.id,
|
||||||
|
organizer=Club.objects.get(name="Kfet").id,
|
||||||
|
attendees_club=Club.objects.get(name="Kfet").id,
|
||||||
|
date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()),
|
||||||
|
date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)),
|
||||||
|
valid=True,
|
||||||
|
))
|
||||||
|
self.assertTrue(Activity.objects.filter(name="Activity created").exists())
|
||||||
|
activity = Activity.objects.get(name="Activity created")
|
||||||
|
self.assertRedirects(response, reverse("activity:activity_detail", args=(activity.pk,)), 302, 200)
|
||||||
|
|
||||||
|
def test_activity_detail(self):
|
||||||
|
"""
|
||||||
|
Display the detail of an activity
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:activity_detail", args=(self.activity.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_activity_update(self):
|
||||||
|
"""
|
||||||
|
Update an activity
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:activity_update", args=(self.activity.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("activity:activity_update", args=(self.activity.pk,)), data=dict(
|
||||||
|
name=str(self.activity) + " updated",
|
||||||
|
description="This activity was successfully updated.",
|
||||||
|
location="Earth",
|
||||||
|
activity_type=ActivityType.objects.get(name="Autre").id,
|
||||||
|
creater=self.user.id,
|
||||||
|
organizer=Club.objects.get(name="Kfet").id,
|
||||||
|
attendees_club=Club.objects.get(name="Kfet").id,
|
||||||
|
date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()),
|
||||||
|
date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)),
|
||||||
|
valid=True,
|
||||||
|
))
|
||||||
|
self.assertTrue(Activity.objects.filter(name="Activity updated").exists())
|
||||||
|
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
|
||||||
|
|
||||||
|
def test_activity_entry(self):
|
||||||
|
"""
|
||||||
|
Create some entries
|
||||||
|
"""
|
||||||
|
self.activity.open = True
|
||||||
|
self.activity.save()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=guest")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=admin")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# User entry
|
||||||
|
response = self.client.post("/api/activity/entry/", data=dict(
|
||||||
|
activity=self.activity.id,
|
||||||
|
note=self.user.note.id,
|
||||||
|
guest="",
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 201) # 201 = Created
|
||||||
|
self.assertTrue(Entry.objects.filter(note=self.user.note, guest=None, activity=self.activity).exists())
|
||||||
|
|
||||||
|
# Guest entry
|
||||||
|
response = self.client.post("/api/activity/entry/", data=dict(
|
||||||
|
activity=self.activity.id,
|
||||||
|
note=self.user.note.id,
|
||||||
|
guest=self.guest.id,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 201) # 201 = Created
|
||||||
|
self.assertTrue(Entry.objects.filter(note=self.user.note, guest=self.guest.id, activity=self.activity).exists())
|
||||||
|
|
||||||
|
def test_activity_invite(self):
|
||||||
|
"""
|
||||||
|
Try to invite people to an activity
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:activity_invite", args=(self.activity.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# The activity is started, can't invite
|
||||||
|
response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict(
|
||||||
|
activity=self.activity.id,
|
||||||
|
inviter=self.user.note.id,
|
||||||
|
last_name="GUEST2",
|
||||||
|
first_name="Guest",
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.activity.date_start += timedelta(days=1)
|
||||||
|
self.activity.save()
|
||||||
|
|
||||||
|
response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict(
|
||||||
|
activity=self.activity.id,
|
||||||
|
inviter=self.user.note.id,
|
||||||
|
last_name="GUEST2",
|
||||||
|
first_name="Guest",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
|
||||||
|
|
||||||
|
def test_activity_ics(self):
|
||||||
|
"""
|
||||||
|
Render the ICS calendar
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("activity:calendar_ics"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
|
@ -14,4 +14,5 @@ urlpatterns = [
|
||||||
path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'),
|
path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'),
|
||||||
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
|
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
|
||||||
path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
|
path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
|
||||||
|
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
# 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 hashlib import md5
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
from note.models import Alias, NoteSpecial, NoteUser
|
from note.models import Alias, NoteSpecial, NoteUser
|
||||||
|
@ -190,10 +194,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
if pattern[0] != "^":
|
if pattern[0] != "^":
|
||||||
pattern = "^" + pattern
|
pattern = "^" + pattern
|
||||||
guest_qs = guest_qs.filter(
|
guest_qs = guest_qs.filter(
|
||||||
Q(first_name__regex=pattern)
|
Q(first_name__iregex=pattern)
|
||||||
| Q(last_name__regex=pattern)
|
| Q(last_name__iregex=pattern)
|
||||||
| Q(inviter__alias__name__regex=pattern)
|
| Q(inviter__alias__name__iregex=pattern)
|
||||||
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))
|
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
|
@ -226,21 +230,19 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
note_qs = note_qs.filter(
|
note_qs = note_qs.filter(
|
||||||
Q(note__noteuser__user__first_name__regex=pattern)
|
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||||
| Q(note__noteuser__user__last_name__regex=pattern)
|
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||||
| Q(name__regex=pattern)
|
| Q(name__iregex=pattern)
|
||||||
| Q(normalized_name__regex=Alias.normalize(pattern))
|
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
note_qs = note_qs.none()
|
note_qs = note_qs.none()
|
||||||
|
|
||||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql':
|
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
|
||||||
note_qs = note_qs.distinct('note__pk')[:20]
|
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
|
||||||
else:
|
# In production mode, please use PostgreSQL.
|
||||||
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
|
note_qs = note_qs.distinct('note__pk')[:20]\
|
||||||
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
|
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||||
# In production mode, please use PostgreSQL.
|
|
||||||
note_qs = note_qs.distinct()[:20]
|
|
||||||
return note_qs
|
return note_qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -281,3 +283,60 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
Entry(activity=a, note=self.request.user.note,))]
|
Entry(activity=a, note=self.request.user.note,))]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarView(View):
|
||||||
|
"""
|
||||||
|
Render an ICS calendar with all valid activities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def multilines(self, string, maxlength, offset=0):
|
||||||
|
newstring = string[:maxlength - offset]
|
||||||
|
string = string[maxlength - offset:]
|
||||||
|
while string:
|
||||||
|
newstring += "\r\n "
|
||||||
|
newstring += string[:maxlength - 1]
|
||||||
|
string = string[maxlength - 1:]
|
||||||
|
return newstring
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
ics = """BEGIN:VCALENDAR
|
||||||
|
VERSION: 2.0
|
||||||
|
PRODID:Note Kfet 2020
|
||||||
|
X-WR-CALNAME:Kfet Calendar
|
||||||
|
NAME:Kfet Calendar
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
TZNAME:CEST
|
||||||
|
DTSTART:19700329T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
TZNAME:CET
|
||||||
|
DTSTART:19701025T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
"""
|
||||||
|
for activity in Activity.objects.filter(valid=True).order_by("-date_start").all():
|
||||||
|
ics += f"""BEGIN:VEVENT
|
||||||
|
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
||||||
|
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
||||||
|
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
||||||
|
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
||||||
|
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
|
||||||
|
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
||||||
|
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
|
||||||
|
-- {activity.organizer.name}
|
||||||
|
END:VEVENT
|
||||||
|
"""
|
||||||
|
ics += "END:VCALENDAR"
|
||||||
|
ics = ics.replace("\r", "").replace("\n", "\r\n")
|
||||||
|
return HttpResponse(ics, content_type="text/calendar; charset=UTF-8")
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
REST API Serializer for Users.
|
||||||
|
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
exclude = (
|
||||||
|
'password',
|
||||||
|
'groups',
|
||||||
|
'user_permissions',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
REST API Serializer for Users.
|
||||||
|
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ContentType
|
||||||
|
fields = '__all__'
|
|
@ -3,103 +3,9 @@
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.contrib.auth.models import User
|
from rest_framework import routers
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.db.models import Q
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from rest_framework import routers, serializers
|
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
|
||||||
from note.models import Alias
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
REST API Serializer for Users.
|
|
||||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
exclude = (
|
|
||||||
'password',
|
|
||||||
'groups',
|
|
||||||
'user_permissions',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
REST API Serializer for Users.
|
|
||||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ContentType
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ReadProtectedModelViewSet):
|
|
||||||
"""
|
|
||||||
REST API View set.
|
|
||||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
|
||||||
then render it on /api/users/
|
|
||||||
"""
|
|
||||||
queryset = User.objects.all()
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
filter_backends = [DjangoFilterBackend]
|
|
||||||
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset().order_by("username")
|
|
||||||
|
|
||||||
if "search" in self.request.GET:
|
|
||||||
pattern = self.request.GET["search"]
|
|
||||||
|
|
||||||
# We match first a user by its username, then if an alias is matched without normalization
|
|
||||||
# And finally if the normalized pattern matches a normalized alias.
|
|
||||||
queryset = queryset.filter(
|
|
||||||
username__iregex="^" + pattern
|
|
||||||
).union(
|
|
||||||
queryset.filter(
|
|
||||||
Q(note__alias__name__iregex="^" + pattern)
|
|
||||||
& ~Q(username__iregex="^" + pattern)
|
|
||||||
), all=True).union(
|
|
||||||
queryset.filter(
|
|
||||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
|
||||||
& ~Q(username__iregex="^" + pattern)
|
|
||||||
),
|
|
||||||
all=True).union(
|
|
||||||
queryset.filter(
|
|
||||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
|
||||||
& ~Q(username__iregex="^" + pattern)
|
|
||||||
),
|
|
||||||
all=True).union(
|
|
||||||
queryset.filter(
|
|
||||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
|
||||||
& ~Q(username__iregex="^" + pattern)
|
|
||||||
),
|
|
||||||
all=True)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
# This ViewSet is the only one that is accessible from all authenticated users!
|
|
||||||
class ContentTypeViewSet(ReadOnlyModelViewSet):
|
|
||||||
"""
|
|
||||||
REST API View set.
|
|
||||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
|
||||||
then render it on /api/users/
|
|
||||||
"""
|
|
||||||
queryset = ContentType.objects.all()
|
|
||||||
serializer_class = ContentTypeSerializer
|
|
||||||
|
|
||||||
|
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||||
|
|
||||||
# Routers provide an easy way of automatically determining the URL conf.
|
# Routers provide an easy way of automatically determining the URL conf.
|
||||||
# Register each app API router and user viewset
|
# Register each app API router and user viewset
|
||||||
|
|
|
@ -2,12 +2,19 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from rest_framework import viewsets
|
|
||||||
from note_kfet.middlewares import get_current_session
|
from note_kfet.middlewares import get_current_session
|
||||||
|
from note.models import Alias
|
||||||
|
|
||||||
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
class ReadProtectedModelViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||||
"""
|
"""
|
||||||
|
@ -19,10 +26,10 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
get_current_session().setdefault("permission_mask", 42)
|
||||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
|
Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
|
||||||
"""
|
"""
|
||||||
|
@ -34,4 +41,72 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
get_current_session().setdefault("permission_mask", 42)
|
||||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewSet(ReadProtectedModelViewSet):
|
||||||
|
"""
|
||||||
|
REST API View set.
|
||||||
|
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||||
|
then render it on /api/users/
|
||||||
|
"""
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
filter_backends = [DjangoFilterBackend]
|
||||||
|
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
# Sqlite doesn't support ORDER BY in subqueries
|
||||||
|
queryset = queryset.order_by("username") \
|
||||||
|
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
||||||
|
|
||||||
|
if "search" in self.request.GET:
|
||||||
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Filter with different rules
|
||||||
|
# We use union-all to keep each filter rule sorted in result
|
||||||
|
queryset = queryset.filter(
|
||||||
|
# Match without normalization
|
||||||
|
note__alias__name__iregex="^" + pattern
|
||||||
|
).union(
|
||||||
|
queryset.filter(
|
||||||
|
# Match with normalization
|
||||||
|
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
|
),
|
||||||
|
all=True,
|
||||||
|
).union(
|
||||||
|
queryset.filter(
|
||||||
|
# Match on lower pattern
|
||||||
|
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||||
|
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
|
),
|
||||||
|
all=True,
|
||||||
|
).union(
|
||||||
|
queryset.filter(
|
||||||
|
# Match on firstname or lastname
|
||||||
|
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
||||||
|
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||||
|
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
|
),
|
||||||
|
all=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
||||||
|
else queryset.order_by("username")
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
# This ViewSet is the only one that is accessible from all authenticated users!
|
||||||
|
class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
REST API View set.
|
||||||
|
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||||
|
then render it on /api/users/
|
||||||
|
"""
|
||||||
|
queryset = ContentType.objects.all()
|
||||||
|
serializer_class = ContentTypeSerializer
|
||||||
|
|
|
@ -50,10 +50,7 @@ def save_object(sender, instance, **kwargs):
|
||||||
in order to store each modification made
|
in order to store each modification made
|
||||||
"""
|
"""
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
|
||||||
return
|
|
||||||
|
|
||||||
if hasattr(instance, "_no_log"):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
|
@ -120,10 +117,7 @@ def delete_object(sender, instance, **kwargs):
|
||||||
Each time a model is deleted, an entry in the table `Changelog` is added in the database
|
Each time a model is deleted, an entry in the table `Changelog` is added in the database
|
||||||
"""
|
"""
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
|
||||||
return
|
|
||||||
|
|
||||||
if hasattr(instance, "_no_log"):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||||
|
|
|
@ -31,9 +31,7 @@ class CustomUserAdmin(UserAdmin):
|
||||||
"""
|
"""
|
||||||
When creating a new user don't show profile one the first step
|
When creating a new user don't show profile one the first step
|
||||||
"""
|
"""
|
||||||
if not obj:
|
return super().get_inline_instances(request, obj) if obj else []
|
||||||
return list()
|
|
||||||
return super().get_inline_instances(request, obj)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Club, site=admin_site)
|
@admin.register(Club, site=admin_site)
|
||||||
|
|
|
@ -172,19 +172,21 @@ class Profile(models.Model):
|
||||||
|
|
||||||
def send_email_validation_link(self):
|
def send_email_validation_link(self):
|
||||||
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
||||||
|
token = email_validation_token.make_token(self.user)
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(self.user_id))
|
||||||
message = loader.render_to_string('registration/mails/email_validation_email.txt',
|
message = loader.render_to_string('registration/mails/email_validation_email.txt',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||||
'token': email_validation_token.make_token(self.user),
|
'token': token,
|
||||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
'uid': uid,
|
||||||
})
|
})
|
||||||
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
||||||
{
|
{
|
||||||
'user': self.user,
|
'user': self.user,
|
||||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||||
'token': email_validation_token.make_token(self.user),
|
'token': token,
|
||||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
'uid': uid,
|
||||||
})
|
})
|
||||||
self.user.email_user(subject, message, html_message=html)
|
self.user.email_user(subject, message, html_message=html)
|
||||||
|
|
||||||
|
@ -339,43 +341,40 @@ class Membership(models.Model):
|
||||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
|
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
|
||||||
|
|
||||||
def renew(self):
|
def renew(self):
|
||||||
if Membership.objects.filter(
|
if not Membership.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
club=self.club,
|
club=self.club,
|
||||||
date_start__gte=self.club.membership_start,
|
date_start__gte=self.club.membership_start,
|
||||||
).exists():
|
).exists():
|
||||||
# Membership is already renewed
|
# Membership is not renewed yet
|
||||||
return
|
new_membership = Membership(
|
||||||
new_membership = Membership(
|
user=self.user,
|
||||||
user=self.user,
|
club=self.club,
|
||||||
club=self.club,
|
date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start),
|
||||||
date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start),
|
)
|
||||||
)
|
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
new_membership._force_renew_parent = True
|
||||||
new_membership._force_renew_parent = True
|
if hasattr(self, '_soge') and self._soge:
|
||||||
if hasattr(self, '_soge') and self._soge:
|
new_membership._soge = True
|
||||||
new_membership._soge = True
|
if hasattr(self, '_force_save') and self._force_save:
|
||||||
if hasattr(self, '_force_save') and self._force_save:
|
new_membership._force_save = True
|
||||||
new_membership._force_save = True
|
new_membership.save()
|
||||||
new_membership.save()
|
new_membership.roles.set(self.roles.all())
|
||||||
new_membership.roles.set(self.roles.all())
|
new_membership.save()
|
||||||
new_membership.save()
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
"""
|
"""
|
||||||
|
created = not self.pk
|
||||||
if self.pk:
|
if not created:
|
||||||
for role in self.roles.all():
|
for role in self.roles.all():
|
||||||
club = role.for_club
|
club = role.for_club
|
||||||
if club is not None:
|
if club is not None:
|
||||||
if club.pk != self.club_id:
|
if club.pk != self.club_id:
|
||||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||||
.format(role=role.name, club=club.name))
|
.format(role=role.name, club=club.name))
|
||||||
|
else:
|
||||||
created = not self.pk
|
|
||||||
if created:
|
|
||||||
if Membership.objects.filter(
|
if Membership.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
club=self.club,
|
club=self.club,
|
||||||
|
@ -384,7 +383,7 @@ class Membership(models.Model):
|
||||||
).exists():
|
).exists():
|
||||||
raise ValidationError(_('User is already a member of the club'))
|
raise ValidationError(_('User is already a member of the club'))
|
||||||
|
|
||||||
if self.club.parent_club is not None and not self.pk:
|
if self.club.parent_club is not None:
|
||||||
# Check that the user is already a member of the parent club if the membership is created
|
# Check that the user is already a member of the parent club if the membership is created
|
||||||
if not Membership.objects.filter(
|
if not Membership.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -433,15 +432,10 @@ class Membership(models.Model):
|
||||||
raise ValidationError(_('User is not a member of the parent club')
|
raise ValidationError(_('User is not a member of the parent club')
|
||||||
+ ' ' + self.club.parent_club.name)
|
+ ' ' + self.club.parent_club.name)
|
||||||
|
|
||||||
if self.user.profile.paid:
|
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||||
self.fee = self.club.membership_fee_paid
|
|
||||||
else:
|
|
||||||
self.fee = self.club.membership_fee_unpaid
|
|
||||||
|
|
||||||
if self.club.membership_duration is not None:
|
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
|
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||||
else:
|
|
||||||
self.date_end = self.date_start + datetime.timedelta(days=424242)
|
|
||||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||||
self.date_end = self.club.membership_end
|
self.date_end = self.club.membership_end
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,10 @@ def save_user_profile(instance, created, raw, **_kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to create and save a profile when an user is updated if it is not registered with the signup form
|
Hook to create and save a profile when an user is updated if it is not registered with the signup form
|
||||||
"""
|
"""
|
||||||
if raw:
|
if not raw and created and instance.is_active:
|
||||||
# When provisionning data, do not try to autocreate
|
|
||||||
return
|
|
||||||
|
|
||||||
if created and instance.is_active:
|
|
||||||
from .models import Profile
|
from .models import Profile
|
||||||
Profile.objects.get_or_create(user=instance)
|
Profile.objects.get_or_create(user=instance)
|
||||||
|
if instance.is_superuser:
|
||||||
|
instance.profile.email_confirmed = True
|
||||||
|
instance.profile.registration_valid = True
|
||||||
instance.profile.save()
|
instance.profile.save()
|
||||||
|
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
@ -48,7 +48,9 @@
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% if user_object.pk == user_object.pk %}
|
{% if user_object.pk == user_object.pk %}
|
||||||
<a class="small float-right text-decoration-none" href="{% url 'member:auth_token' %}">
|
<div class="text-center">
|
||||||
{% trans 'Manage auth token' %}
|
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||||
</a>
|
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
||||||
{% endif %}
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "member/base.html" %}
|
{% extends "base.html" %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
|
@ -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 django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from note.models import TransactionTemplate, TemplateCategory
|
from django.urls import reverse
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Test that login page still works
|
Test that login page still works
|
||||||
|
@ -31,7 +31,20 @@ class TemplateLoggedInTests(TestCase):
|
||||||
sess.save()
|
sess.save()
|
||||||
|
|
||||||
def test_login_page(self):
|
def test_login_page(self):
|
||||||
response = self.client.get('/accounts/login/')
|
response = self.client.get(reverse("login"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
response = self.client.post('/accounts/login/', data=dict(
|
||||||
|
username="admin",
|
||||||
|
password="adminadmin",
|
||||||
|
permission_mask=3,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200)
|
||||||
|
|
||||||
|
def test_logout(self):
|
||||||
|
response = self.client.get(reverse("logout"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_admin_index(self):
|
def test_admin_index(self):
|
||||||
|
@ -41,22 +54,3 @@ class TemplateLoggedInTests(TestCase):
|
||||||
def test_accounts_password_reset(self):
|
def test_accounts_password_reset(self):
|
||||||
response = self.client.get('/accounts/password_reset/')
|
response = self.client.get('/accounts/password_reset/')
|
||||||
self.assertEqual(response.status_code, 200)
|
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):
|
|
||||||
# Create one button and ensure that it is visible
|
|
||||||
cat = TemplateCategory.objects.create()
|
|
||||||
TransactionTemplate.objects.create(
|
|
||||||
destination_id=5,
|
|
||||||
category=cat,
|
|
||||||
amount=0,
|
|
||||||
)
|
|
||||||
response = self.client.get('/note/consos/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
|
@ -0,0 +1,405 @@
|
||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
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 Club, Membership, Profile
|
||||||
|
from note.models import Alias, NoteSpecial
|
||||||
|
from permission.models import Role
|
||||||
|
from treasury.models import SogeCredit
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create some users and clubs and test that all pages are rendering properly
|
||||||
|
and that memberships are working.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemberships(TestCase):
|
||||||
|
fixtures = ('initial', )
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""
|
||||||
|
Create a sample superuser, a club and a membership for all tests.
|
||||||
|
"""
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="toto",
|
||||||
|
email="toto@example.com",
|
||||||
|
password="toto",
|
||||||
|
)
|
||||||
|
self.user.profile.registration_valid = True
|
||||||
|
self.user.profile.email_confirmed = True
|
||||||
|
self.user.profile.save()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.club = Club.objects.create(name="totoclub", parent_club=Club.objects.get(name="BDE"))
|
||||||
|
self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
|
||||||
|
self.membership = Membership.objects.create(user=self.user, club=self.club)
|
||||||
|
self.membership.roles.add(Role.objects.get(name="Bureau de club"))
|
||||||
|
self.membership.save()
|
||||||
|
|
||||||
|
def test_admin_pages(self):
|
||||||
|
"""
|
||||||
|
Check that Django Admin pages for the member app are loading successfully.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "member/membership/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:index") + "member/club/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:index") + "auth/user/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("admin:index") + "auth/user/" + str(self.user.pk) + "/change/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_club_list(self):
|
||||||
|
"""
|
||||||
|
Render the list of all clubs, with a search.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("member:club_list") + "?search=toto")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_club_create(self):
|
||||||
|
"""
|
||||||
|
Try to create a new club.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("member:club_create"), data=dict(
|
||||||
|
name="Club toto",
|
||||||
|
email="clubtoto@example.com",
|
||||||
|
parent_club=self.club.pk,
|
||||||
|
require_memberships=False,
|
||||||
|
membership_fee_paid=0,
|
||||||
|
membership_fee_unpaid=0,
|
||||||
|
))
|
||||||
|
self.assertTrue(Club.objects.filter(name="Club toto").exists())
|
||||||
|
club = Club.objects.get(name="Club toto")
|
||||||
|
self.assertRedirects(response, club.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
def test_render_club_detail(self):
|
||||||
|
"""
|
||||||
|
Display the detail of a club.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_detail", args=(self.club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_club_update(self):
|
||||||
|
"""
|
||||||
|
Try to update the information about a club.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_update", args=(self.club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("member:club_update", args=(self.club.pk, )), data=dict(
|
||||||
|
name="Toto club updated",
|
||||||
|
email="clubtoto@example.com",
|
||||||
|
require_memberships=True,
|
||||||
|
membership_fee_paid=0,
|
||||||
|
membership_fee_unpaid=0,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.club.get_absolute_url(), 302, 200)
|
||||||
|
self.assertTrue(Club.objects.exclude(name="Toto club updated"))
|
||||||
|
|
||||||
|
def test_render_club_update_picture(self):
|
||||||
|
"""
|
||||||
|
Try to update the picture of the note of a club.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_update_pic", args=(self.club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
old_pic = self.club.note.display_image
|
||||||
|
|
||||||
|
with open("apps/member/static/member/img/default_picture.png", "rb") as f:
|
||||||
|
image = SimpleUploadedFile("image.png", f.read(), "image/png")
|
||||||
|
response = self.client.post(reverse("member:club_update_pic", args=(self.club.pk,)), dict(
|
||||||
|
image=image,
|
||||||
|
x=0,
|
||||||
|
y=0,
|
||||||
|
width=200,
|
||||||
|
height=200,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.club.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
self.club.note.refresh_from_db()
|
||||||
|
self.assertTrue(os.path.exists(self.club.note.display_image.path))
|
||||||
|
os.remove(self.club.note.display_image.path)
|
||||||
|
|
||||||
|
self.club.note.display_image = old_pic
|
||||||
|
self.club.note.save()
|
||||||
|
|
||||||
|
def test_render_club_aliases(self):
|
||||||
|
"""
|
||||||
|
Display the list of the aliases of a club.
|
||||||
|
"""
|
||||||
|
# Alias creation and deletion is already tested in the note app
|
||||||
|
response = self.client.get(reverse("member:club_alias", args=(self.club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_club_members(self):
|
||||||
|
"""
|
||||||
|
Display the list of the members of a club.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_members", args=(self.club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member:club_members", args=(self.club.pk,)) + "?search=toto&roles="
|
||||||
|
+ ",".join([str(role.pk) for role in
|
||||||
|
Role.objects.filter(weirole__isnull=True).all()]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_club_add_member(self):
|
||||||
|
"""
|
||||||
|
Try to add memberships and renew them.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_add_member", args=(Club.objects.get(name="BDE").pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
user = User.objects.create(username="totototo")
|
||||||
|
user.profile.registration_valid = True
|
||||||
|
user.profile.email_confirmed = True
|
||||||
|
user.profile.save()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# We create a club without any parent and one club with parent BDE (that is the club Kfet)
|
||||||
|
for bde_parent in False, True:
|
||||||
|
if bde_parent:
|
||||||
|
club = Club.objects.get(name="Kfet")
|
||||||
|
else:
|
||||||
|
club = Club.objects.create(
|
||||||
|
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
|
||||||
|
parent_club=None,
|
||||||
|
email="newclub@example.com",
|
||||||
|
require_memberships=True,
|
||||||
|
membership_fee_paid=1000,
|
||||||
|
membership_fee_unpaid=500,
|
||||||
|
membership_start=date.today(),
|
||||||
|
membership_end=date.today() + timedelta(days=366),
|
||||||
|
membership_duration=366,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member:club_add_member", args=(club.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Create a new membership
|
||||||
|
response = self.client.post(reverse("member:club_add_member", args=(club.pk,)), data=dict(
|
||||||
|
user=user.pk,
|
||||||
|
date_start="{:%Y-%m-%d}".format(timezone.now().date()),
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
|
credit_amount=4200,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Le matelas",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, club.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
self.assertTrue(Membership.objects.filter(user=user, club=club).exists())
|
||||||
|
|
||||||
|
# Membership is sent to the past to check renewals
|
||||||
|
membership = Membership.objects.get(user=user, club=club)
|
||||||
|
self.assertTrue(membership.valid)
|
||||||
|
membership.date_start = date(year=2000, month=1, day=1)
|
||||||
|
membership.date_end = date(year=2000, month=12, day=31)
|
||||||
|
membership.save()
|
||||||
|
self.assertFalse(membership.valid)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member:club_members", args=(club.pk,)) + "?only_active=0")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
bde_membership = self.bde_membership
|
||||||
|
if bde_parent:
|
||||||
|
bde_membership = Membership.objects.get(club__name="BDE", user=user)
|
||||||
|
bde_membership.date_start = date(year=2000, month=1, day=1)
|
||||||
|
bde_membership.date_end = date(year=2000, month=12, day=31)
|
||||||
|
bde_membership.save()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member:club_renew_membership", args=(bde_membership.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("member:club_renew_membership", args=(membership.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Renew membership
|
||||||
|
response = self.client.post(reverse("member:club_renew_membership", args=(membership.pk,)), data=dict(
|
||||||
|
user=user.pk,
|
||||||
|
date_start="{:%Y-%m-%d}".format(timezone.now().date()),
|
||||||
|
soge=bde_parent,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
|
credit_amount=14242,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Bank",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, club.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
response = self.client.get(user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_auto_join_kfet_when_join_bde_with_soge(self):
|
||||||
|
"""
|
||||||
|
When we join the BDE club with a Soge registration, a Kfet membership is automatically created.
|
||||||
|
We check that it is the case.
|
||||||
|
"""
|
||||||
|
user = User.objects.create(username="new1A")
|
||||||
|
user.profile.registration_valid = True
|
||||||
|
user.profile.email_confirmed = True
|
||||||
|
user.profile.save()
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
bde = Club.objects.get(name="BDE")
|
||||||
|
kfet = Club.objects.get(name="Kfet")
|
||||||
|
|
||||||
|
response = self.client.post(reverse("member:club_add_member", args=(bde.pk,)), data=dict(
|
||||||
|
user=user.pk,
|
||||||
|
date_start="{:%Y-%m-%d}".format(timezone.now().date()),
|
||||||
|
soge=True,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Virement bancaire").id,
|
||||||
|
credit_amount=(bde.membership_fee_paid + kfet.membership_fee_paid) / 100,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, bde.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
self.assertTrue(Membership.objects.filter(user=user, club=bde).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists())
|
||||||
|
self.assertTrue(SogeCredit.objects.filter(user=user).exists())
|
||||||
|
|
||||||
|
def test_change_roles(self):
|
||||||
|
"""
|
||||||
|
Check to change the roles of a membership.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:club_manage_roles", args=(self.membership.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||||
|
roles=[role.id for role in Role.objects.filter(
|
||||||
|
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
self.membership.refresh_from_db()
|
||||||
|
self.assertEqual(self.membership.roles.count(), 3)
|
||||||
|
|
||||||
|
def test_render_user_list(self):
|
||||||
|
"""
|
||||||
|
Display the user search page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:user_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("member:user_list") + "?search=toto")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_user_detail(self):
|
||||||
|
"""
|
||||||
|
Display the user detail page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_render_user_update(self):
|
||||||
|
"""
|
||||||
|
Update some data about the user.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:user_update_profile", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("member:user_update_profile", args=(self.user.pk,)), data=dict(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="Toto",
|
||||||
|
username="toto changed",
|
||||||
|
email="updated@example.com",
|
||||||
|
phone_number="+33600000000",
|
||||||
|
section="",
|
||||||
|
department="A0",
|
||||||
|
promotion=timezone.now().year,
|
||||||
|
address="Earth",
|
||||||
|
paid=True,
|
||||||
|
ml_events_registration="en",
|
||||||
|
ml_sports_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
report_frequency=7,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
||||||
|
self.assertTrue(Profile.objects.filter(address="Earth").exists())
|
||||||
|
self.assertTrue(Alias.objects.filter(normalized_name="totochanged").exists())
|
||||||
|
|
||||||
|
def test_render_user_update_picture(self):
|
||||||
|
"""
|
||||||
|
Update the note picture of the user.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:user_update_pic", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
old_pic = self.user.note.display_image
|
||||||
|
|
||||||
|
with open("apps/member/static/member/img/default_picture.png", "rb") as f:
|
||||||
|
image = SimpleUploadedFile("image.png", f.read(), "image/png")
|
||||||
|
response = self.client.post(reverse("member:user_update_pic", args=(self.user.pk,)), dict(
|
||||||
|
image=image,
|
||||||
|
x=0,
|
||||||
|
y=0,
|
||||||
|
width=200,
|
||||||
|
height=200,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
|
||||||
|
self.user.note.refresh_from_db()
|
||||||
|
self.assertTrue(os.path.exists(self.user.note.display_image.path))
|
||||||
|
os.remove(self.user.note.display_image.path)
|
||||||
|
|
||||||
|
self.user.note.display_image = old_pic
|
||||||
|
self.user.note.save()
|
||||||
|
|
||||||
|
def test_render_user_aliases(self):
|
||||||
|
"""
|
||||||
|
Display the list of aliases of the user.
|
||||||
|
"""
|
||||||
|
# Alias creation and deletion is already tested in the note app
|
||||||
|
response = self.client.get(reverse("member:user_alias", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_manage_auth_token(self):
|
||||||
|
"""
|
||||||
|
Display the page to see the API authentication token, see it and regenerate it.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("member:auth_token"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("member:auth_token") + "?view")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("member:auth_token") + "?regenerate")
|
||||||
|
self.assertRedirects(response, reverse("member:auth_token") + "?view", 302, 200)
|
||||||
|
|
||||||
|
def test_random_coverage(self):
|
||||||
|
# Useless, only for coverage
|
||||||
|
self.assertEqual(str(self.user), str(self.user.profile))
|
||||||
|
self.user.profile.promotion = None
|
||||||
|
self.assertEqual(self.user.profile.ens_year, 0)
|
||||||
|
self.membership.date_end = None
|
||||||
|
self.assertTrue(self.membership.valid)
|
||||||
|
|
||||||
|
def test_nk15_hasher(self):
|
||||||
|
"""
|
||||||
|
Test that NK15 passwords are successfully imported.
|
||||||
|
"""
|
||||||
|
salt = "42"
|
||||||
|
password = "strongpassword42"
|
||||||
|
hashed = hashlib.sha256((salt + password).encode("utf-8")).hexdigest()
|
||||||
|
self.user.password = "custom_nk15$1$" + salt + "|" + hashed
|
||||||
|
self.user.save()
|
||||||
|
self.assertTrue(self.user.check_password(password))
|
|
@ -97,8 +97,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
note = NoteUser.objects.filter(
|
note = NoteUser.objects.filter(
|
||||||
alias__normalized_name=Alias.normalize(new_username))
|
alias__normalized_name=Alias.normalize(new_username))
|
||||||
if note.exists() and note.get().user != self.object:
|
if note.exists() and note.get().user != self.object:
|
||||||
form.add_error('username',
|
form.add_error('username', _("An alias with a similar name already exists."))
|
||||||
_("An alias with a similar name already exists."))
|
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
# Check if the username is one of user's aliases.
|
# Check if the username is one of user's aliases.
|
||||||
alias = Alias.objects.filter(name=new_username)
|
alias = Alias.objects.filter(name=new_username)
|
||||||
|
@ -141,10 +140,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
We can't display information of a not registered user.
|
We can't display information of a not registered user.
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset()
|
return super().get_queryset().filter(profile__registration_valid=True)
|
||||||
if self.request.user.is_superuser and self.request.session.get("permission_mask", -1) >= 42:
|
|
||||||
return qs
|
|
||||||
return qs.filter(profile__registration_valid=True)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -204,14 +200,16 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
Filter the user list with the given pattern.
|
Filter the user list with the given pattern.
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset().distinct("username").annotate(alias=F("note__alias__name"))\
|
qs = super().get_queryset().annotate(alias=F("note__alias__name"))\
|
||||||
.annotate(normalized_alias=F("note__alias__normalized_name"))\
|
.annotate(normalized_alias=F("note__alias__normalized_name"))\
|
||||||
.filter(profile__registration_valid=True).order_by("username")
|
.filter(profile__registration_valid=True)
|
||||||
if "search" in self.request.GET:
|
|
||||||
pattern = self.request.GET["search"]
|
|
||||||
|
|
||||||
if not pattern:
|
# Sqlite doesn't support order by in subqueries
|
||||||
return qs.none()
|
qs = qs.order_by("username").distinct("username")\
|
||||||
|
if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql' else qs.distinct()
|
||||||
|
|
||||||
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
username__iregex="^" + pattern
|
username__iregex="^" + pattern
|
||||||
|
@ -270,12 +268,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if form.is_valid():
|
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
|
||||||
return self.form_valid(form)
|
|
||||||
else:
|
|
||||||
print('is_invalid')
|
|
||||||
print(form)
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
image_field = form.cleaned_data['image']
|
image_field = form.cleaned_data['image']
|
||||||
|
@ -320,8 +313,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||||
Token.objects.get(user=self.request.user).delete()
|
Token.objects.get(user=self.request.user).delete()
|
||||||
return redirect(reverse_lazy('member:auth_token') + "?show",
|
return redirect(reverse_lazy('member:auth_token') + "?show")
|
||||||
permanent=True)
|
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -351,8 +343,9 @@ class ClubCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
email="",
|
email="",
|
||||||
)
|
)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def get_success_url(self):
|
||||||
return super().form_valid(form)
|
self.object.refresh_from_db()
|
||||||
|
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
|
@ -655,7 +648,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
|
fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
|
||||||
c = c.parent_club
|
c = c.parent_club
|
||||||
|
|
||||||
if user.note.balance + credit_amount < fee and not Membership.objects.filter(
|
if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter(
|
||||||
club__name="Kfet",
|
club__name="Kfet",
|
||||||
user=user,
|
user=user,
|
||||||
date_start__lte=date.today(),
|
date_start__lte=date.today(),
|
||||||
|
@ -683,7 +676,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
|
||||||
if club.membership_end and form.instance.date_start > club.membership_end:
|
if club.membership_end and form.instance.date_start > club.membership_end:
|
||||||
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
|
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
|
||||||
.format(form.instance.club.membership_start))
|
.format(form.instance.club.membership_end))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
# Now, all is fine, the membership can be created.
|
# Now, all is fine, the membership can be created.
|
||||||
|
@ -719,46 +712,38 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
transaction._force_save = True
|
transaction._force_save = True
|
||||||
transaction.save()
|
transaction.save()
|
||||||
|
|
||||||
|
# Parent club memberships are automatically renewed / created.
|
||||||
|
# For example, a Kfet membership creates a BDE membership if it does not exist.
|
||||||
form.instance._force_renew_parent = True
|
form.instance._force_renew_parent = True
|
||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
if club.name == "BDE":
|
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all()
|
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
|
||||||
elif club.name == "Kfet":
|
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all()
|
|
||||||
else:
|
|
||||||
member_role = Role.objects.filter(name="Membre de club").all()
|
|
||||||
form.instance.roles.set(member_role)
|
form.instance.roles.set(member_role)
|
||||||
form.instance._force_save = True
|
form.instance._force_save = True
|
||||||
form.instance.save()
|
form.instance.save()
|
||||||
|
|
||||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||||
# Kfet membership.
|
# Kfet membership.
|
||||||
if soge:
|
if soge and club.name == "BDE":
|
||||||
# If not already done, create BDE and Kfet memberships
|
|
||||||
bde = Club.objects.get(name="BDE")
|
|
||||||
kfet = Club.objects.get(name="Kfet")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
|
fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
|
|
||||||
soge_clubs = [bde, kfet]
|
# Get current membership, to get the end date
|
||||||
for club in soge_clubs:
|
old_membership = Membership.objects.filter(
|
||||||
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
club=kfet,
|
||||||
|
user=user,
|
||||||
# Get current membership, to get the end date
|
).order_by("-date_start")
|
||||||
old_membership = Membership.objects.filter(
|
|
||||||
club=club,
|
|
||||||
user=user,
|
|
||||||
).order_by("-date_start")
|
|
||||||
|
|
||||||
if old_membership.filter(date_start__gte=club.membership_start).exists():
|
|
||||||
# Membership is already renewed
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
if not old_membership.filter(date_start__gte=kfet.membership_start).exists():
|
||||||
|
# If the membership is not already renewed
|
||||||
membership = Membership(
|
membership = Membership(
|
||||||
club=club,
|
club=kfet,
|
||||||
user=user,
|
user=user,
|
||||||
fee=fee,
|
fee=fee,
|
||||||
date_start=max(old_membership.first().date_end + timedelta(days=1), club.membership_start)
|
date_start=max(old_membership.first().date_end + timedelta(days=1), kfet.membership_start)
|
||||||
if old_membership.exists() else form.instance.date_start,
|
if old_membership.exists() else form.instance.date_start,
|
||||||
)
|
)
|
||||||
membership._force_save = True
|
membership._force_save = True
|
||||||
|
@ -767,10 +752,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
if old_membership.exists():
|
if old_membership.exists():
|
||||||
membership.roles.set(old_membership.get().roles.all())
|
membership.roles.set(old_membership.get().roles.all())
|
||||||
elif c.name == "BDE":
|
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
|
|
||||||
elif c.name == "Kfet":
|
|
||||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -830,9 +812,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
||||||
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
|
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
|
||||||
|
|
||||||
if "roles" in self.request.GET:
|
if "roles" in self.request.GET:
|
||||||
if not self.request.GET["roles"]:
|
roles_str = self.request.GET["roles"].replace(' ', '').split(',') if self.request.GET["roles"] else ['0']
|
||||||
return qs.none()
|
|
||||||
roles_str = self.request.GET["roles"].replace(' ', '').split(',')
|
|
||||||
roles_int = map(int, roles_str)
|
roles_int = map(int, roles_str)
|
||||||
qs = qs.filter(roles__in=roles_int)
|
qs = qs.filter(roles__in=roles_int)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# 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 django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
@ -117,6 +117,9 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
|
# Sqlite doesn't support ORDER BY in subqueries
|
||||||
|
queryset = queryset.order_by("name") \
|
||||||
|
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
queryset = queryset.prefetch_related('note')
|
queryset = queryset.prefetch_related('note')
|
||||||
|
@ -137,7 +140,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
return queryset.order_by('name').distinct()
|
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
||||||
|
else queryset.order_by("name")
|
||||||
|
|
||||||
|
return queryset.distinct()
|
||||||
|
|
||||||
|
|
||||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||||
|
|
|
@ -356,4 +356,4 @@ class MembershipTransaction(Transaction):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('membership transaction')
|
return _('membership').capitalize()
|
||||||
|
|
|
@ -29,6 +29,7 @@ class HistoryTable(tables.Table):
|
||||||
source = tables.Column(
|
source = tables.Column(
|
||||||
attrs={
|
attrs={
|
||||||
"td": {
|
"td": {
|
||||||
|
"class": "text-nowrap",
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
"title": lambda record: _("used alias").capitalize() + " : " + record.source_alias,
|
"title": lambda record: _("used alias").capitalize() + " : " + record.source_alias,
|
||||||
}
|
}
|
||||||
|
@ -38,15 +39,46 @@ class HistoryTable(tables.Table):
|
||||||
destination = tables.Column(
|
destination = tables.Column(
|
||||||
attrs={
|
attrs={
|
||||||
"td": {
|
"td": {
|
||||||
|
"class": "text-nowrap",
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
"title": lambda record: _("used alias").capitalize() + " : " + record.destination_alias,
|
"title": lambda record: _("used alias").capitalize() + " : " + record.destination_alias,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
created_at = tables.DateTimeColumn(format='Y-m-d H:i:s',
|
||||||
|
attrs={
|
||||||
|
"td": {
|
||||||
|
"class": "text-nowrap",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = tables.Column(
|
||||||
|
attrs={
|
||||||
|
"td": {
|
||||||
|
"class": "text-nowrap",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = tables.Column(
|
||||||
|
attrs={
|
||||||
|
"td": {
|
||||||
|
"class": "text-break",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type = tables.Column()
|
type = tables.Column()
|
||||||
|
|
||||||
total = tables.Column() # will use Transaction.total() !!
|
total = tables.Column( # will use Transaction.total() !!
|
||||||
|
attrs={
|
||||||
|
"td": {
|
||||||
|
"class": "text-nowrap",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
valid = tables.Column(
|
valid = tables.Column(
|
||||||
attrs={
|
attrs={
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light border-success mb-4 text-center">
|
<div class="card bg-light border-success mb-4 text-center">
|
||||||
<a id="profile_pic_link" href="#">
|
<a id="profile_pic_link" href="#">
|
||||||
<img src="/media/pic/default.png"
|
<img src="{% static "member/img/default_picture.png" %}"
|
||||||
id="profile_pic" alt="" class="card-img-top">
|
id="profile_pic" alt="" class="card-img-top">
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body text-center text-break">
|
<div class="card-body text-center text-break">
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
{# Preview note profile (picture, username and balance) #}
|
{# Preview note profile (picture, username and balance) #}
|
||||||
<div class="col-md-3" id="note_infos_div">
|
<div class="col-md-3" id="note_infos_div">
|
||||||
<div class="card bg-light border-success shadow mb-4">
|
<div class="card bg-light border-success shadow mb-4">
|
||||||
<a id="profile_pic_link" href="#"><img src="/media/pic/default.png"
|
<a id="profile_pic_link" href="#"><img src="{% static "member/img/default_picture.png" %}"
|
||||||
id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a>
|
id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<span id="user_note"></span>
|
<span id="user_note"></span>
|
||||||
|
|
|
@ -2567,6 +2567,70 @@
|
||||||
"description": "(Dé)bloquer sa propre note et modifier la raison"
|
"description": "(Dé)bloquer sa propre note et modifier la raison"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 165,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"auth",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"query": "{}",
|
||||||
|
"type": "change",
|
||||||
|
"mask": 1,
|
||||||
|
"field": "password",
|
||||||
|
"permanent": true,
|
||||||
|
"description": "Changer son mot de passe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 166,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"note",
|
||||||
|
"transaction"
|
||||||
|
],
|
||||||
|
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]",
|
||||||
|
"type": "add",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Créer une transaction quelconque tant que la source reste au-dessus de -50 €"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 167,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"note",
|
||||||
|
"transaction"
|
||||||
|
],
|
||||||
|
"query": "[\"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",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Modifier le statut de validation d'une transaction si c'est possible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 168,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"note",
|
||||||
|
"transaction"
|
||||||
|
],
|
||||||
|
"query": "[\"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": "invalidity_reason",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Modifier la raison d'invalidité d'une transaction si c'est possible"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model": "permission.role",
|
"model": "permission.role",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
|
@ -2591,7 +2655,8 @@
|
||||||
52,
|
52,
|
||||||
126,
|
126,
|
||||||
161,
|
161,
|
||||||
162
|
162,
|
||||||
|
165
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2697,7 +2762,11 @@
|
||||||
127,
|
127,
|
||||||
133,
|
133,
|
||||||
141,
|
141,
|
||||||
142
|
142,
|
||||||
|
150,
|
||||||
|
166,
|
||||||
|
167,
|
||||||
|
168
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2711,8 +2780,7 @@
|
||||||
24,
|
24,
|
||||||
25,
|
25,
|
||||||
26,
|
26,
|
||||||
27,
|
27
|
||||||
33
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2932,7 +3000,8 @@
|
||||||
161,
|
161,
|
||||||
162,
|
162,
|
||||||
163,
|
163,
|
||||||
164
|
164,
|
||||||
|
165
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2944,7 +3013,6 @@
|
||||||
"name": "GC Kfet",
|
"name": "GC Kfet",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
32,
|
32,
|
||||||
33,
|
|
||||||
56,
|
56,
|
||||||
58,
|
58,
|
||||||
55,
|
55,
|
||||||
|
@ -2959,7 +3027,10 @@
|
||||||
29,
|
29,
|
||||||
30,
|
30,
|
||||||
31,
|
31,
|
||||||
143
|
143,
|
||||||
|
166,
|
||||||
|
167,
|
||||||
|
168
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
|
@ -56,29 +57,29 @@ class PermissionQueryTestCase(TestCase):
|
||||||
We use a random user with a random WEIClub (to use permissions for the WEI) in a random team in a random bus.
|
We use a random user with a random WEIClub (to use permissions for the WEI) in a random team in a random bus.
|
||||||
"""
|
"""
|
||||||
for perm in Permission.objects.all():
|
for perm in Permission.objects.all():
|
||||||
instanced = perm.about(
|
|
||||||
user=User.objects.get(),
|
|
||||||
club=WEIClub.objects.get(),
|
|
||||||
membership=Membership.objects.get(),
|
|
||||||
User=User,
|
|
||||||
Club=Club,
|
|
||||||
Membership=Membership,
|
|
||||||
Note=Note,
|
|
||||||
NoteUser=NoteUser,
|
|
||||||
NoteClub=NoteClub,
|
|
||||||
NoteSpecial=NoteSpecial,
|
|
||||||
F=F,
|
|
||||||
Q=Q,
|
|
||||||
now=timezone.now(),
|
|
||||||
today=date.today(),
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
|
instanced = perm.about(
|
||||||
|
user=User.objects.get(),
|
||||||
|
club=WEIClub.objects.get(),
|
||||||
|
membership=Membership.objects.get(),
|
||||||
|
User=User,
|
||||||
|
Club=Club,
|
||||||
|
Membership=Membership,
|
||||||
|
Note=Note,
|
||||||
|
NoteUser=NoteUser,
|
||||||
|
NoteClub=NoteClub,
|
||||||
|
NoteSpecial=NoteSpecial,
|
||||||
|
F=F,
|
||||||
|
Q=Q,
|
||||||
|
now=timezone.now(),
|
||||||
|
today=date.today(),
|
||||||
|
)
|
||||||
instanced.update_query()
|
instanced.update_query()
|
||||||
query = instanced.query
|
query = instanced.query
|
||||||
model = perm.model.model_class()
|
model = perm.model.model_class()
|
||||||
model.objects.filter(query).all()
|
model.objects.filter(query).all()
|
||||||
# print("Good query for permission", perm)
|
# print("Good query for permission", perm)
|
||||||
except (FieldError, AttributeError, ValueError, TypeError):
|
except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError):
|
||||||
print("Query error for permission", perm)
|
print("Query error for permission", perm)
|
||||||
print("Query:", perm.query)
|
print("Query:", perm.query)
|
||||||
if instanced.query:
|
if instanced.query:
|
||||||
|
|
|
@ -0,0 +1,386 @@
|
||||||
|
# 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.db.models import Q
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.http import urlsafe_base64_encode
|
||||||
|
from member.models import Club, Membership
|
||||||
|
from note.models import NoteUser, NoteSpecial, Transaction
|
||||||
|
from registration.tokens import email_validation_token
|
||||||
|
from treasury.models import SogeCredit
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check that pre-registrations and validations are working as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestSignup(TestCase):
|
||||||
|
"""
|
||||||
|
Assume we are a new user.
|
||||||
|
Check that it can pre-register without any problem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ("initial", )
|
||||||
|
|
||||||
|
def test_signup(self):
|
||||||
|
"""
|
||||||
|
A first year member signs up and validates its email address.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:signup"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Signup
|
||||||
|
response = self.client.post(reverse("registration:signup"), dict(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
username="toto",
|
||||||
|
email="toto@example.com",
|
||||||
|
password1="toto1234",
|
||||||
|
password2="toto1234",
|
||||||
|
phone_number="+33123456789",
|
||||||
|
department="EXT",
|
||||||
|
promotion=Club.objects.get(name="BDE").membership_start.year,
|
||||||
|
address="Earth",
|
||||||
|
paid=False,
|
||||||
|
ml_events_registration="en",
|
||||||
|
ml_sport_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="toto").exists())
|
||||||
|
user = User.objects.get(username="toto")
|
||||||
|
# A preregistred user has no note
|
||||||
|
self.assertFalse(NoteUser.objects.filter(user=user).exists())
|
||||||
|
self.assertFalse(user.profile.registration_valid)
|
||||||
|
self.assertFalse(user.profile.email_confirmed)
|
||||||
|
self.assertFalse(user.is_active)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("registration:email_validation_sent"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Check that the email validation link is valid
|
||||||
|
token = email_validation_token.make_token(user)
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||||
|
response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
user.profile.refresh_from_db()
|
||||||
|
self.assertTrue(user.profile.email_confirmed)
|
||||||
|
|
||||||
|
# Token has expired
|
||||||
|
response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token)))
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
# Uid does not exist
|
||||||
|
response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=0, token="toto")))
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_invalid_signup(self):
|
||||||
|
"""
|
||||||
|
Send wrong data and check that it is not valid
|
||||||
|
"""
|
||||||
|
User.objects.create_superuser(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
username="toto",
|
||||||
|
email="toto@example.com",
|
||||||
|
password="toto1234",
|
||||||
|
)
|
||||||
|
|
||||||
|
# The email is already used
|
||||||
|
response = self.client.post(reverse("registration:signup"), dict(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
username="tôtö",
|
||||||
|
email="toto@example.com",
|
||||||
|
password1="toto1234",
|
||||||
|
password2="toto1234",
|
||||||
|
phone_number="+33123456789",
|
||||||
|
department="EXT",
|
||||||
|
promotion=Club.objects.get(name="BDE").membership_start.year,
|
||||||
|
address="Earth",
|
||||||
|
paid=False,
|
||||||
|
ml_events_registration="en",
|
||||||
|
ml_sport_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
))
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
|
# The username is similar to a known alias
|
||||||
|
response = self.client.post(reverse("registration:signup"), dict(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
username="tôtö",
|
||||||
|
email="othertoto@example.com",
|
||||||
|
password1="toto1234",
|
||||||
|
password2="toto1234",
|
||||||
|
phone_number="+33123456789",
|
||||||
|
department="EXT",
|
||||||
|
promotion=Club.objects.get(name="BDE").membership_start.year,
|
||||||
|
address="Earth",
|
||||||
|
paid=False,
|
||||||
|
ml_events_registration="en",
|
||||||
|
ml_sport_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
))
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
|
# The phone number is invalid
|
||||||
|
response = self.client.post(reverse("registration:signup"), dict(
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
username="Ihaveanotherusername",
|
||||||
|
email="othertoto@example.com",
|
||||||
|
password1="toto1234",
|
||||||
|
password2="toto1234",
|
||||||
|
phone_number="invalid phone number",
|
||||||
|
department="EXT",
|
||||||
|
promotion=Club.objects.get(name="BDE").membership_start.year,
|
||||||
|
address="Earth",
|
||||||
|
paid=False,
|
||||||
|
ml_events_registration="en",
|
||||||
|
ml_sport_registration=True,
|
||||||
|
ml_art_registration=True,
|
||||||
|
))
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateRegistration(TestCase):
|
||||||
|
"""
|
||||||
|
Test the admin interface to validate users
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.superuser = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="toto1234",
|
||||||
|
email="admin.toto@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
self.user = User.objects.create(
|
||||||
|
username="toto",
|
||||||
|
first_name="Toto",
|
||||||
|
last_name="TOTO",
|
||||||
|
email="toto@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
def test_future_user_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of pre-registered users
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:future_user_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("registration:future_user_list") + "?search=toto")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invalid_registrations(self):
|
||||||
|
"""
|
||||||
|
Send wrong data and check that errors are detected
|
||||||
|
"""
|
||||||
|
|
||||||
|
# BDE Membership is mandatory
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
|
credit_amount=4200,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=False,
|
||||||
|
join_Kfet=False,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["form"].errors)
|
||||||
|
|
||||||
|
# Same
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type="",
|
||||||
|
credit_amount=0,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=False,
|
||||||
|
join_Kfet=True,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["form"].errors)
|
||||||
|
|
||||||
|
# The BDE membership is not free
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
|
credit_amount=0,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="J'ai pas d'argent",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=True,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["form"].errors)
|
||||||
|
|
||||||
|
# Last and first names are required for a credit
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
|
credit_amount=4000,
|
||||||
|
last_name="",
|
||||||
|
first_name="",
|
||||||
|
bank="",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=True,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["form"].errors)
|
||||||
|
|
||||||
|
# The username admïntoto is too similar with the alias admintoto.
|
||||||
|
# Since the form is valid, the user must update its username.
|
||||||
|
self.user.username = "admïntoto"
|
||||||
|
self.user.save()
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
|
credit_amount=500,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=False,
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["form"].errors)
|
||||||
|
|
||||||
|
def test_validate_bde_registration(self):
|
||||||
|
"""
|
||||||
|
The user wants only to join the BDE. We validate the registration.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
self.user.profile.email_confirmed = True
|
||||||
|
self.user.profile.save()
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
|
credit_amount=500,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=False,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
self.user.profile.refresh_from_db()
|
||||||
|
self.assertTrue(self.user.profile.registration_valid)
|
||||||
|
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
|
||||||
|
self.assertFalse(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
|
||||||
|
self.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
|
||||||
|
self.assertEqual(Transaction.objects.filter(
|
||||||
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2)
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_validate_kfet_registration(self):
|
||||||
|
"""
|
||||||
|
The user joins the BDE and the Kfet.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
self.user.profile.email_confirmed = True
|
||||||
|
self.user.profile.save()
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=False,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
|
credit_amount=4000,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
self.user.profile.refresh_from_db()
|
||||||
|
self.assertTrue(self.user.profile.registration_valid)
|
||||||
|
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
|
||||||
|
self.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
|
||||||
|
self.assertEqual(Transaction.objects.filter(
|
||||||
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_validate_kfet_registration_with_soge(self):
|
||||||
|
"""
|
||||||
|
The user joins the BDE and the Kfet, but the membership is paid by the Société générale.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
self.user.profile.email_confirmed = True
|
||||||
|
self.user.profile.save()
|
||||||
|
|
||||||
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
|
soge=True,
|
||||||
|
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
|
credit_amount=4000,
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
join_BDE=True,
|
||||||
|
join_Kfet=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
|
self.user.profile.refresh_from_db()
|
||||||
|
self.assertTrue(self.user.profile.registration_valid)
|
||||||
|
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
|
||||||
|
self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
|
||||||
|
self.assertTrue(SogeCredit.objects.filter(user=self.user).exists())
|
||||||
|
self.assertEqual(Transaction.objects.filter(
|
||||||
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2)
|
||||||
|
self.assertFalse(Transaction.objects.filter(valid=True).exists())
|
||||||
|
|
||||||
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invalidate_registration(self):
|
||||||
|
"""
|
||||||
|
Try to invalidate (= delete) pre-registration.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:future_user_invalidate", args=(self.user.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("registration:future_user_list"), 302, 200)
|
||||||
|
self.assertFalse(User.objects.filter(pk=self.user.pk).exists())
|
||||||
|
|
||||||
|
def test_resend_email_validation_link(self):
|
||||||
|
"""
|
||||||
|
Resend email validation linK.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("registration:email_validation_resend", args=(self.user.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("registration:future_user_detail", args=(self.user.pk,)), 302, 200)
|
|
@ -16,7 +16,7 @@ from django.views.generic.edit import FormMixin
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from member.forms import ProfileForm
|
from member.forms import ProfileForm
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
from note.models import SpecialTransaction
|
from note.models import SpecialTransaction, Alias
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
|
@ -101,7 +101,7 @@ class UserValidateView(TemplateView):
|
||||||
user.profile.email_confirmed = True
|
user.profile.email_confirmed = True
|
||||||
user.save()
|
user.save()
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
return self.render_to_response(self.get_context_data())
|
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
|
||||||
|
|
||||||
def get_user(self, uidb64):
|
def get_user(self, uidb64):
|
||||||
"""
|
"""
|
||||||
|
@ -169,12 +169,9 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
|
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
if not pattern:
|
|
||||||
return qs.none()
|
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(first_name__iregex=pattern)
|
Q(first_name__iregex=pattern)
|
||||||
| Q(last_name__iregex=pattern)
|
| Q(last_name__iregex=pattern)
|
||||||
|
@ -205,10 +202,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if form.is_valid():
|
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
|
||||||
return self.form_valid(form)
|
|
||||||
else:
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -239,6 +233,10 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
|
|
||||||
|
if Alias.objects.filter(normalized_name=Alias.normalize(user.username)).exists():
|
||||||
|
form.add_error(None, _("An alias with a similar name already exists."))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
soge = form.cleaned_data["soge"]
|
soge = form.cleaned_data["soge"]
|
||||||
credit_type = form.cleaned_data["credit_type"]
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
|
@ -276,9 +274,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
if credit_type is None:
|
if credit_type is None:
|
||||||
credit_amount = 0
|
credit_amount = 0
|
||||||
|
|
||||||
if join_Kfet and not join_BDE:
|
|
||||||
form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club."))
|
|
||||||
|
|
||||||
if fee > credit_amount and not soge:
|
if fee > credit_amount and not soge:
|
||||||
# Check if the user credits enough money
|
# Check if the user credits enough money
|
||||||
form.add_error('credit_type',
|
form.add_error('credit_type',
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c1c0a8797179d110ad919912378f05b030f44f61
|
Subproject commit 4e1bcd1808a24b532aa27bf2a119f6f8155af534
|
|
@ -24,9 +24,7 @@ class RemittanceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
|
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not obj:
|
return not obj or (not obj.closed and super().has_change_permission(request, obj))
|
||||||
return True
|
|
||||||
return not obj.closed and super().has_change_permission(request, obj)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SogeCredit, site=admin_site)
|
@admin.register(SogeCredit, site=admin_site)
|
||||||
|
|
|
@ -16,7 +16,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||||
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/invoice/
|
then render it on /api/treasury/invoice/
|
||||||
"""
|
"""
|
||||||
queryset = Invoice.objects.all()
|
queryset = Invoice.objects.order_by("id").all()
|
||||||
serializer_class = InvoiceSerializer
|
serializer_class = InvoiceSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['bde', ]
|
filterset_fields = ['bde', ]
|
||||||
|
@ -28,7 +28,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
|
||||||
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/product/
|
then render it on /api/treasury/product/
|
||||||
"""
|
"""
|
||||||
queryset = Product.objects.all()
|
queryset = Product.objects.order_by("invoice_id", "id").all()
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [SearchFilter]
|
||||||
search_fields = ['$designation', ]
|
search_fields = ['$designation', ]
|
||||||
|
@ -40,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||||
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
|
||||||
then render it on /api/treasury/remittance_type/
|
then render it on /api/treasury/remittance_type/
|
||||||
"""
|
"""
|
||||||
queryset = RemittanceType.objects
|
queryset = RemittanceType.objects.order_by("id")
|
||||||
serializer_class = RemittanceTypeSerializer
|
serializer_class = RemittanceTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||||
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/remittance/
|
then render it on /api/treasury/remittance/
|
||||||
"""
|
"""
|
||||||
queryset = Remittance.objects
|
queryset = Remittance.objects.order_by("id")
|
||||||
serializer_class = RemittanceSerializer
|
serializer_class = RemittanceSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,5 +60,5 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
|
||||||
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/treasury/soge_credit/
|
then render it on /api/treasury/soge_credit/
|
||||||
"""
|
"""
|
||||||
queryset = SogeCredit.objects
|
queryset = SogeCredit.objects.order_by("id")
|
||||||
serializer_class = SogeCreditSerializer
|
serializer_class = SogeCreditSerializer
|
||||||
|
|
|
@ -16,21 +16,15 @@ class InvoiceForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
# If the invoice is locked, it can't be updated.
|
||||||
if self.instance and self.instance.locked:
|
if self.instance and self.instance.locked:
|
||||||
for field_name in self.fields:
|
for field_name in self.fields:
|
||||||
self.cleaned_data[field_name] = getattr(self.instance, field_name)
|
self.cleaned_data[field_name] = getattr(self.instance, field_name)
|
||||||
self.errors.clear()
|
self.errors.clear()
|
||||||
|
self.add_error(None, _('This invoice is locked and can no longer be edited.'))
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
"""
|
|
||||||
If the invoice is locked, don't save it
|
|
||||||
"""
|
|
||||||
if not self.instance.locked:
|
|
||||||
super().save(commit)
|
|
||||||
return self.instance
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
exclude = ('bde', 'date', 'tex', )
|
exclude = ('bde', 'date', 'tex', )
|
||||||
|
|
|
@ -85,7 +85,7 @@ class Invoice(models.Model):
|
||||||
|
|
||||||
old_invoice = Invoice.objects.filter(id=self.id)
|
old_invoice = Invoice.objects.filter(id=self.id)
|
||||||
if old_invoice.exists():
|
if old_invoice.exists():
|
||||||
if old_invoice.get().locked:
|
if old_invoice.get().locked and not self._force_save:
|
||||||
raise ValidationError(_("This invoice is locked and can no longer be edited."))
|
raise ValidationError(_("This invoice is locked and can no longer be edited."))
|
||||||
|
|
||||||
products = self.products.all()
|
products = self.products.all()
|
||||||
|
@ -224,7 +224,7 @@ class Remittance(models.Model):
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
# Check if all transactions have the right type.
|
# Check if all transactions have the right type.
|
||||||
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
||||||
raise ValidationError("All transactions in a remittance must have the same type")
|
raise ValidationError("All transactions in a remittance must have the same type")
|
||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 752 KiB |
Before Width: | Height: | Size: 664 KiB After Width: | Height: | Size: 664 KiB |
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 414 KiB |
Before Width: | Height: | Size: 375 KiB After Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
|
@ -34,7 +34,7 @@ class InvoiceTable(tables.Table):
|
||||||
|
|
||||||
delete = tables.LinkColumn(
|
delete = tables.LinkColumn(
|
||||||
'treasury:invoice_delete',
|
'treasury:invoice_delete',
|
||||||
args=[A('pk')],
|
args=[A('id')],
|
||||||
verbose_name=_("delete"),
|
verbose_name=_("delete"),
|
||||||
text=_("Delete"),
|
text=_("Delete"),
|
||||||
attrs={
|
attrs={
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="#" class="btn btn-sm btn-outline-primary active">
|
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
\parbox[b][\paperheight]{\paperwidth}{%
|
\parbox[b][\paperheight]{\paperwidth}{%
|
||||||
\vfill
|
\vfill
|
||||||
\centering
|
\centering
|
||||||
{\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}%
|
{\transparent{0.1}\includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}}}%
|
||||||
\vfill
|
\vfill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Invoice" %}s
|
{% trans "Invoice" %}s
|
||||||
</a>
|
</a>
|
||||||
|
@ -59,9 +59,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
function reloadTable() {
|
function reloadTable() {
|
||||||
let pattern = searchbar_obj.val();
|
let pattern = searchbar_obj.val();
|
||||||
|
|
||||||
if (pattern === old_pattern || pattern === "")
|
|
||||||
return;
|
|
||||||
|
|
||||||
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
|
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
|
||||||
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,403 @@
|
||||||
|
# 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.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from member.models import Membership, Club
|
||||||
|
from note.models import SpecialTransaction, NoteSpecial, Transaction
|
||||||
|
from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvoices(TestCase):
|
||||||
|
"""
|
||||||
|
Check that invoices can be created and rendered properly.
|
||||||
|
"""
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.invoice = Invoice.objects.create(
|
||||||
|
id=1,
|
||||||
|
object="Object",
|
||||||
|
description="Description",
|
||||||
|
name="Me",
|
||||||
|
address="Earth",
|
||||||
|
acquitted=False,
|
||||||
|
)
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
invoice=self.invoice,
|
||||||
|
designation="Product",
|
||||||
|
quantity=3,
|
||||||
|
amount=3.14,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Display the invoice admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/invoice/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoices_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of invoices.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoice_create(self):
|
||||||
|
"""
|
||||||
|
Try to create a new invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:invoice_create"), data={
|
||||||
|
"id": 42,
|
||||||
|
"object": "Same object",
|
||||||
|
"description": "Longer description",
|
||||||
|
"name": "Me and others",
|
||||||
|
"address": "Alwways earth",
|
||||||
|
"acquitted": True,
|
||||||
|
"products-0-designation": "Designation",
|
||||||
|
"products-0-quantity": 1,
|
||||||
|
"products-0-amount": 42,
|
||||||
|
"products-TOTAL_FORMS": 1,
|
||||||
|
"products-INITIAL_FORMS": 0,
|
||||||
|
"products-MIN_NUM_FORMS": 0,
|
||||||
|
"products-MAX_NUM_FORMS": 1000,
|
||||||
|
})
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.assertTrue(Invoice.objects.filter(object="Same object", id=42).exists())
|
||||||
|
self.assertTrue(Product.objects.filter(designation="Designation", invoice_id=42).exists())
|
||||||
|
self.assertTrue(Invoice.objects.get(id=42).tex)
|
||||||
|
|
||||||
|
def test_invoice_update(self):
|
||||||
|
"""
|
||||||
|
Try to update an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"object": "Same object",
|
||||||
|
"description": "Longer description",
|
||||||
|
"name": "Me and others",
|
||||||
|
"address": "Always earth",
|
||||||
|
"acquitted": True,
|
||||||
|
"locked": True,
|
||||||
|
"products-0-designation": "Designation",
|
||||||
|
"products-0-quantity": 1,
|
||||||
|
"products-0-amount": 4200,
|
||||||
|
"products-1-designation": "Second designation",
|
||||||
|
"products-1-quantity": 5,
|
||||||
|
"products-1-amount": -1800,
|
||||||
|
"products-TOTAL_FORMS": 2,
|
||||||
|
"products-INITIAL_FORMS": 0,
|
||||||
|
"products-MIN_NUM_FORMS": 0,
|
||||||
|
"products-MAX_NUM_FORMS": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data)
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.invoice.refresh_from_db()
|
||||||
|
self.assertTrue(Invoice.objects.filter(pk=1, object="Same object", locked=True).exists())
|
||||||
|
self.assertTrue(Product.objects.filter(designation="Second designation", invoice_id=1).exists())
|
||||||
|
|
||||||
|
# Resend the same data, but the invoice is locked.
|
||||||
|
response = self.client.get(reverse("treasury:invoice_update", args=(self.invoice.id,)))
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
response = self.client.post(reverse("treasury:invoice_update", args=(self.invoice.id,)), data=data)
|
||||||
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_delete_invoice(self):
|
||||||
|
"""
|
||||||
|
Try to delete an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Can't delete a locked invoice
|
||||||
|
self.invoice.locked = True
|
||||||
|
self.invoice.save()
|
||||||
|
response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(Invoice.objects.filter(pk=self.invoice.id).exists())
|
||||||
|
|
||||||
|
# Unlock invoice and truly delete it.
|
||||||
|
self.invoice.locked = False
|
||||||
|
self.invoice._force_save = True
|
||||||
|
self.invoice.save()
|
||||||
|
response = self.client.delete(reverse("treasury:invoice_delete", args=(self.invoice.id,)))
|
||||||
|
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
|
||||||
|
self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists())
|
||||||
|
|
||||||
|
def test_invoice_render_pdf(self):
|
||||||
|
"""
|
||||||
|
Generate the PDF file of an invoice.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:invoice_render", args=(self.invoice.id,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/invoice/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get("/api/treasury/product/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemittances(TestCase):
|
||||||
|
"""
|
||||||
|
Create some credits and close remittances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.credit = SpecialTransaction.objects.create(
|
||||||
|
source=NoteSpecial.objects.get(special_type="Chèque"),
|
||||||
|
destination=self.user.note,
|
||||||
|
amount=4200,
|
||||||
|
reason="Credit",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.second_credit = SpecialTransaction.objects.create(
|
||||||
|
source=self.user.note,
|
||||||
|
destination=NoteSpecial.objects.get(special_type="Chèque"),
|
||||||
|
amount=424200,
|
||||||
|
reason="Second credit",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
bank="Société générale",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remittance = Remittance.objects.create(
|
||||||
|
remittance_type=RemittanceType.objects.get(),
|
||||||
|
comment="Test remittance",
|
||||||
|
closed=False,
|
||||||
|
)
|
||||||
|
self.credit.specialtransactionproxy.remittance = self.remittance
|
||||||
|
self.credit.specialtransactionproxy.save()
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Load the admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/remittance/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_remittances_list(self):
|
||||||
|
"""
|
||||||
|
Display the remittance list.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_list"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_remittance_create(self):
|
||||||
|
"""
|
||||||
|
Create a new Remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_create"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_create"), data=dict(
|
||||||
|
remittance_type=RemittanceType.objects.get().pk,
|
||||||
|
comment="Created remittance",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Created remittance").exists())
|
||||||
|
|
||||||
|
def test_remittance_update(self):
|
||||||
|
"""
|
||||||
|
Update an existing remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict(
|
||||||
|
comment="Updated remittance",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Updated remittance").exists())
|
||||||
|
|
||||||
|
def test_remittance_close(self):
|
||||||
|
"""
|
||||||
|
Try to close an open remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:remittance_update", args=(self.remittance.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:remittance_update", args=(self.remittance.pk,)), data=dict(
|
||||||
|
comment="Closed remittance",
|
||||||
|
close=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.assertTrue(Remittance.objects.filter(comment="Closed remittance", closed=True).exists())
|
||||||
|
|
||||||
|
def test_remittance_link_transaction(self):
|
||||||
|
"""
|
||||||
|
Link a transaction to an open remittance.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:link_transaction", args=(self.credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:link_transaction", args=(self.credit.pk,)), data=dict(
|
||||||
|
remittance=self.remittance.pk,
|
||||||
|
last_name="Last Name",
|
||||||
|
first_name="First Name",
|
||||||
|
bank="Bank",
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
self.credit.refresh_from_db()
|
||||||
|
self.assertEqual(self.credit.last_name, "Last Name")
|
||||||
|
self.assertEqual(self.remittance.transactions.count(), 1)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:unlink_transaction", args=(self.credit.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("treasury:remittance_list"), 302, 200)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/remittance_type/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get("/api/treasury/remittance/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSogeCredits(TestCase):
|
||||||
|
"""
|
||||||
|
Check that credits from the Société générale are working correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="admintoto",
|
||||||
|
password="totototo",
|
||||||
|
email="admin@example.com",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
self.kfet = Club.objects.get(name="Kfet")
|
||||||
|
self.bde = self.kfet.parent_club
|
||||||
|
|
||||||
|
self.kfet_membership = Membership(
|
||||||
|
user=self.user,
|
||||||
|
club=self.kfet,
|
||||||
|
)
|
||||||
|
self.kfet_membership._force_renew_parent = True
|
||||||
|
self.kfet_membership._soge = True
|
||||||
|
self.kfet_membership.save()
|
||||||
|
|
||||||
|
def test_admin_page(self):
|
||||||
|
"""
|
||||||
|
Render the admin page.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("admin:index") + "treasury/sogecredit/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_sogecredit_list(self):
|
||||||
|
"""
|
||||||
|
Display the list of all credits.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("treasury:soge_credits"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("treasury:soge_credits") + "?search=toto&valid=")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_validate_soge_credit(self):
|
||||||
|
"""
|
||||||
|
Try to validate a credit.
|
||||||
|
"""
|
||||||
|
soge_credit = SogeCredit.objects.get(user=self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(
|
||||||
|
validate=True,
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), 302, 200)
|
||||||
|
soge_credit.refresh_from_db()
|
||||||
|
self.assertTrue(soge_credit.valid)
|
||||||
|
self.user.note.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.note.balance, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
|
self.assertTrue(self.user.profile.soge)
|
||||||
|
|
||||||
|
def test_delete_soge_credit(self):
|
||||||
|
"""
|
||||||
|
Try to invalidate a credit.
|
||||||
|
"""
|
||||||
|
soge_credit = SogeCredit.objects.get(user=self.user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
|
||||||
|
raise AssertionError("It is not possible to delete the soge credit until the note is not credited.")
|
||||||
|
except ValidationError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SpecialTransaction.objects.create(
|
||||||
|
source=NoteSpecial.objects.get(special_type="Carte bancaire"),
|
||||||
|
destination=self.user.note,
|
||||||
|
amount=self.bde.membership_fee_paid + self.kfet.membership_fee_paid,
|
||||||
|
quantity=1,
|
||||||
|
reason="Registration is not complete, pliz pay",
|
||||||
|
last_name="TOTO",
|
||||||
|
first_name="Toto",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)),
|
||||||
|
data=dict(delete=True))
|
||||||
|
# 403 because no SogeCredit exists anymore, then a PermissionDenied is raised
|
||||||
|
self.assertRedirects(response, reverse("treasury:soge_credits"), 302, 403)
|
||||||
|
self.assertFalse(SogeCredit.objects.filter(pk=soge_credit.pk))
|
||||||
|
self.user.note.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.note.balance, 0)
|
||||||
|
self.assertEqual(
|
||||||
|
Transaction.objects.filter(Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
|
self.assertFalse(self.user.profile.soge)
|
||||||
|
|
||||||
|
def test_invoice_api(self):
|
||||||
|
"""
|
||||||
|
Load some API pages
|
||||||
|
"""
|
||||||
|
response = self.client.get("/api/treasury/soge_credit/")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
|
@ -60,6 +60,11 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
del form.fields["locked"]
|
||||||
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
|
@ -134,6 +139,11 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_form(self, form_class=None):
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
del form.fields["id"]
|
||||||
|
return form
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
|
@ -165,6 +175,11 @@ class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||||
model = Invoice
|
model = Invoice
|
||||||
extra_context = {"title": _("Delete invoice")}
|
extra_context = {"title": _("Delete invoice")}
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
if self.get_object().locked:
|
||||||
|
raise PermissionDenied(_("This invoice is locked and can't be deleted."))
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:invoice_list')
|
return reverse_lazy('treasury:invoice_list')
|
||||||
|
|
||||||
|
@ -387,7 +402,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
if not self.get_queryset().exists():
|
if not super().get_queryset().exists():
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@ function displayStyle (note) {
|
||||||
*/
|
*/
|
||||||
function displayNote (note, alias, user_note_field = null, profile_pic_field = null) {
|
function displayNote (note, alias, user_note_field = null, profile_pic_field = null) {
|
||||||
if (!note.display_image) {
|
if (!note.display_image) {
|
||||||
note.display_image = '/media/pic/default.png';
|
note.display_image = '/static/member/img/default_picture.png';
|
||||||
}
|
}
|
||||||
let img = note.display_image;
|
let img = note.display_image;
|
||||||
if (alias !== note.name && note.name)
|
if (alias !== note.name && note.name)
|
||||||
|
|
|
@ -27,7 +27,7 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Switching in double consumptions mode should update the layout
|
// Switching in double consumptions mode should update the layout
|
||||||
$("#double_conso").click(function() {
|
$("#double_conso").change(function() {
|
||||||
$("#consos_list_div").removeClass('d-none');
|
$("#consos_list_div").removeClass('d-none');
|
||||||
$("#user_select_div").attr('class', 'col-xl-4');
|
$("#user_select_div").attr('class', 'col-xl-4');
|
||||||
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
|
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
|
||||||
|
@ -47,7 +47,7 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#single_conso").click(function() {
|
$("#single_conso").change(function() {
|
||||||
$("#consos_list_div").addClass('d-none');
|
$("#consos_list_div").addClass('d-none');
|
||||||
$("#user_select_div").attr('class', 'col-xl-7');
|
$("#user_select_div").attr('class', 'col-xl-7');
|
||||||
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
|
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
|
||||||
|
@ -158,7 +158,7 @@ function reset() {
|
||||||
$("#consos_list").html("");
|
$("#consos_list").html("");
|
||||||
$("#note").val("");
|
$("#note").val("");
|
||||||
$("#note").attr("data-original-title", "").tooltip("hide");
|
$("#note").attr("data-original-title", "").tooltip("hide");
|
||||||
$("#profile_pic").attr("src", "/media/pic/default.png");
|
$("#profile_pic").attr("src", "/static/member/img/default_picture.png");
|
||||||
$("#profile_pic_link").attr("href", "#");
|
$("#profile_pic_link").attr("href", "#");
|
||||||
refreshHistory();
|
refreshHistory();
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Konami code support
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Cursor denote the position in konami code
|
||||||
|
let cursor = 0
|
||||||
|
const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
|
||||||
|
|
||||||
|
function afterKonami() {
|
||||||
|
// Load Rythm.js
|
||||||
|
var rythmScript = document.createElement('script')
|
||||||
|
rythmScript.setAttribute('src','//unpkg.com/rythm.js@2.2.5/rythm.min.js')
|
||||||
|
document.head.appendChild(rythmScript)
|
||||||
|
|
||||||
|
rythmScript.addEventListener('load', function() {
|
||||||
|
// Ker-Lyon audio courtesy of @adalan, ker-lyon.fr
|
||||||
|
const audioElement = new Audio('/static/song/konami.ogg')
|
||||||
|
audioElement.loop = true
|
||||||
|
audioElement.play()
|
||||||
|
|
||||||
|
const rythm = new Rythm()
|
||||||
|
rythm.connectExternalAudioElement(audioElement)
|
||||||
|
rythm.addRythm('card', 'pulse', 50, 50, {
|
||||||
|
min: 1,
|
||||||
|
max: 1.1
|
||||||
|
})
|
||||||
|
rythm.addRythm('d-flex', 'color', 50, 50, {
|
||||||
|
from: [64,64,64],
|
||||||
|
to:[128,64,128]
|
||||||
|
})
|
||||||
|
rythm.addRythm('nav-link', 'jump', 150, 50, {
|
||||||
|
min: 0,
|
||||||
|
max: 10
|
||||||
|
})
|
||||||
|
rythm.start()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register custom event
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
cursor = (e.keyCode == KONAMI_CODE[cursor]) ? cursor + 1 : 0;
|
||||||
|
if (cursor == KONAMI_CODE.length) {
|
||||||
|
afterKonami()
|
||||||
|
}
|
||||||
|
});
|
|
@ -40,7 +40,7 @@ function reset(refresh=true) {
|
||||||
$("#first_name").val("");
|
$("#first_name").val("");
|
||||||
$("#bank").val("");
|
$("#bank").val("");
|
||||||
$("#user_note").val("");
|
$("#user_note").val("");
|
||||||
$("#profile_pic").attr("src", "/media/pic/default.png");
|
$("#profile_pic").attr("src", "/static/member/img/default_picture.png");
|
||||||
$("#profile_pic_link").attr("href", "#");
|
$("#profile_pic_link").attr("href", "#");
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
|
@ -96,7 +96,7 @@ $(document).ready(function() {
|
||||||
let source = $("#source_note");
|
let source = $("#source_note");
|
||||||
let dest = $("#dest_note");
|
let dest = $("#dest_note");
|
||||||
|
|
||||||
$("#type_transfer").click(function() {
|
$("#type_transfer").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ $(document).ready(function() {
|
||||||
location.hash = "transfer";
|
location.hash = "transfer";
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_credit").click(function() {
|
$("#type_credit").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ $(document).ready(function() {
|
||||||
location.hash = "credit";
|
location.hash = "credit";
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#type_debit").click(function() {
|
$("#type_debit").change(function() {
|
||||||
if (LOCK)
|
if (LOCK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="{% static "js/base.js" %}"></script>
|
<script src="{% static "js/base.js" %}"></script>
|
||||||
|
<script src="{% static "js/konami.js" %}"></script>
|
||||||
|
|
||||||
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
|
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
|
||||||
{% if form.media %}
|
{% if form.media %}
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
django-htcpcp-tea==0.3.1
|
beautifulsoup4~=4.7.1
|
||||||
django-mailer==2.0.1
|
Django~=2.2.15
|
||||||
django-phonenumber-field==5.0.0
|
django-bootstrap-datepicker-plus~=3.0.5
|
||||||
django-tables2==2.3.1
|
django-cas-server>=0.9.0
|
||||||
django-rest-polymorphic==0.1.9
|
django-colorfield~=0.3.2
|
||||||
django-bootstrap-datepicker-plus==3.0.5
|
django-crispy-forms~=1.7.2
|
||||||
django-colorfield==0.3.2
|
django-extensions~=2.1.4
|
||||||
|
django-filter~=2.1.0
|
||||||
|
django-htcpcp-tea~=0.3.1
|
||||||
|
django-mailer~=2.0.1
|
||||||
|
django-phonenumber-field~=5.0.0
|
||||||
|
django-polymorphic~=2.0.3
|
||||||
|
djangorestframework~=3.9.0
|
||||||
|
django-rest-polymorphic~=0.1.9
|
||||||
|
django-tables2~=2.3.1
|
||||||
|
phonenumbers~=8.9.10
|
||||||
|
Pillow>=5.4.1
|
||||||
|
|
6
tox.ini
|
@ -11,8 +11,6 @@ skipsdist = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
sitepackages = True
|
sitepackages = True
|
||||||
setenv =
|
|
||||||
PYTHONWARNINGS = all
|
|
||||||
deps =
|
deps =
|
||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
coverage
|
coverage
|
||||||
|
@ -23,7 +21,6 @@ commands =
|
||||||
|
|
||||||
[testenv:linters]
|
[testenv:linters]
|
||||||
deps =
|
deps =
|
||||||
-r{toxinidir}/requirements.txt
|
|
||||||
flake8
|
flake8
|
||||||
flake8-colors
|
flake8-colors
|
||||||
flake8-import-order
|
flake8-import-order
|
||||||
|
@ -34,8 +31,7 @@ commands =
|
||||||
flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury apps/wei
|
flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury apps/wei
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
# Ignore too many errors, should be reduced in the future
|
ignore = W503, I100, I101
|
||||||
ignore = D203, W503, E203, I100, I101, C901
|
|
||||||
exclude =
|
exclude =
|
||||||
.tox,
|
.tox,
|
||||||
.git,
|
.git,
|
||||||
|
|