Merge branch 'beta' into 'master'

v1.0.0

See merge request bde/nk20!114
This commit is contained in:
ynerant 2020-09-06 16:06:26 +02:00
commit 1ed74021a2
22 changed files with 196 additions and 125 deletions

View File

@ -18,8 +18,7 @@ py37-django22:
python3-django-extensions python3-django-filters python3-django-polymorphic python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-bs4 python3-setuptools tox python3-bs4 python3-setuptools tox texlive-xetex
texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended
script: tox -e py37-django22 script: tox -e py37-django22
# Ubuntu 20.04 # Ubuntu 20.04
@ -36,8 +35,7 @@ py38-django22:
python3-django-extensions python3-django-filters python3-django-polymorphic python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-bs4 python3-setuptools tox python3-bs4 python3-setuptools tox texlive-xetex
texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended
script: tox -e py38-django22 script: tox -e py38-django22
linters: linters:

View File

@ -12,8 +12,7 @@ RUN apt-get update && \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \ uwsgi uwsgi-plugin-python3 \
texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
gettext libjs-bootstrap4 fonts-font-awesome && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Instal PyPI requirements # Instal PyPI requirements

View File

@ -23,14 +23,13 @@ Bien que cela permette de créer une instance sur toutes les distributions,
$ sudo apt update $ sudo apt update
$ sudo apt install --no-install-recommends -y \ $ sudo apt install --no-install-recommends -y \
ipython3 python3-setuptools python3-venv python3-dev \ ipython3 python3-setuptools python3-venv python3-dev \
texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
gettext libjs-bootstrap4 fonts-font-awesome git
``` ```
2. **Clonage du dépot** là où vous voulez : 2. **Clonage du dépot** là où vous voulez :
```bash ```bash
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20 $ git clone git@gitlab.crans.org:bde/nk20.git --recursive && cd nk20
``` ```
3. **Création d'un environment de travail Python décorrélé du système.** 3. **Création d'un environment de travail Python décorrélé du système.**
@ -98,8 +97,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \ uwsgi uwsgi-plugin-python3 \
texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
gettext libjs-bootstrap4 fonts-font-awesome \
nginx python3-venv git acl nginx python3-venv git acl
``` ```
@ -109,7 +107,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ sudo mkdir -p /var/www/note_kfet && cd /var/www/note_kfet $ sudo mkdir -p /var/www/note_kfet && cd /var/www/note_kfet
$ sudo chown www-data:www-data . $ sudo chown www-data:www-data .
$ sudo chmod g+rwx . $ sudo chmod g+rwx .
$ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git $ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git --recursive
``` ```
3. **Création d'un environment de travail Python décorrélé du système.** 3. **Création d'un environment de travail Python décorrélé du système.**
@ -221,7 +219,7 @@ Il est possible de travailler sur une instance Docker.
Pour construire l'image Docker `nk20`, Pour construire l'image Docker `nk20`,
``` ```
git clone https://gitlab.crans.org/bde/nk20/ && cd nk20 git clone https://gitlab.crans.org/bde/nk20/ --recursive && cd nk20
docker build . -t nk20 docker build . -t nk20
``` ```

View File

@ -38,10 +38,7 @@
- python3-venv - python3-venv
# LaTeX (PDF generation) # LaTeX (PDF generation)
- texlive-fonts-recommended - texlive-xetex
- texlive-lang-french
- texlive-latex-base
- texlive-latex-recommended
# WSGI server # WSGI server
- uwsgi - uwsgi

View File

@ -341,6 +341,9 @@ class Membership(models.Model):
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
def renew(self): def renew(self):
"""
If the current membership comes to expiration, create a new membership that starts immediately after this one.
"""
if not Membership.objects.filter( if not Membership.objects.filter(
user=self.user, user=self.user,
club=self.club, club=self.club,
@ -362,6 +365,48 @@ class Membership(models.Model):
new_membership.roles.set(self.roles.all()) new_membership.roles.set(self.roles.all())
new_membership.save() new_membership.save()
def renew_parent(self):
"""
Ensure that the parent membership is renewed, and renew/create it if needed.
"""
parent_membership = Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
).order_by("-date_start")
if parent_membership.exists():
# Renew the previous membership of the parent club
parent_membership = parent_membership.first()
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.renew()
else:
# Create a new membership in the parent club
parent_membership = Membership(
user=self.user,
club=self.club.parent_club,
date_start=self.date_start,
)
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.save()
parent_membership.refresh_from_db()
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
Calculate fee and end date before saving the membership and creating the transaction if needed. Calculate fee and end date before saving the membership and creating the transaction if needed.
@ -391,43 +436,7 @@ class Membership(models.Model):
date_start__gte=self.club.parent_club.membership_start, date_start__gte=self.club.parent_club.membership_start,
).exists(): ).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent: if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
parent_membership = Membership.objects.filter( self.renew_parent()
user=self.user,
club=self.club.parent_club,
).order_by("-date_start")
if parent_membership.exists():
# Renew the previous membership of the parent club
parent_membership = parent_membership.first()
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.renew()
else:
# Create a new membership in the parent club
parent_membership = Membership(
user=self.user,
club=self.club.parent_club,
date_start=self.date_start,
)
parent_membership._force_renew_parent = True
if hasattr(self, '_soge'):
parent_membership._soge = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.save()
parent_membership.refresh_from_db()
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
else: else:
raise ValidationError(_('User is not a member of the parent club') raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name) + ' ' + self.club.parent_club.name)

