mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-30 04:13:01 +00:00
Merge branch 'beta' into 'master'
v1.0.0 See merge request bde/nk20!114
This commit is contained in:
commit
1ed74021a2
@ -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:
|
||||||
|
@ -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
|
||||||
|
12
README.md
12
README.md
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,35 +365,10 @@ 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 save(self, *args, **kwargs):
|
def renew_parent(self):
|
||||||
"""
|
"""
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
Ensure that the parent membership is renewed, and renew/create it if needed.
|
||||||
"""
|
"""
|
||||||
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))
|
|
||||||
else:
|
|
||||||
if Membership.objects.filter(
|
|
||||||
user=self.user,
|
|
||||||
club=self.club,
|
|
||||||
date_start__lte=self.date_start,
|
|
||||||
date_end__gte=self.date_start,
|
|
||||||
).exists():
|
|
||||||
raise ValidationError(_('User is already a member of the club'))
|
|
||||||
|
|
||||||
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,
|
|
||||||
club=self.club.parent_club,
|
|
||||||
date_start__gte=self.club.parent_club.membership_start,
|
|
||||||
).exists():
|
|
||||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
|
||||||
parent_membership = Membership.objects.filter(
|
parent_membership = Membership.objects.filter(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
club=self.club.parent_club,
|
club=self.club.parent_club,
|
||||||
@ -428,6 +406,37 @@ class Membership(models.Model):
|
|||||||
else:
|
else:
|
||||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||||
parent_membership.save()
|
parent_membership.save()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
if Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club,
|
||||||
|
date_start__lte=self.date_start,
|
||||||
|
date_end__gte=self.date_start,
|
||||||
|
).exists():
|
||||||
|
raise ValidationError(_('User is already a member of the club'))
|
||||||
|
|
||||||
|
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,
|
||||||
|
club=self.club.parent_club,
|
||||||
|
date_start__gte=self.club.parent_club.membership_start,
|
||||||
|
).exists():
|
||||||
|
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||||
|
self.renew_parent()
|
||||||
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)
|
||||||
|
@ -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,
|
||||||
|
44
apps/permission/tests/test_rights_page.py
Normal file
44
apps/permission/tests/test_rights_page.py
Normal 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"])
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
BIN
apps/treasury/static/img/Finalist_bg.jpg
Normal file
BIN
apps/treasury/static/img/Finalist_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
BIN
apps/treasury/static/img/Kataclist_bg.jpg
Normal file
BIN
apps/treasury/static/img/Kataclist_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
apps/treasury/static/img/Listorique_bg.jpg
Normal file
BIN
apps/treasury/static/img/Listorique_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
apps/treasury/static/img/Monopolist_bg.jpg
Normal file
BIN
apps/treasury/static/img/Monopolist_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
apps/treasury/static/img/Saperlistpopette_bg.jpg
Normal file
BIN
apps/treasury/static/img/Saperlistpopette_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
apps/treasury/static/img/Satellist_bg.jpg
Normal file
BIN
apps/treasury/static/img/Satellist_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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"),
|
||||||
|
@ -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 }}}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
2
tox.ini
2
tox.ini
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user