Merge branch 'beta' into 'master'
Beta Closes #59 See merge request bde/nk20!107
@ -40,15 +40,7 @@ linters:
|
||||
stage: quality-assurance
|
||||
image: debian:buster-backports
|
||||
before_script:
|
||||
- >
|
||||
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
|
||||
- apt-get update && apt-get install -y tox
|
||||
script: tox -e linters
|
||||
|
||||
# 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)
|
||||
[![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.
|
||||
Sinon vous pouvez suivre les étapes ici.
|
||||
Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||
|
||||
### Installation avec Debian/Ubuntu
|
||||
|
||||
0. **Activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
||||
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
||||
|
||||
```bash
|
||||
$ 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"
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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."))
|
||||
|
||||
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():
|
||||
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
|
||||
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>
|
||||
<div class="row">
|
||||
<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">
|
||||
{% trans "Transfer" %}
|
||||
</a>
|
||||
|
176
apps/activity/tests/test_activities.py
Normal file
@ -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>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
|
||||
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
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||
from django_tables2.views import SingleTableView
|
||||
from note.models import Alias, NoteSpecial, NoteUser
|
||||
@ -190,10 +194,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
if pattern[0] != "^":
|
||||
pattern = "^" + pattern
|
||||
guest_qs = guest_qs.filter(
|
||||
Q(first_name__regex=pattern)
|
||||
| Q(last_name__regex=pattern)
|
||||
| Q(inviter__alias__name__regex=pattern)
|
||||
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(inviter__alias__name__iregex=pattern)
|
||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
guest_qs = guest_qs.none()
|
||||
@ -226,21 +230,19 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
note_qs = note_qs.filter(
|
||||
Q(note__noteuser__user__first_name__regex=pattern)
|
||||
| Q(note__noteuser__user__last_name__regex=pattern)
|
||||
| Q(name__regex=pattern)
|
||||
| Q(normalized_name__regex=Alias.normalize(pattern))
|
||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||
| Q(name__iregex=pattern)
|
||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
note_qs = note_qs.none()
|
||||
|
||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql':
|
||||
note_qs = note_qs.distinct('note__pk')[:20]
|
||||
else:
|
||||
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
|
||||
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
|
||||
# In production mode, please use PostgreSQL.
|
||||
note_qs = note_qs.distinct()[:20]
|
||||
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
|
||||
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
|
||||
# In production mode, please use PostgreSQL.
|
||||
note_qs = note_qs.distinct('note__pk')[:20]\
|
||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||
return note_qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -281,3 +283,60 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
Entry(activity=a, note=self.request.user.note,))]
|
||||
|
||||
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")
|
||||
|
33
apps/api/serializers.py
Normal file
@ -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.urls import url, include
|
||||
from django.contrib.auth.models import User
|
||||
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 rest_framework import routers
|
||||
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
# Register each app API router and user viewset
|
||||
|
@ -2,12 +2,19 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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 rest_framework import viewsets
|
||||
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.
|
||||
"""
|
||||
@ -19,10 +26,10 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
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.
|
||||
"""
|
||||
@ -34,4 +41,72 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
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
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
if hasattr(instance, "_no_log"):
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
|
||||
return
|
||||
|
||||
# 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
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
if hasattr(instance, "_no_log"):
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
|
||||
return
|
||||
|
||||
# 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
|
||||
"""
|
||||
if not obj:
|
||||
return list()
|
||||
return super().get_inline_instances(request, obj)
|
||||
return super().get_inline_instances(request, obj) if obj else []
|
||||
|
||||
|
||||
@admin.register(Club, site=admin_site)
|
||||
|
@ -172,19 +172,21 @@ class Profile(models.Model):
|
||||
|
||||
def send_email_validation_link(self):
|
||||
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',
|
||||
{
|
||||
'user': self.user,
|
||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||
'token': email_validation_token.make_token(self.user),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
||||
'token': token,
|
||||
'uid': uid,
|
||||
})
|
||||
html = loader.render_to_string('registration/mails/email_validation_email.html',
|
||||
{
|
||||
'user': self.user,
|
||||
'domain': os.getenv("NOTE_URL", "note.example.com"),
|
||||
'token': email_validation_token.make_token(self.user),
|
||||
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
|
||||
'token': token,
|
||||
'uid': uid,
|
||||
})
|
||||
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()
|
||||
|
||||
def renew(self):
|
||||
if Membership.objects.filter(
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
date_start__gte=self.club.membership_start,
|
||||
).exists():
|
||||
# Membership is already renewed
|
||||
return
|
||||
new_membership = Membership(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
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:
|
||||
new_membership._force_renew_parent = True
|
||||
if hasattr(self, '_soge') and self._soge:
|
||||
new_membership._soge = True
|
||||
if hasattr(self, '_force_save') and self._force_save:
|
||||
new_membership._force_save = True
|
||||
new_membership.save()
|
||||
new_membership.roles.set(self.roles.all())
|
||||
new_membership.save()
|
||||
# Membership is not renewed yet
|
||||
new_membership = Membership(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
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:
|
||||
new_membership._force_renew_parent = True
|
||||
if hasattr(self, '_soge') and self._soge:
|
||||
new_membership._soge = True
|
||||
if hasattr(self, '_force_save') and self._force_save:
|
||||
new_membership._force_save = True
|
||||
new_membership.save()
|
||||
new_membership.roles.set(self.roles.all())
|
||||
new_membership.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
|
||||
if self.pk:
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
|
||||
created = not self.pk
|
||||
if created:
|
||||
else:
|
||||
if Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
@ -384,7 +383,7 @@ class Membership(models.Model):
|
||||
).exists():
|
||||
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
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
@ -433,15 +432,10 @@ class Membership(models.Model):
|
||||
raise ValidationError(_('User is not a member of the parent club')
|
||||
+ ' ' + self.club.parent_club.name)
|
||||
|
||||
if self.user.profile.paid:
|
||||
self.fee = self.club.membership_fee_paid
|
||||
else:
|
||||
self.fee = self.club.membership_fee_unpaid
|
||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else 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)
|
||||
else:
|
||||
self.date_end = self.date_start + datetime.timedelta(days=424242)
|
||||
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)
|
||||
if self.club.membership_end is not None and 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
|
||||
"""
|
||||
if raw:
|
||||
# When provisionning data, do not try to autocreate
|
||||
return
|
||||
|
||||
if created and instance.is_active:
|
||||
if not raw and created and instance.is_active:
|
||||
from .models import Profile
|
||||
Profile.objects.get_or_create(user=instance)
|
||||
if instance.is_superuser:
|
||||
instance.profile.email_confirmed = True
|
||||
instance.profile.registration_valid = True
|
||||
instance.profile.save()
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -48,7 +48,9 @@
|
||||
</dl>
|
||||
|
||||
{% if user_object.pk == user_object.pk %}
|
||||
<a class="small float-right text-decoration-none" href="{% url 'member:auth_token' %}">
|
||||
{% trans 'Manage auth token' %}
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
@ -1,4 +1,4 @@
|
||||
{% extends "member/base.html" %}
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from note.models import TransactionTemplate, TemplateCategory
|
||||
from django.urls import reverse
|
||||
|
||||
"""
|
||||
Test that login page still works
|
||||
@ -31,7 +31,20 @@ class TemplateLoggedInTests(TestCase):
|
||||
sess.save()
|
||||
|
||||
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)
|
||||
|
||||
def test_admin_index(self):
|
||||
@ -41,22 +54,3 @@ class TemplateLoggedInTests(TestCase):
|
||||
def test_accounts_password_reset(self):
|
||||
response = self.client.get('/accounts/password_reset/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_logout_page(self):
|
||||
response = self.client.get('/accounts/logout/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_transfer_page(self):
|
||||
response = self.client.get('/note/transfer/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_consos_page(self):
|
||||
# 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)
|
||||
|
405
apps/member/tests/test_memberships.py
Normal file
@ -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(
|
||||
alias__normalized_name=Alias.normalize(new_username))
|
||||
if note.exists() and note.get().user != self.object:
|
||||
form.add_error('username',
|
||||
_("An alias with a similar name already exists."))
|
||||
form.add_error('username', _("An alias with a similar name already exists."))
|
||||
return super().form_invalid(form)
|
||||
# Check if the username is one of user's aliases.
|
||||
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.
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
if self.request.user.is_superuser and self.request.session.get("permission_mask", -1) >= 42:
|
||||
return qs
|
||||
return qs.filter(profile__registration_valid=True)
|
||||
return super().get_queryset().filter(profile__registration_valid=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
@ -204,14 +200,16 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
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"))\
|
||||
.filter(profile__registration_valid=True).order_by("username")
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
.filter(profile__registration_valid=True)
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
# Sqlite doesn't support order by in subqueries
|
||||
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(
|
||||
username__iregex="^" + pattern
|
||||
@ -270,12 +268,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
self.object = self.get_object()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
print('is_invalid')
|
||||
print(form)
|
||||
return self.form_invalid(form)
|
||||
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
image_field = form.cleaned_data['image']
|
||||
@ -320,8 +313,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||
Token.objects.get(user=self.request.user).delete()
|
||||
return redirect(reverse_lazy('member:auth_token') + "?show",
|
||||
permanent=True)
|
||||
return redirect(reverse_lazy('member:auth_token') + "?show")
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@ -351,8 +343,9 @@ class ClubCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
email="",
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
def get_success_url(self):
|
||||
self.object.refresh_from_db()
|
||||
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
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
|
||||
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",
|
||||
user=user,
|
||||
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:
|
||||
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)
|
||||
|
||||
# Now, all is fine, the membership can be created.
|
||||
@ -719,46 +712,38 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
transaction._force_save = True
|
||||
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
|
||||
|
||||
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()
|
||||
elif club.name == "Kfet":
|
||||
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()
|
||||
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() \
|
||||
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||
form.instance.roles.set(member_role)
|
||||
form.instance._force_save = True
|
||||
form.instance.save()
|
||||
|
||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||
# Kfet membership.
|
||||
if soge:
|
||||
# If not already done, create BDE and Kfet memberships
|
||||
bde = Club.objects.get(name="BDE")
|
||||
if soge and club.name == "BDE":
|
||||
kfet = Club.objects.get(name="Kfet")
|
||||
fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||
|
||||
soge_clubs = [bde, kfet]
|
||||
for club in soge_clubs:
|
||||
fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
||||
|
||||
# Get current membership, to get the end date
|
||||
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
|
||||
# Get current membership, to get the end date
|
||||
old_membership = Membership.objects.filter(
|
||||
club=kfet,
|
||||
user=user,
|
||||
).order_by("-date_start")
|
||||
|
||||
if not old_membership.filter(date_start__gte=kfet.membership_start).exists():
|
||||
# If the membership is not already renewed
|
||||
membership = Membership(
|
||||
club=club,
|
||||
club=kfet,
|
||||
user=user,
|
||||
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,
|
||||
)
|
||||
membership._force_save = True
|
||||
@ -767,10 +752,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
membership.refresh_from_db()
|
||||
if old_membership.exists():
|
||||
membership.roles.set(old_membership.get().roles.all())
|
||||
elif c.name == "BDE":
|
||||
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.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||
membership.save()
|
||||
|
||||
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())
|
||||
|
||||
if "roles" in self.request.GET:
|
||||
if not self.request.GET["roles"]:
|
||||
return qs.none()
|
||||
roles_str = self.request.GET["roles"].replace(' ', '').split(',')
|
||||
roles_str = self.request.GET["roles"].replace(' ', '').split(',') if self.request.GET["roles"] else ['0']
|
||||
roles_int = map(int, roles_str)
|
||||
qs = qs.filter(roles__in=roles_int)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
@ -117,6 +117,9 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
|
||||
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", ".*")
|
||||
queryset = queryset.prefetch_related('note')
|
||||
@ -137,7 +140,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
),
|
||||
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):
|
||||
|
@ -356,4 +356,4 @@ class MembershipTransaction(Transaction):
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('membership transaction')
|
||||
return _('membership').capitalize()
|
||||
|
@ -29,6 +29,7 @@ class HistoryTable(tables.Table):
|
||||
source = tables.Column(
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-nowrap",
|
||||
"data-toggle": "tooltip",
|
||||
"title": lambda record: _("used alias").capitalize() + " : " + record.source_alias,
|
||||
}
|
||||
@ -38,15 +39,46 @@ class HistoryTable(tables.Table):
|
||||
destination = tables.Column(
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-nowrap",
|
||||
"data-toggle": "tooltip",
|
||||
"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()
|
||||
|
||||
total = tables.Column() # will use Transaction.total() !!
|
||||
total = tables.Column( # will use Transaction.total() !!
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-nowrap",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
valid = tables.Column(
|
||||
attrs={
|
||||
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="col">
|
||||
<div class="card bg-light border-success mb-4 text-center">
|
||||
<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">
|
||||
</a>
|
||||
<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) #}
|
||||
<div class="col-md-3" id="note_infos_div">
|
||||
<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>
|
||||
<div class="card-body text-center">
|
||||
<span id="user_note"></span>
|
||||
|
@ -2567,6 +2567,70 @@
|
||||
"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",
|
||||
"pk": 1,
|
||||
@ -2591,7 +2655,8 @@
|
||||
52,
|
||||
126,
|
||||
161,
|
||||
162
|
||||
162,
|
||||
165
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2697,7 +2762,11 @@
|
||||
127,
|
||||
133,
|
||||
141,
|
||||
142
|
||||
142,
|
||||
150,
|
||||
166,
|
||||
167,
|
||||
168
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2711,8 +2780,7 @@
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33
|
||||
27
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2932,7 +3000,8 @@
|
||||
161,
|
||||
162,
|
||||
163,
|
||||
164
|
||||
164,
|
||||
165
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2944,7 +3013,6 @@
|
||||
"name": "GC Kfet",
|
||||
"permissions": [
|
||||
32,
|
||||
33,
|
||||
56,
|
||||
58,
|
||||
55,
|
||||
@ -2959,7 +3027,10 @@
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
143
|
||||
143,
|
||||
166,
|
||||
167,
|
||||
168
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
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()
|
||||
query = instanced.query
|
||||
model = perm.model.model_class()
|
||||
model.objects.filter(query).all()
|
||||
# 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:", perm.query)
|
||||
if instanced.query:
|
||||
|
0
apps/registration/tests/__init__.py
Normal file
386
apps/registration/tests/test_registration.py
Normal file
@ -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 member.forms import ProfileForm
|
||||
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 permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
@ -101,7 +101,7 @@ class UserValidateView(TemplateView):
|
||||
user.profile.email_confirmed = True
|
||||
user.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):
|
||||
"""
|
||||
@ -169,12 +169,9 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||
:return:
|
||||
"""
|
||||
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"]
|
||||
|
||||
if not pattern:
|
||||
return qs.none()
|
||||
|
||||
qs = qs.filter(
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
@ -205,10 +202,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
self.object = self.get_object()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
@ -239,6 +233,10 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
def form_valid(self, form):
|
||||
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
|
||||
soge = form.cleaned_data["soge"]
|
||||
credit_type = form.cleaned_data["credit_type"]
|
||||
@ -276,9 +274,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
if credit_type is None:
|
||||
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:
|
||||
# Check if the user credits enough money
|
||||
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', )
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not obj:
|
||||
return True
|
||||
return not obj.closed and super().has_change_permission(request, obj)
|
||||
return not obj or (not obj.closed and super().has_change_permission(request, obj))
|
||||
|
||||
|
||||
@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,
|
||||
then render it on /api/treasury/invoice/
|
||||
"""
|
||||
queryset = Invoice.objects.all()
|
||||
queryset = Invoice.objects.order_by("id").all()
|
||||
serializer_class = InvoiceSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
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,
|
||||
then render it on /api/treasury/product/
|
||||
"""
|
||||
queryset = Product.objects.all()
|
||||
queryset = Product.objects.order_by("invoice_id", "id").all()
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
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
|
||||
then render it on /api/treasury/remittance_type/
|
||||
"""
|
||||
queryset = RemittanceType.objects
|
||||
queryset = RemittanceType.objects.order_by("id")
|
||||
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,
|
||||
then render it on /api/treasury/remittance/
|
||||
"""
|
||||
queryset = Remittance.objects
|
||||
queryset = Remittance.objects.order_by("id")
|
||||
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,
|
||||
then render it on /api/treasury/soge_credit/
|
||||
"""
|
||||
queryset = SogeCredit.objects
|
||||
queryset = SogeCredit.objects.order_by("id")
|
||||
serializer_class = SogeCreditSerializer
|
||||
|
@ -16,21 +16,15 @@ class InvoiceForm(forms.ModelForm):
|
||||
"""
|
||||
|
||||
def clean(self):
|
||||
# If the invoice is locked, it can't be updated.
|
||||
if self.instance and self.instance.locked:
|
||||
for field_name in self.fields:
|
||||
self.cleaned_data[field_name] = getattr(self.instance, field_name)
|
||||
self.errors.clear()
|
||||
self.add_error(None, _('This invoice is locked and can no longer be edited.'))
|
||||
return self.cleaned_data
|
||||
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:
|
||||
model = Invoice
|
||||
exclude = ('bde', 'date', 'tex', )
|
||||
|
@ -85,7 +85,7 @@ class Invoice(models.Model):
|
||||
|
||||
old_invoice = Invoice.objects.filter(id=self.id)
|
||||
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."))
|
||||
|
||||
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):
|
||||
# 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")
|
||||
|
||||
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(
|
||||
'treasury:invoice_delete',
|
||||
args=[A('pk')],
|
||||
args=[A('id')],
|
||||
verbose_name=_("delete"),
|
||||
text=_("Delete"),
|
||||
attrs={
|
||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<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">
|
||||
{% trans "Invoice" %}s
|
||||
</a>
|
||||
|
@ -58,7 +58,7 @@
|
||||
\parbox[b][\paperheight]{\paperwidth}{%
|
||||
\vfill
|
||||
\centering
|
||||
{\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}%
|
||||
{\transparent{0.1}\includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}}}%
|
||||
\vfill
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<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">
|
||||
{% trans "Invoice" %}s
|
||||
</a>
|
||||
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<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">
|
||||
{% trans "Invoice" %}s
|
||||
</a>
|
||||
@ -59,9 +59,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
function reloadTable() {
|
||||
let pattern = searchbar_obj.val();
|
||||
|
||||
if (pattern === old_pattern || pattern === "")
|
||||
return;
|
||||
|
||||
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
|
||||
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
|
||||
|
||||
|
0
apps/treasury/tests/__init__.py
Normal file
403
apps/treasury/tests/test_treasury.py
Normal file
@ -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
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
del form.fields["locked"]
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
ret = super().form_valid(form)
|
||||
|
||||
@ -134,6 +139,11 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
|
||||
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):
|
||||
ret = super().form_valid(form)
|
||||
|
||||
@ -165,6 +175,11 @@ class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||
model = 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):
|
||||
return reverse_lazy('treasury:invoice_list')
|
||||
|
||||
@ -387,7 +402,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||
if not request.user.is_authenticated:
|
||||
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."))
|
||||
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) {
|
||||
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;
|
||||
if (alias !== note.name && note.name)
|
||||
|
@ -27,7 +27,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// Switching in double consumptions mode should update the layout
|
||||
$("#double_conso").click(function() {
|
||||
$("#double_conso").change(function() {
|
||||
$("#consos_list_div").removeClass('d-none');
|
||||
$("#user_select_div").attr('class', 'col-xl-4');
|
||||
$("#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');
|
||||
$("#user_select_div").attr('class', 'col-xl-7');
|
||||
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
|
||||
@ -158,7 +158,7 @@ function reset() {
|
||||
$("#consos_list").html("");
|
||||
$("#note").val("");
|
||||
$("#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", "#");
|
||||
refreshHistory();
|
||||
refreshBalance();
|
||||
|
45
note_kfet/static/js/konami.js
Normal file
@ -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("");
|
||||
$("#bank").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", "#");
|
||||
if (refresh) {
|
||||
refreshBalance();
|
||||
@ -96,7 +96,7 @@ $(document).ready(function() {
|
||||
let source = $("#source_note");
|
||||
let dest = $("#dest_note");
|
||||
|
||||
$("#type_transfer").click(function() {
|
||||
$("#type_transfer").change(function() {
|
||||
if (LOCK)
|
||||
return;
|
||||
|
||||
@ -117,7 +117,7 @@ $(document).ready(function() {
|
||||
location.hash = "transfer";
|
||||
});
|
||||
|
||||
$("#type_credit").click(function() {
|
||||
$("#type_credit").change(function() {
|
||||
if (LOCK)
|
||||
return;
|
||||
|
||||
@ -146,7 +146,7 @@ $(document).ready(function() {
|
||||
location.hash = "credit";
|
||||
});
|
||||
|
||||
$("#type_debit").click(function() {
|
||||
$("#type_debit").change(function() {
|
||||
if (LOCK)
|
||||
return;
|
||||
|
||||
|
BIN
note_kfet/static/song/konami.ogg
Normal file
@ -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"
|
||||
crossorigin="anonymous"></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 #}
|
||||
{% if form.media %}
|
||||
|
@ -1,7 +1,17 @@
|
||||
django-htcpcp-tea==0.3.1
|
||||
django-mailer==2.0.1
|
||||
django-phonenumber-field==5.0.0
|
||||
django-tables2==2.3.1
|
||||
django-rest-polymorphic==0.1.9
|
||||
django-bootstrap-datepicker-plus==3.0.5
|
||||
django-colorfield==0.3.2
|
||||
beautifulsoup4~=4.7.1
|
||||
Django~=2.2.15
|
||||
django-bootstrap-datepicker-plus~=3.0.5
|
||||
django-cas-server>=0.9.0
|
||||
django-colorfield~=0.3.2
|
||||
django-crispy-forms~=1.7.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]
|
||||
sitepackages = True
|
||||
setenv =
|
||||
PYTHONWARNINGS = all
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
coverage
|
||||
@ -23,7 +21,6 @@ commands =
|
||||
|
||||
[testenv:linters]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
flake8
|
||||
flake8-colors
|
||||
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]
|
||||
# Ignore too many errors, should be reduced in the future
|
||||
ignore = D203, W503, E203, I100, I101, C901
|
||||
ignore = W503, I100, I101
|
||||
exclude =
|
||||
.tox,
|
||||
.git,
|
||||
|