View File

@ -584,6 +584,64 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return context return context
def perform_verifications(self, form, user, club, fee):
"""
Make some additional verifications to check that the membership can be created.
:return: True if the form is clean, False if there is an error.
"""
error = False
# Retrieve form data
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
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(),
date_end__gte=date.today(),
).exists():
# Users without a valid Kfet membership can't have a negative balance.
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
form.add_error('user',
_("This user don't have enough money to join this club, and can't have a negative balance."))
error = True
if Membership.objects.filter(
user=form.instance.user,
club=club,
date_start__lte=form.instance.date_start,
date_end__gte=form.instance.date_start,
).exists():
form.add_error('user', _('User is already a member of the club'))
error = True
if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
error = True
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_end))
error = True
if credit_amount:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
if not first_name:
form.add_error('first_name', _("This field is required."))
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
return not error
def form_valid(self, form): def form_valid(self, form):
""" """
Create membership, check that all is good, make transactions Create membership, check that all is good, make transactions
@ -630,36 +688,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
c = c.parent_club c = c.parent_club
if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter( # Make some verifications about the form, and if there is an error, then assume that the form is invalid
club__name="Kfet", if not self.perform_verifications(form, user, club, fee):
user=user, return self.form_invalid(form)
date_start__lte=date.today(),
date_end__gte=date.today(),
).exists():
# Users without a valid Kfet membership can't have a negative balance.
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
form.add_error('user',
_("This user don't have enough money to join this club, and can't have a negative balance."))
return super().form_invalid(form)
if Membership.objects.filter(
user=form.instance.user,
club=club,
date_start__lte=form.instance.date_start,
date_end__gte=form.instance.date_start,
).exists():
form.add_error('user', _('User is already a member of the club'))
return super().form_invalid(form)
if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
return super().form_invalid(form)
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_end))
return super().form_invalid(form)
# Now, all is fine, the membership can be created. # Now, all is fine, the membership can be created.
@ -671,15 +702,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# Credit note before the membership is created. # Credit note before the membership is created.
if credit_amount > 0: if credit_amount > 0:
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
if not first_name:
form.add_error('first_name', _("This field is required."))
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
transaction = SpecialTransaction( transaction = SpecialTransaction(
source=credit_type, source=credit_type,
destination=user.note, destination=user.note,

View File

@ -0,0 +1,44 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from member.models import Membership, Club
from permission.models import Role
class TestRightsPage(TestCase):
"""
Display the rights page.
"""
fixtures = ("initial",)
def test_anonymous_rights_page(self):
"""
Check that we can properly see the rights page even if we are not connected.
We can't nethertheless see the club managers.
"""
response = self.client.get(reverse("permission:rights"))
self.assertEqual(response.status_code, 200)
self.assertFalse("special_memberships_table" in response.context)
self.assertFalse("superusers" in response.context)
def test_authenticated_rights_page(self):
"""
Connect to the note and check that the club mangers are also displayed.
"""
user = User.objects.create_superuser(
username="ploptoto",
password="totototo",
email="toto@example.com",
)
self.client.force_login(user)
membership = Membership.objects.create(user=user, club=Club.objects.get(name="BDE"))
membership.roles.add(Role.objects.get(name="Respo info"))
membership.save()
response = self.client.get(reverse("permission:rights"))
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.context["special_memberships_table"])
self.assertIsNotNone(response.context["superusers"])

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.16 on 2020-09-06 13:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='bde',
field=models.CharField(choices=[('Saperlistpopette', 'Saper[list]popette'), ('Finalist', 'Fina[list]'), ('Listorique', '[List]orique'), ('Satellist', 'Satel[list]'), ('Monopolist', 'Monopo[list]'), ('Kataclist', 'Katac[list]')], default='Saperlistpopette', max_length=32, verbose_name='BDE'),
),
]

View File

@ -25,14 +25,14 @@ class Invoice(models.Model):
bde = models.CharField( bde = models.CharField(
max_length=32, max_length=32,
default='Saperlistpopette.png', default='Saperlistpopette',
choices=( choices=(
('Saperlistpopette.png', 'Saper[list]popette'), ('Saperlistpopette', 'Saper[list]popette'),
('Finalist.png', 'Fina[list]'), ('Finalist', 'Fina[list]'),
('Listorique.png', '[List]orique'), ('Listorique', '[List]orique'),
('Satellist.png', 'Satel[list]'), ('Satellist', 'Satel[list]'),
('Monopolist.png', 'Monopo[list]'), ('Monopolist', 'Monopo[list]'),
('Kataclist.png', 'Katac[list]'), ('Kataclist', 'Katac[list]'),
), ),
verbose_name=_("BDE"), verbose_name=_("BDE"),
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,22 +1,14 @@
{% load escape_tex %} {% load escape_tex %}
\documentclass[a4paper,11pt]{article}
\nonstopmode \usepackage{fontspec}
\documentclass[11pt]{article} \usepackage{geometry}
\usepackage[french]{babel}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[a4paper]{geometry}
%\usepackage{bera}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{fancyhdr} \usepackage{fancyhdr}
\usepackage{fp} \usepackage{fp}
\usepackage{transparent}
\usepackage{eso-pic} \usepackage{eso-pic}
\usepackage{ifthen} \usepackage{ifthen}
\DeclareUnicodeCharacter{00B0}{$^\circ$}
\def\TVA{0} % Taux de la TVA \def\TVA{0} % Taux de la TVA
\def\TotalHT{0} \def\TotalHT{0}
@ -60,7 +52,7 @@
\parbox[b][\paperheight]{\paperwidth}{% \parbox[b][\paperheight]{\paperwidth}{%
\vfill \vfill
\centering \centering
{\transparent{0.1}\includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}}}% \includegraphics[width=\textwidth]{../../apps/treasury/static/img/{{ obj.bde }}_bg.jpg};
\vfill \vfill
} }
} }

View File

@ -1,6 +1,5 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from unittest import skip
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -143,7 +142,6 @@ class TestInvoices(TestCase):
self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200)
self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists()) self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists())
@skip("LaTeX is buggy in the CI")
def test_invoice_render_pdf(self): def test_invoice_render_pdf(self):
""" """
Generate the PDF file of an invoice. Generate the PDF file of an invoice.

View File

@ -209,7 +209,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
# The file has to be rendered twice # The file has to be rendered twice
for ignored in range(2): for ignored in range(2):
error = subprocess.Popen( error = subprocess.Popen(
["pdflatex", "invoice-{}.tex".format(pk)], ["xelatex", "-interaction=nonstopmode", "invoice-{}.tex".format(pk)],
cwd=tmp_dir, cwd=tmp_dir,
stdin=open(os.devnull, "r"), stdin=open(os.devnull, "r"),
stderr=open(os.devnull, "wb"), stderr=open(os.devnull, "wb"),

View File

@ -1,15 +1,11 @@
\documentclass[landscape,10pt]{article} \documentclass[a4paper,landscape,10pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[french]{babel}
\usepackage{fontspec}
\usepackage[margin=1.5cm]{geometry} \usepackage[margin=1.5cm]{geometry}
\usepackage{lmodern}
\begin{document} \begin{document}
\begin{center} \begin{center}
\huge{Liste des inscrits \og {{ wei.name }} \fg{}} \huge{Liste des inscrits « {{ wei.name }} »}
{% if bus %} {% if bus %}
\LARGE{Bus {{ bus.name|safe }}} \LARGE{Bus {{ bus.name|safe }}}

View File

@ -690,7 +690,7 @@ class TestWEIRegistration(TestCase):
""" """
with open("/dev/null", "wb") as devnull: with open("/dev/null", "wb") as devnull:
return subprocess.call( return subprocess.call(
["which", "pdflatex"], ["which", "xelatex"],
stdout=devnull, stdout=devnull,
stderr=devnull, stderr=devnull,
) == 0 ) == 0

View File

@ -1103,7 +1103,7 @@ class MemberListRenderView(LoginRequiredMixin, View):
with open(os.devnull, "wb") as devnull: with open(os.devnull, "wb") as devnull:
error = subprocess.Popen( error = subprocess.Popen(
["pdflatex", "{}/wei-list.tex".format(tmp_dir)], ["xelatex", "-interaction=nonstopmode", "{}/wei-list.tex".format(tmp_dir)],
cwd=tmp_dir, cwd=tmp_dir,
stderr=devnull, stderr=devnull,
stdout=devnull, stdout=devnull,

View File

@ -42,7 +42,7 @@ exclude =
.cache, .cache,
.eggs, .eggs,
*migrations* *migrations*
max-complexity = 10 max-complexity = 15
max-line-length = 160 max-line-length = 160
import-order-style = google import-order-style = google
application-import-names = flake8 application-import-names = flake8