Compare commits

..

1 commit

312 changed files with 1825 additions and 3658 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

4
.gitignore vendored
View file

@ -5,7 +5,6 @@ cof/settings.py
settings.py settings.py
*~ *~
venv/ venv/
.venv/
.vagrant .vagrant
/src /src
media/ media/
@ -19,5 +18,4 @@ media/
.cache .cache
# VSCode # VSCode
.vscode/ .vscode/
.direnv

View file

@ -17,15 +17,12 @@ variables:
# psql password authentication # psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD PGPASSWORD: $POSTGRES_PASSWORD
# apps to check migrations for
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
.test_template: .test_template:
before_script: before_script:
- mkdir -p vendor/{pip,apt} - mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet # Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements-prod.txt coverage tblib - pip install --upgrade -r requirements-prod.txt coverage tblib
@ -43,27 +40,19 @@ variables:
# Keep this disabled for now, as it may kill GitLab... # Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
kfettest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test kfet
coftest: coftest:
stage: test stage: test
extends: .test_template extends: .test_template
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod"
script: script:
- coverage run manage.py test gestioncof bda petitscours shared --parallel - coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
bdstest: bdstest:
stage: test stage: test
extends: .test_template extends: .test_template
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod" DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod"
script: script:
- coverage run manage.py test bds clubs events --parallel - coverage run manage.py test bds clubs events --parallel
@ -76,7 +65,7 @@ linters:
- black --check . - black --check .
- isort --check --diff . - isort --check --diff .
# Print errors only # Print errors only
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared
cache: cache:
key: linters key: linters
paths: paths:
@ -86,14 +75,14 @@ linters:
migration_checks: migration_checks:
stage: test stage: test
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local" DJANGO_SETTINGS_MODULE: "cof.settings.local"
before_script: before_script:
- mkdir -p vendor/{pip,apt} - mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py - cp cof/settings/secret_example.py cof/settings/secret.py
- pip install --upgrade -r requirements-devel.txt - pip install --upgrade -r requirements-devel.txt
- python --version - python --version
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS script: python manage.py makemigrations --dry-run --check
services: services:
# this should not be necessary… # this should not be necessary…
- postgres:11.7 - postgres:11.7

View file

@ -21,122 +21,13 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
adhérents ni des cotisations. adhérents ni des cotisations.
## TODO Prod ## Version ??? - dans un futur proche
- Lancer `python manage.py update_translation_fields` après la migration
- Mettre à jour les units systemd `daphne.service` et `worker.service`
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
## Version ??? - ??/??/????
## Version 0.15.1 - 15/06/2023
### K-Fêt
- Rattrape les erreurs d'envoi de mail de négatif
- Utilise l'adresse chefs pour les envois de négatifs
## Version 0.15 - 22/05/2023
### K-Fêt
- Rajoute un formulaire de contact
- Rajoute un formulaire de demande de soirée
- Désactive les mails d'envoi de négatifs sur les comptes gelés
## Version 0.14 - 19/05/2023
- Répare les dépendances en spécifiant toutes les versions
### K-Fêt
- Répare la gestion des changement d'heure via moment.js
## Version 0.13 - 19/02/2023
### K-Fêt
- Rajoute la valeur des inventaires
- Résout les problèmes de négatif ne disparaissant pas
- Affiche son surnom s'il y en a un
- Bugfixes
## Version 0.12.1 - 03/10/2022
### K-Fêt
- Fixe un problème de rendu causé par l'agrandissement du menu
- Mise à jour vers Channels 3.x et Django 3.2
## Version 0.12 - 17/06/2022
### K-Fêt
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
## Version 0.11 - 26/10/2021
### COF
- Répare un problème de rendu sur le wagtail du COF
### K-Fêt
- Ajoute de mails de rappels pour les comptes en négatif
- La recherche de comptes sur K-Psul remarche normalement
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
- Modification du gel de compte:
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
- Modification du fonctionnement des négatifs
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
- il n'y a plus de limite de temps sur les négatifs
- supression des autorisations de négatif
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
- La supression d'opérations anciennes est réparée
## Version 0.10 - 18/04/2021
### K-Fêt
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
la fois.
- L'interface indique plus clairement quand on fait une erreur en modifiant un
compte.
- On supprime la fonction "décalage de balance".
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
confidentialité. Les chefs/trez peuvent disposer d'une permission
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
L'accès à son historique personnel n'est pas limité. Les durées sont
configurables dans `settings/cof_prod.py`.
### COF
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
## Version 0.9 - 06/02/2020
### COF / BdA ### COF / BdA
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
KDE).
- La page d'accueil affiche la date de fermeture des tirages BdA.
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes - On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
ses places pour pouvoir revendre. ses places pour pouvoir revendre.
- On s'assure que l'email fourni lors d'une demande de petit cours est valide. - On s'assure que les emails dans les demandes de petits cours sont valides.
### BDS
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
en modifiant le profil d'un membre du BDS.
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
la page d'accueil.
### K-Fêt ### K-Fêt

View file

@ -1,4 +1,4 @@
# GestioCOF / GestioBDS # GestioCOF
[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) [![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) [![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
@ -38,11 +38,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier
pip install -U pip # parfois nécessaire la première fois pip install -U pip # parfois nécessaire la première fois
pip install -r requirements-devel.txt pip install -r requirements-devel.txt
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique `cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier: pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py gestioasso/settings/secret.py ln -s secret_example.py cof/settings/secret.py
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez

View file

@ -33,6 +33,20 @@ class ReadOnlyMixin(object):
return readonly_fields + self.readonly_fields_update return readonly_fields + self.readonly_fields_update
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority"
class AttributionTabularAdminForm(forms.ModelForm): class AttributionTabularAdminForm(forms.ModelForm):
listing = None listing = None
@ -211,6 +225,7 @@ class AttributionAdminForm(forms.ModelForm):
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
list_display = ("id", "spectacle", "participant", "given", "paid") list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = ( search_fields = (
"spectacle__title", "spectacle__title",
@ -223,7 +238,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
class ChoixSpectacleAdmin(admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin):
autocomplete_fields = ["participant", "spectacle"] form = ChoixSpectacleAdminForm
def tirage(self, obj): def tirage(self, obj):
return obj.participant.tirage return obj.participant.tirage

View file

@ -2,6 +2,7 @@ import random
class Algorithm(object): class Algorithm(object):
shows = None shows = None
ranks = None ranks = None
origranks = None origranks = None

View file

@ -81,7 +81,7 @@ class Command(MyBaseCommand):
shows = random.sample( shows = random.sample(
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2 list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
) )
for rank, show in enumerate(shows): for (rank, show) in enumerate(shows):
choices.append( choices.append(
ChoixSpectacle( ChoixSpectacle(
participant=part, participant=part,

View file

@ -22,7 +22,7 @@ class Command(BaseCommand):
delay = timedelta(days=4) delay = timedelta(days=4)
shows = ( shows = (
Spectacle.objects.filter(date__range=(now, now + delay)) Spectacle.objects.filter(date__range=(now, now + delay))
.filter(tirage__active=True) .filter(tirage__active=True, tirage__send_rappels_auto=True)
.filter(rappel_sent__isnull=True) .filter(rappel_sent__isnull=True)
.all() .all()
) )

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [ operations = [

View file

@ -35,6 +35,7 @@ def fill_tirage_fields(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0001_initial")] dependencies = [("bda", "0001_initial")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0002_add_tirage")] dependencies = [("bda", "0002_add_tirage")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0003_update_tirage_and_spectacle")] dependencies = [("bda", "0003_update_tirage_and_spectacle")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0004_mails-rappel")] dependencies = [("bda", "0004_mails-rappel")]
operations = [ operations = [

View file

@ -18,6 +18,7 @@ def forwards_func(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0005_encoding")] dependencies = [("bda", "0005_encoding")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0006_add_tirage_switch")] dependencies = [("bda", "0006_add_tirage_switch")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0007_extends_spectacle")] dependencies = [("bda", "0007_extends_spectacle")]
operations = [ operations = [

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0008_py3")] dependencies = [("bda", "0008_py3")]
operations = [ operations = [

View file

@ -21,6 +21,7 @@ def forwards_func(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0009_revente")] dependencies = [("bda", "0009_revente")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0010_spectaclerevente_shotgun")] dependencies = [("bda", "0010_spectaclerevente_shotgun")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")] dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [ operations = [

View file

@ -13,6 +13,7 @@ def swap_double_choice(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")] dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [ operations = [

View file

@ -6,6 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")] dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
operations = [] operations = []

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0013_merge_20180524_2123")] dependencies = [("bda", "0013_merge_20180524_2123")]
operations = [ operations = [

View file

@ -29,6 +29,7 @@ def set_participant_payment(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0014_attribution_paid_field")] dependencies = [("bda", "0014_attribution_paid_field")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0015_move_bda_payment")] dependencies = [("bda", "0015_move_bda_payment")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0016_delete_participant_paid")] dependencies = [("bda", "0016_delete_participant_paid")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bda", "0017_participant_accepte_charte"), ("bda", "0017_participant_accepte_charte"),
] ]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.13 on 2022-06-30 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0018_auto_20201021_1818"),
]
operations = [
migrations.AlterUniqueTogether(
name="choixspectacle",
unique_together=set(),
),
migrations.AddConstraint(
model_name="choixspectacle",
constraint=models.UniqueConstraint(
fields=("participant", "spectacle"), name="unique_participation"
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 2.2.17 on 2021-01-08 15:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0018_auto_20201021_1818"),
]
operations = [
migrations.AddField(
model_name="tirage",
name="send_rappels_auto",
field=models.BooleanField(
default=True, verbose_name="Envoi automatique des mails de rappel"
),
),
]

View file

@ -32,6 +32,9 @@ class Tirage(models.Model):
) )
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False) enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
archived = models.BooleanField("Archivé", default=False) archived = models.BooleanField("Archivé", default=False)
send_rappels_auto = models.BooleanField(
"Envoi automatique des mails de rappel", default=True
)
def __str__(self): def __str__(self):
return "%s - %s" % ( return "%s - %s" % (
@ -253,11 +256,7 @@ class ChoixSpectacle(models.Model):
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)
constraints = [ unique_together = (("participant", "spectacle"),)
models.UniqueConstraint(
fields=["participant", "spectacle"], name="unique_participation"
)
]
verbose_name = "voeu" verbose_name = "voeu"
verbose_name_plural = "voeux" verbose_name_plural = "voeux"

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>État des inscriptions BdA</h2> <h2>État des inscriptions BdA</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script> <script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>{{ spectacle }}</h2> <h2>{{ spectacle }}</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{%block realcontent %} {%block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>Inscription à une revente</h2> <h2>Inscription à une revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles%}
{% block realcontent %} {% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2> <h2>Inscriptions pour BdA-Revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -356,9 +356,7 @@ class TestReventeManageTest(TestCase):
def test_can_get(self): def test_can_get(self):
client = Client() client = Client()
client.force_login( client.force_login(self.user)
self.user, backend="django.contrib.auth.backends.ModelBackend"
)
r = client.get(self.url) r = client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)

View file

@ -1,80 +1,80 @@
from django.urls import re_path from django.conf.urls import url
from django.urls import path
from bda import views from bda import views
from bda.views import SpectacleListView from bda.views import SpectacleListView
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
urlpatterns = [ urlpatterns = [
re_path( url(
r"^inscription/(?P<tirage_id>\d+)$", r"^inscription/(?P<tirage_id>\d+)$",
views.inscription, views.inscription,
name="bda-tirage-inscription", name="bda-tirage-inscription",
), ),
re_path(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"), url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
re_path( url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places" url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
), url(
re_path(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
re_path(
r"^spectacles/(?P<tirage_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)$",
buro_required(SpectacleListView.as_view()), buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles", name="bda-liste-spectacles",
), ),
re_path( url(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
views.spectacle, views.spectacle,
name="bda-spectacle", name="bda-spectacle",
), ),
re_path( url(
r"^spectacles/unpaid/(?P<tirage_id>\d+)$", r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
views.UnpaidParticipants.as_view(), views.UnpaidParticipants.as_view(),
name="bda-unpaid", name="bda-unpaid",
), ),
re_path( url(
r"^spectacles/autocomplete$", r"^spectacles/autocomplete$",
views.spectacle_autocomplete, views.spectacle_autocomplete,
name="bda-spectacle-autocomplete", name="bda-spectacle-autocomplete",
), ),
re_path( url(
r"^participants/autocomplete$", r"^participants/autocomplete$",
views.participant_autocomplete, views.participant_autocomplete,
name="bda-participant-autocomplete", name="bda-participant-autocomplete",
), ),
# Urls BdA-Revente # Urls BdA-Revente
re_path( url(
r"^revente/(?P<tirage_id>\d+)/manage$", r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage, views.revente_manage,
name="bda-revente-manage", name="bda-revente-manage",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/subscribe$", r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe, views.revente_subscribe,
name="bda-revente-subscribe", name="bda-revente-subscribe",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/tirages$", r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages, views.revente_tirages,
name="bda-revente-tirages", name="bda-revente-tirages",
), ),
re_path( url(
r"^revente/(?P<spectacle_id>\d+)/buy$", r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy, views.revente_buy,
name="bda-revente-buy", name="bda-revente-buy",
), ),
re_path( url(
r"^revente/(?P<revente_id>\d+)/confirm$", r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm, views.revente_confirm,
name="bda-revente-confirm", name="bda-revente-confirm",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/shotgun$", r"^revente/(?P<tirage_id>\d+)/shotgun$",
views.revente_shotgun, views.revente_shotgun,
name="bda-revente-shotgun", name="bda-revente-shotgun",
), ),
re_path( url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels" url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
), path(
re_path( "toggle-rappels/<int:tirage_id>",
r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue" views.toggle_rappels,
name="bda-toggle-rappels",
), ),
] ]

View file

@ -274,13 +274,13 @@ def do_tirage(tirage_elt, token):
results = Algorithm(data["shows"], data["members"], choices)(token) results = Algorithm(data["shows"], data["members"], choices)(token)
# On compte les places attribuées et les déçus # On compte les places attribuées et les déçus
for _, members, losers in results: for (_, members, losers) in results:
data["total_slots"] += len(members) data["total_slots"] += len(members)
data["total_losers"] += len(losers) data["total_losers"] += len(losers)
# On calcule le déficit et les bénéfices pour le BdA # On calcule le déficit et les bénéfices pour le BdA
# FIXME: le traitement de l'opéra est sale # FIXME: le traitement de l'opéra est sale
for show, members, _ in results: for (show, members, _) in results:
deficit = (show.slots - len(members)) * show.price deficit = (show.slots - len(members)) * show.price
data["total_sold"] += show.slots * show.price data["total_sold"] += show.slots * show.price
if deficit >= 0: if deficit >= 0:
@ -293,8 +293,8 @@ def do_tirage(tirage_elt, token):
# so assign a single object for each Participant id # so assign a single object for each Participant id
members_uniq = {} members_uniq = {}
members2 = {} members2 = {}
for show, members, _ in results: for (show, members, _) in results:
for member, _, _, _ in members: for (member, _, _, _) in members:
if member.id not in members_uniq: if member.id not in members_uniq:
members_uniq[member.id] = member members_uniq[member.id] = member
members2[member] = [] members2[member] = []
@ -734,6 +734,18 @@ class SpectacleListView(ListView):
return context return context
def toggle_rappels(request, tirage_id):
"""
Permet de désactiver ou de réactiver les envois de mails automatiques pour
les spectacles appartenant à ce tirage.
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
tirage.send_rappels_auto = not tirage.send_rappels_auto
tirage.save()
return HttpResponseRedirect(reverse("home"))
class UnpaidParticipants(BuroRequiredMixin, ListView): class UnpaidParticipants(BuroRequiredMixin, ListView):
context_object_name = "unpaid" context_object_name = "unpaid"
template_name = "bda-unpaid.html" template_name = "bda-unpaid.html"

View file

@ -0,0 +1 @@
default_app_config = "bds.apps.BdsConfig"

View file

@ -1,4 +1,5 @@
from django.apps import AppConfig, apps as global_apps from django import apps as global_apps
from django.apps import AppConfig
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate

View file

@ -1,7 +1,6 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from bds.models import BDSProfile from bds.models import BDSProfile
@ -9,8 +8,6 @@ User = get_user_model()
class UserForm(forms.ModelForm): class UserForm(forms.ModelForm):
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
class Meta: class Meta:
model = User model = User
fields = ["email", "first_name", "last_name"] fields = ["email", "first_name", "last_name"]
@ -36,6 +33,4 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = BDSProfile model = BDSProfile
exclude = ["user"] exclude = ["user"]
widgets = { widgets = {"birthdate": forms.DateInput(attrs={"type": "date"})}
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
}

View file

@ -8,6 +8,7 @@ import bds.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]

View file

@ -9,6 +9,7 @@ def create_bds_buro_group(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bds", "0001_initial")] dependencies = [("bds", "0001_initial")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0002_bds_group"), ("bds", "0002_bds_group"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0003_staff_permission"), ("bds", "0003_staff_permission"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0004_is_member_cotiz_type"), ("bds", "0004_is_member_cotiz_type"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0005_remove_bdsprofile_certificate_file"), ("bds", "0005_remove_bdsprofile_certificate_file"),
] ]

View file

@ -3,7 +3,6 @@ from os.path import splitext
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared.utils import choices_length from shared.utils import choices_length
@ -94,16 +93,6 @@ class BDSProfile(models.Model):
), ),
) )
@classmethod
def expired_members(cls):
now = timezone.now()
qs = cls.objects.filter(is_member=True)
if now.month > 1 and now.month < 7:
return qs.filter(cotisation_period="SE1")
elif now.month < 2 or now.month > 8:
return qs.none()
return qs
class Meta: class Meta:
verbose_name = _("Profil BDS") verbose_name = _("Profil BDS")
verbose_name_plural = _("Profils BDS") verbose_name_plural = _("Profils BDS")

View file

@ -1,4 +1,4 @@
{% load static %} {% load staticfiles %}
{% load bulma_utils %} {% load bulma_utils %}
<!DOCTYPE html> <!DOCTYPE html>

View file

@ -1,22 +0,0 @@
{% extends "bds/base.html" %}
{% block content %}
<h1 class="title">Liste des adhésions expirées</h1>
{% if object_list %}
<div class="content">
<ul>
{% for p in object_list %}
<li>{{ p.user.first_name }} {{ p.user.last_name }} ({{ p.user.username }}), {{ p.get_cotisation_period_display }}</li>
{% endfor %}
</ul>
</div>
<div class="buttons is-centered">
<a class="button is-danger" href="{% url 'bds:members.reset' %}">Réinitialiser les adhésions expirées</a>
</div>
{% endif %}
{% endblock %}

View file

@ -7,7 +7,7 @@
<section class="section"> <section class="section">
<div class="box"> <div class="box">
<div class="content has-text-centered"> <div class="content has-text-centered">
<h1>{{ member_count }}</h1> <h1>{{ member_count }}</h1>
adhérent·e·s adhérent·e·s
</div> </div>
</div> </div>
@ -34,13 +34,6 @@
<br> <br>
<br> <br>
<a class=button href="{% url 'bds:export.members' %}">Télécharger la liste des membres (CSV)</a>
<a class=button href="{% url 'bds:members.expired' %}">Liste des adhésions expirées ({{ nb_expired }})</a>
<br>
<br>
Le site est encore en développement. Le site est encore en développement.
<br> <br>
Suivez notre avancement sur Suivez notre avancement sur
@ -59,4 +52,4 @@
{% endblock layout %} {% endblock layout %}

View file

@ -5,100 +5,100 @@
{% block content %} {% block content %}
{% for form in forms.values %} {% for form in forms.values %}
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
<div class="notification is-danger"> <div class="notification is-danger">
{{ error }} {{ error }}
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<h1 class="title">{% trans "Modification du profil " %}{{ view.user.username }}</h1> <h1 class="title">{% trans "Modification du profil " %}{{ view.user.username }}</h1>
<div class="container"> <div class="container">
<form method="post" action="" id="user-update-form"> <form method="post" action="" id="user-update-form">
{% csrf_token %} {% csrf_token %}
{% for form in forms.values %} {% for form in forms.values %}
{% include "bds/forms/form.html" with form=form errors=False %} {% include "bds/forms/form.html" with form=form errors=False %}
{% endfor %} {% endfor %}
</form> </form>
<form method="post" action="{% url 'bds:user.delete' view.user.pk %}" id="user-delete-form"> <form method="post" action="{% url 'bds:user.delete' view.user.pk %}" id="user-delete-form">
{% csrf_token %} {% csrf_token %}
</form> </form>
<div class="columns is-gapless mt-5"> <div class="columns is-gapless mt-5">
<div class="column is-5"> <div class="column is-5">
<button id="user-update-button" class="button is-fullwidth is-primary">Enregistrer</button> <button id="user-update-button" class="button is-fullwidth is-primary">Enregistrer</button>
</div>
<div class="column is-2">
</div>
<div class="column is-5">
<button id="user-delete-button" class="button is-fullwidth is-danger">Supprimer</button>
</div>
</div> </div>
<div class="column is-2">
</div>
<div class="column is-5">
<button id="user-delete-button" class="button is-fullwidth is-danger">Supprimer</button>
</div>
</div>
</div> </div>
<!-- Hidden by default --> <!-- Hidden by default -->
<div id="confirm-delete-modal" class="modal"> <div id="confirm-delete-modal" class="modal">
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">Confirmer la suppression</p> <p class="modal-card-title">Confirmer la suppression</p>
<button id="modal-close" class="delete" aria-label="close"></button> <button id= "modal-close" class="delete" aria-label="close"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
Voulez-vous réellement supprimer ce compte ? Attention, cette opération est irréversible ! Voulez-vous réellement supprimer ce compte ? Attention, cette opération est irréversible !
</section> </section>
<footer class="modal-card-foot"> <footer class="modal-card-foot">
<button id="confirm-delete-button" class="button is-primary">Supprimer</button> <button id="confirm-delete-button" class="button is-primary">Supprimer</button>
<button id="cancel-delete-button" class="button">Annuler</button> <button id="cancel-delete-button" class="button">Annuler</button>
</footer> </footer>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
"use strict"; "use strict";
// Adapted from https://bulma.io/lib/main.js // Adapted from https://bulma.io/lib/main.js
function openModal(target) { function openModal(target) {
$(document).addClass('is-clipped'); $(document).addClass('is-clipped');
$(`#${target}`).addClass('is-active'); $(`#${target}`).addClass('is-active');
} }
function closeModals() { function closeModals() {
$(document).removeClass('is-clipped'); $(document).removeClass('is-clipped');
$(".modal").removeClass('is-active'); $(".modal").removeClass('is-active');
} }
// Si on clique sur enregistrer, ça marche // Si on clique sur enregistrer, ça marche
$("#user-update-button").on("click", function() { $("#user-update-button").on("click", function() {
$("#user-update-form").submit(); $("#user-update-form").submit();
}) })
// Si on clique sur supprimer, confirmation demandée
$("#user-delete-button").on("click", function() {
openModal("confirm-delete-modal");
});
$(".modal-background, #modal-close, #cancel-delete-button").on("click", closeModals);
$("#confirm-delete-button").on("click", function() {
$("#user-delete-form").submit();
});
$(document).on("keydown", function(e) {
if (e.key == "Escape") {
closeModals();
}
if (e.key == "Enter") {
$("#user-update-form").submit();
}
});
// Si on clique sur supprimer, confirmation demandée
$("#user-delete-button").on("click", function () {
openModal("confirm-delete-modal");
}); });
$(".modal-background, #modal-close, #cancel-delete-button").on("click", closeModals);
$("#confirm-delete-button").on("click", function() {
$("#user-delete-form").submit();
});
$(document).on("keydown", function (e) {
if (e.key == "Escape") {
closeModals();
}
if (e.key == "Enter") {
$("#user-update-form").submit();
}
});
});
</script> </script>
{% endblock %} {% endblock %}

View file

@ -22,18 +22,6 @@ def login_url(next=None):
return "{}?next={}".format(login_url, next) return "{}?next={}".format(login_url, next)
class TestHomeView(TestCase):
@mock.patch("gestioncof.signals.messages")
def test_get(self, mock_messages):
user = User.objects.create_user(username="random_user")
give_bds_buro_permissions(user)
self.client.force_login(
user, backend="django.contrib.auth.backends.ModelBackend"
)
resp = self.client.get(reverse("bds:home"))
self.assertEqual(resp.status_code, 200)
class TestRegistrationView(TestCase): class TestRegistrationView(TestCase):
@mock.patch("gestioncof.signals.messages") @mock.patch("gestioncof.signals.messages")
def test_get_autocomplete(self, mock_messages): def test_get_autocomplete(self, mock_messages):
@ -46,14 +34,14 @@ class TestRegistrationView(TestCase):
self.assertRedirects(resp, login_url(next=url)) self.assertRedirects(resp, login_url(next=url))
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 403) self.assertEquals(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEquals(resp.status_code, 200)
@mock.patch("gestioncof.signals.messages") @mock.patch("gestioncof.signals.messages")
def test_get(self, mock_messages): def test_get(self, mock_messages):
@ -66,11 +54,11 @@ class TestRegistrationView(TestCase):
self.assertRedirects(resp, login_url(next=url)) self.assertRedirects(resp, login_url(next=url))
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 403) self.assertEquals(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEquals(resp.status_code, 200)

View file

@ -14,11 +14,4 @@ urlpatterns = [
name="user.create.fromclipper", name="user.create.fromclipper",
), ),
path("user/delete/<int:pk>", views.UserDeleteView.as_view(), name="user.delete"), path("user/delete/<int:pk>", views.UserDeleteView.as_view(), name="user.delete"),
path("members", views.export_members, name="export.members"),
path(
"members/expired",
views.ResetMembershipListView.as_view(),
name="members.expired",
),
path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"),
] ]

View file

@ -1,14 +1,9 @@
import csv
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Permission
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DeleteView, ListView, RedirectView, TemplateView from django.views.generic import DeleteView, TemplateView
from bds.autocomplete import bds_search from bds.autocomplete import bds_search
from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm
@ -30,7 +25,6 @@ class Home(StaffRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["member_count"] = BDSProfile.objects.filter(is_member=True).count() context["member_count"] = BDSProfile.objects.filter(is_member=True).count()
context["nb_expired"] = BDSProfile.expired_members().count()
return context return context
@ -42,9 +36,6 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView):
"profile": ProfileForm, "profile": ProfileForm,
} }
def get_user_initial(self):
return {"is_buro": self.get_user_instance().has_perm("bds.is_team")}
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.user = get_object_or_404(User, pk=self.kwargs["pk"]) self.user = get_object_or_404(User, pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -61,11 +52,6 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView):
def form_valid(self, forms): def form_valid(self, forms):
user = forms["user"].save() user = forms["user"].save()
profile = forms["profile"].save(commit=False) profile = forms["profile"].save(commit=False)
perm = Permission.objects.get(content_type__app_label="bds", codename="is_team")
if forms["user"].cleaned_data["is_buro"]:
user.user_permissions.add(perm)
else:
user.user_permissions.remove(perm)
profile.user = user profile.user = user
profile.save() profile.save()
messages.success(self.request, _("Profil mis à jour avec succès !")) messages.success(self.request, _("Profil mis à jour avec succès !"))
@ -142,43 +128,3 @@ class UserDeleteView(StaffRequiredMixin, DeleteView):
messages.success(request, self.success_message) messages.success(request, self.success_message)
return super().delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
class ResetMembershipListView(StaffRequiredMixin, ListView):
model = BDSProfile
template_name = "bds/expired_members.html"
def get_queryset(self):
return BDSProfile.expired_members()
class ResetMembershipView(StaffRequiredMixin, RedirectView):
url = reverse_lazy("bds:members.expired")
def get(self, request, *args, **kwargs):
qs = BDSProfile.expired_members()
nb = qs.count()
qs.update(cotisation_period="NO", is_member=False, mails_bds=False)
messages.success(request, f"{nb} adhésions réinitialisées")
return super().get(request, *args, **kwargs)
@permission_required("bds.is_team")
def export_members(request):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=membres_bds.csv"
writer = csv.writer(response)
for profile in BDSProfile.objects.filter(is_member=True).all():
user = profile.user
bits = [
user.username,
user.get_full_name(),
user.email,
]
writer.writerow([str(bit) for bit in bits])
return response

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]

8
cof/asgi.py Normal file
View file

@ -0,0 +1,8 @@
import os
from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
channel_layer = get_channel_layer()

3
cof/routing.py Normal file
View file

@ -0,0 +1,3 @@
from channels.routing import include
routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")]

View file

@ -1,9 +1,7 @@
""" """
Settings de production de GestioBDS. Django development settings for the cof project.
The settings that are not listed here are imported from .common
Surcharge les settings définis dans common.py
""" """
from .common import * # NOQA from .common import * # NOQA
from .common import INSTALLED_APPS from .common import INSTALLED_APPS

View file

@ -1,13 +1,8 @@
""" """
Settings de production de GestioCOF. Django development settings for the cof project.
The settings that are not listed here are imported from .common
Surcharge les settings définis dans common.py
""" """
import os import os
from datetime import timedelta
from django.utils import timezone
from .common import * # NOQA from .common import * # NOQA
from .common import ( from .common import (
@ -28,8 +23,8 @@ REDIS_DB = import_secret("REDIS_DB")
REDIS_HOST = import_secret("REDIS_HOST") REDIS_HOST = import_secret("REDIS_HOST")
REDIS_PORT = import_secret("REDIS_PORT") REDIS_PORT = import_secret("REDIS_PORT")
HCAPTCHA_SITEKEY = import_secret("HCAPTCHA_SITEKEY") RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
HCAPTCHA_SECRET = import_secret("HCAPTCHA_SECRET") RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
# --- # ---
@ -52,7 +47,7 @@ INSTALLED_APPS = (
+ [ + [
"bda", "bda",
"petitscours", "petitscours",
"hcaptcha", "captcha",
"kfet", "kfet",
"kfet.open", "kfet.open",
"channels", "channels",
@ -85,6 +80,7 @@ MIDDLEWARE = (
+ MIDDLEWARE + MIDDLEWARE
+ [ + [
"djconfig.middleware.DjConfigMiddleware", "djconfig.middleware.DjConfigMiddleware",
"wagtail.core.middleware.SiteMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
] ]
) )
@ -108,23 +104,15 @@ MEDIA_URL = "/gestion/media/"
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
ASGI_APPLICATION = "gestioasso.routing.application"
# --- # ---
# Auth-related stuff # Auth-related stuff
# --- # ---
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS += [
[ "gestioncof.shared.COFCASBackend",
# Must be in first "kfet.auth.backends.GenericBackend",
"kfet.auth.backends.BlockFrozenAccountBackend" ]
]
+ AUTHENTICATION_BACKENDS
+ [
"gestioncof.shared.COFCASBackend",
"kfet.auth.backends.GenericBackend",
]
)
LOGIN_URL = "cof-login" LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
@ -148,7 +136,7 @@ CACHES = {
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "shared.channels.ChannelLayer", "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [ "hosts": [
( (
@ -161,9 +149,11 @@ CHANNEL_LAYERS = {
) )
] ]
}, },
"ROUTING": "cof.routing.routing",
} }
} }
# --- # ---
# reCAPTCHA settings # reCAPTCHA settings
# https://github.com/praekelt/django-recaptcha # https://github.com/praekelt/django-recaptcha
@ -205,29 +195,8 @@ MAIL_DATA = {
"REPLYTO": "cof@ens.fr", "REPLYTO": "cof@ens.fr",
}, },
"rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"}, "rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
"kfet": {
"FROM": "La K-Fêt <chefs-k-fet@ens.fr>",
"REPLYTO": "La K-Fêt <chefs-k-fet@ens.fr>",
},
"revente": { "revente": {
"FROM": "BdA-Revente <bda-revente@ens.fr>", "FROM": "BdA-Revente <bda-revente@ens.fr>",
"REPLYTO": "BdA-Revente <bda-revente@ens.fr>", "REPLYTO": "BdA-Revente <bda-revente@ens.fr>",
}, },
} }
# ---
# kfet history limits
# ---
# L'historique n'est accesible que d'aujourd'hui
# à aujourd'hui - KFET_HISTORY_DATE_LIMIT
KFET_HISTORY_DATE_LIMIT = timedelta(days=7)
# Limite plus longue pour les chefs/trez
# (qui ont la permission kfet.access_old_history)
KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30)
# These accounts don't represent actual people and can be freely accessed
# Identification based on trigrammes
KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES = ["LIQ", "#13"]
KFET_HISTORY_NO_DATE_LIMIT = timezone.datetime(1794, 10, 30) # AKA the distant past

View file

@ -1,5 +1,8 @@
""" """
Settings par défaut et settings communs à GestioCOF et GestioBDS. Django common settings for cof project.
Everything which is supposed to be identical between the production server and
the local development server should be here.
""" """
import os import os
@ -62,7 +65,7 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.admindocs", "django.contrib.admindocs",
"gestioasso.apps.IgnoreSrcStaticFilesConfig", "cof.apps.IgnoreSrcStaticFilesConfig",
"django_cas_ng", "django_cas_ng",
"bootstrapform", "bootstrapform",
"widget_tweaks", "widget_tweaks",
@ -79,7 +82,7 @@ MIDDLEWARE = [
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
] ]
ROOT_URLCONF = "gestioasso.urls" ROOT_URLCONF = "cof.urls"
TEMPLATES = [ TEMPLATES = [
{ {
@ -101,7 +104,7 @@ TEMPLATES = [
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": DBNAME, "NAME": DBNAME,
"USER": DBUSER, "USER": DBUSER,
"PASSWORD": DBPASSWD, "PASSWORD": DBPASSWD,
@ -111,7 +114,6 @@ DATABASES = {
SITE_ID = 1 SITE_ID = 1
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# --- # ---
# Internationalization # Internationalization
@ -124,7 +126,7 @@ USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = (("fr", "Français"), ("en", "English")) LANGUAGES = (("fr", "Français"), ("en", "English"))
FORMAT_MODULE_PATH = "gestioasso.locale" FORMAT_MODULE_PATH = "cof.locale"
# --- # ---

View file

@ -1,10 +1,4 @@
""" """Django local development settings."""
Settings utilisés dans la VM Vagrant.
Active toutes les applications (de GestioCOF et de GestioBDS).
Surcharge les settings définis dans common.py
"""
import os import os
from . import bds_prod from . import bds_prod

View file

@ -1,9 +1,4 @@
""" """Django local development settings."""
Settings utilisés lors d'un développement en local (dans un virtualenv).
Active toutes les applications (de GestioCOF et de GestioBDS).
Surcharge les settings définis dans common.py
"""
import os import os
from . import bds_prod from . import bds_prod
@ -47,7 +42,8 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
# Use the default in memory asgi backend for local development # Use the default in memory asgi backend for local development
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "channels.layers.InMemoryChannelLayer", "BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "cof.routing.routing",
} }
} }

View file

@ -1,7 +1,3 @@
"""
Secrets à re-définir en production.
"""
SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah" SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah"
ADMINS = None ADMINS = None
SERVER_EMAIL = "root@vagrant" SERVER_EMAIL = "root@vagrant"
@ -16,8 +12,8 @@ REDIS_PORT = 6379
REDIS_DB = 0 REDIS_DB = 0
REDIS_HOST = "127.0.0.1" REDIS_HOST = "127.0.0.1"
HCAPTCHA_SITEKEY = "10000000-ffff-ffff-ffff-000000000001" RECAPTCHA_PUBLIC_KEY = "DUMMY"
HCAPTCHA_SECRET = "0x0000000000000000000000000000000000000000" RECAPTCHA_PRIVATE_KEY = "DUMMY"
EMAIL_HOST = None EMAIL_HOST = None

View file

@ -8,23 +8,21 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
bds_is_alone = (
"bds" in settings.INSTALLED_APPS and "gestioncof" not in settings.INSTALLED_APPS
)
admin.autodiscover() admin.autodiscover()
urlpatterns = [ urlpatterns = [
# Redirection / → /gestion, only useful for developpers.
path("", RedirectView.as_view(url="gestion/")),
# Website administration (independent from installed apps) # Website administration (independent from installed apps)
path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/doc/", include("django.contrib.admindocs.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] ]
if not bds_is_alone:
# Redirection / → /gestion, only useful for developpers.
urlpatterns.append(path("", RedirectView.as_view(url="gestion/")))
# App-specific urls # App-specific urls
bds_is_alone = (
"bds" in settings.INSTALLED_APPS and "gestioncof" not in settings.INSTALLED_APPS
)
app_dict = { app_dict = {
"bds": "" if bds_is_alone else "bds/", "bds": "" if bds_is_alone else "bds/",
"kfet": "k-fet/", "kfet": "k-fet/",
@ -35,7 +33,7 @@ app_dict = {
"events": "gestion/event_v2/", # the events module is still experimental ! "events": "gestion/event_v2/", # the events module is still experimental !
"authens": "gestion/authens/", "authens": "gestion/authens/",
} }
for app_name, url_prefix in app_dict.items(): for (app_name, url_prefix) in app_dict.items():
if app_name in settings.INSTALLED_APPS: if app_name in settings.INSTALLED_APPS:
urlpatterns += [path(url_prefix, include("{}.urls".format(app_name)))] urlpatterns += [path(url_prefix, include("{}.urls".format(app_name)))]

View file

@ -2,5 +2,5 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.bds_prod") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.bds_prod")
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0001_event"), ("events", "0001_event"),

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0002_event_subscribers"), ("events", "0002_event_subscribers"),

View file

@ -5,6 +5,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0003_options_and_extra_fields"), ("events", "0003_options_and_extra_fields"),

View file

@ -1,63 +0,0 @@
# Generated by Django 3.2.13 on 2022-06-30 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0004_unique_constraints"),
]
operations = [
migrations.AlterUniqueTogether(
name="extrafield",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="extrafieldcontent",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="option",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="optionchoice",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="registration",
unique_together=set(),
),
migrations.AddConstraint(
model_name="extrafield",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_extra_field"
),
),
migrations.AddConstraint(
model_name="extrafieldcontent",
constraint=models.UniqueConstraint(
fields=("field", "registration"), name="unique_extra_field_content"
),
),
migrations.AddConstraint(
model_name="option",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_event_option"
),
),
migrations.AddConstraint(
model_name="optionchoice",
constraint=models.UniqueConstraint(
fields=("option", "choice"), name="unique_option_choice"
),
),
migrations.AddConstraint(
model_name="registration",
constraint=models.UniqueConstraint(
fields=("event", "user"), name="unique_registration"
),
),
]

View file

@ -72,13 +72,9 @@ class Option(models.Model):
multi_choices = models.BooleanField(_("choix multiples"), default=False) multi_choices = models.BooleanField(_("choix multiples"), default=False)
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "name"], name="unique_event_option"
)
]
verbose_name = _("option d'événement") verbose_name = _("option d'événement")
verbose_name_plural = _("options d'événement") verbose_name_plural = _("options d'événement")
unique_together = [["event", "name"]]
def __str__(self): def __str__(self):
return self.name return self.name
@ -91,13 +87,9 @@ class OptionChoice(models.Model):
choice = models.CharField(_("choix"), max_length=200) choice = models.CharField(_("choix"), max_length=200)
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["option", "choice"], name="unique_option_choice"
)
]
verbose_name = _("choix d'option d'événement") verbose_name = _("choix d'option d'événement")
verbose_name_plural = _("choix d'option d'événement") verbose_name_plural = _("choix d'option d'événement")
unique_together = [["option", "choice"]]
def __str__(self): def __str__(self):
return self.choice return self.choice
@ -126,9 +118,7 @@ class ExtraField(models.Model):
field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE) field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE)
class Meta: class Meta:
constraints = [ unique_together = [["event", "name"]]
models.UniqueConstraint(fields=["event", "name"], name="unique_extra_field")
]
class ExtraFieldContent(models.Model): class ExtraFieldContent(models.Model):
@ -147,13 +137,9 @@ class ExtraFieldContent(models.Model):
) )
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["field", "registration"], name="unique_extra_field_content"
)
]
verbose_name = _("contenu d'un champ événement supplémentaire") verbose_name = _("contenu d'un champ événement supplémentaire")
verbose_name_plural = _("contenus d'un champ événement supplémentaire") verbose_name_plural = _("contenus d'un champ événement supplémentaire")
unique_together = [["field", "registration"]]
def __str__(self): def __str__(self):
max_length = 50 max_length = 50
@ -177,13 +163,9 @@ class Registration(models.Model):
) )
class Meta: class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "user"], name="unique_registration"
)
]
verbose_name = _("inscription à un événement") verbose_name = _("inscription à un événement")
verbose_name_plural = _("inscriptions à un événement") verbose_name_plural = _("inscriptions à un événement")
unique_together = [["event", "user"]]
def __str__(self): def __str__(self):
return "inscription de {} à {}".format(self.user, self.event) return "inscription de {} à {}".format(self.user, self.event)

View file

@ -54,9 +54,7 @@ class CSVExportAccessTest(MessagePatch, TestCase):
def test_get(self): def test_get(self):
client = Client() client = Client()
client.force_login( client.force_login(self.staff)
self.staff, backend="django.contrib.auth.backends.ModelBackend"
)
r = client.get(self.url) r = client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -68,7 +66,7 @@ class CSVExportAccessTest(MessagePatch, TestCase):
def test_unauthorised(self): def test_unauthorised(self):
client = Client() client = Client()
client.force_login(self.u1, backend="django.contrib.auth.backends.ModelBackend") client.force_login(self.u1)
r = client.get(self.url) r = client.get(self.url)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@ -88,9 +86,7 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase):
) )
self.staff = make_staff_user("staff") self.staff = make_staff_user("staff")
self.client = Client() self.client = Client()
self.client.force_login( self.client.force_login(self.staff)
self.staff, backend="django.contrib.auth.backends.ModelBackend"
)
def test_simple_event(self): def test_simple_event(self):
self.event.subscribers.set([self.u1, self.u2]) self.event.subscribers.set([self.u1, self.u2])

View file

@ -1,15 +0,0 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local")
django.setup()
application = get_default_application()

View file

@ -1,20 +0,0 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import path
from kfet.routing import KFRouter
application = ProtocolTypeRouter(
{
# WebSocket chat handler
"websocket": AuthMiddlewareStack(
URLRouter(
[
path("ws/k-fet", KFRouter),
]
)
),
"http": get_asgi_application(),
}
)

View file

@ -0,0 +1 @@
default_app_config = "gestioncof.apps.GestioncofConfig"

View file

@ -6,7 +6,7 @@ from django.contrib.auth.models import Group, Permission, User
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from gestioncof.models import ( from gestioncof.models import (
Club, Club,
@ -100,6 +100,28 @@ class CofProfileInline(admin.StackedInline):
inline_classes = ("collapse open",) inline_classes = ("collapse open",)
class FkeyLookup(object):
def __init__(self, fkeydecl, short_description=None, admin_order_field=None):
self.fk, fkattrs = fkeydecl.split("__", 1)
self.fkattrs = fkattrs.split("__")
self.short_description = short_description or self.fkattrs[-1]
self.admin_order_field = admin_order_field or fkeydecl
def __get__(self, obj, klass):
if obj is None:
"""
hack required to make Django validate (if obj is
None, then we're a class, and classes are callable
<wink>)
"""
return self
item = getattr(obj, self.fk)
for attr in self.fkattrs:
item = getattr(item, attr)
return item
def ProfileInfo(field, short_description, boolean=False): def ProfileInfo(field, short_description, boolean=False):
def getter(self): def getter(self):
try: try:
@ -112,6 +134,7 @@ def ProfileInfo(field, short_description, boolean=False):
return getter return getter
User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper")
User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement") User.profile_departement = ProfileInfo("departement", "Departement")
@ -140,6 +163,7 @@ class UserProfileAdmin(UserAdmin):
is_cof.boolean = True is_cof.boolean = True
list_display = UserAdmin.list_display + ( list_display = UserAdmin.list_display + (
"profile_login_clipper",
"profile_phone", "profile_phone",
"profile_occupation", "profile_occupation",
"profile_mailing_cof", "profile_mailing_cof",

View file

@ -0,0 +1 @@
default_app_config = "gestioncof.cms.apps.COFCMSAppConfig"

View file

@ -13,6 +13,7 @@ import gestioncof.cms.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("cofcms", "0001_initial")] dependencies = [("cofcms", "0001_initial")]
operations = [ operations = [

View file

@ -6,6 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("cofcms", "0002_auto_20190523_1521"), ("cofcms", "0002_auto_20190523_1521"),
] ]

View file

@ -6,6 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("cofcms", "0003_directory_entry_optional_links"), ("cofcms", "0003_directory_entry_optional_links"),
] ]

View file

@ -16,7 +16,7 @@
<link rel="stylesheet" type="text/css" href="{% static "fonts/SourceSansPro/sourceSansPro.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "fonts/SourceSansPro/sourceSansPro.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "cofcms/css/screen.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "cofcms/css/screen.css" %}"/>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0; minimum-scale=1.0;"> <meta name="viewport" content="width=device-width, initial-scale=1.0; minimum-scale=1.0;">
</head> </head>
@ -32,27 +32,27 @@
<section class="bottom-menu"> <section class="bottom-menu">
<nav> <nav>
{% flat_menu "cof-nav-int" template="cofcms/base_nav.html" apply_active_classes=True %} {% flat_menu "cof-nav-int" template="cofcms/base_nav.html" apply_active_classes=True %}
{% get_current_language as curlang %}
{% get_current_language as curlang %} <div class="lang-select">
<div class="lang-select">
{% if curlang == 'en' %} {% if curlang == 'en' %}
{% language 'fr' %} {% language 'fr' %}
<a href="{% pageurl self %}" title="Français"><img src="{% static "cofcms/images/fr.png" %}"></a> <a href="{% pageurl self %}" title="Français"><img src="{% static "cofcms/images/fr.png" %}"></a>
{% endlanguage %} {% endlanguage %}
{% else %} {% else %}
{% language 'en' %} {% language 'en' %}
<a href="{% pageurl self %}" title="English"><img src="{% static "cofcms/images/en.png" %}"></a> <a href="{% pageurl self %}" title="English"><img src="{% static "cofcms/images/en.png" %}"></a>
{% endlanguage %} {% endlanguage %}
{% endif %} {% endif %}
</div> </div>
</nav> </nav>
</section> </section>
</header> </header>
<div class="container"> <div class="container">
{% block superaside %}{% endblock %} {% block superaside %}{% endblock %}
<div class="content"> <div class="content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>

View file

@ -17,17 +17,17 @@
{% block content %} {% block content %}
<section class="intro"> <section class="intro">
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
<div>{{ page.introduction|richtext }}</div> <div>{{ page.introduction|safe }}</div>
</section> </section>
<section class="actulist"> <section class="actulist">
{% if actus.has_previous %} {% if actus.has_previous %}
<a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a> <a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a>
{% endif %} {% endif %}
{% if actus.has_next %} {% if actus.has_next %}
<a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a> <a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a>
{% endif %} {% endif %}
{% for actu in page.actus %} {% for actu in page.actus %}
<article class="actu"> <article class="actu">
<div class="actu-image" {% if actu.image %}{% image actu.image fill-400x200 as img %}style="background-image:url('{{ img.url }}');"{% endif %}></div> <div class="actu-image" {% if actu.image %}{% image actu.image fill-400x200 as img %}style="background-image:url('{{ img.url }}');"{% endif %}></div>
@ -36,7 +36,7 @@
{% if actu.is_event %} {% if actu.is_event %}
<p><span class="actu-dates">{{ actu|dates|capfirst }}</span><br />{{ actu.chapo }}</p> <p><span class="actu-dates">{{ actu|dates|capfirst }}</span><br />{{ actu.chapo }}</p>
{% else %} {% else %}
{{ actu.body|richtext|truncatewords_html:15 }} {{ actu.body|safe|truncatewords_html:15 }}
{% endif %} {% endif %}
<a href="{% pageurl actu %}">{% trans "Lire plus" %} &gt;</a> <a href="{% pageurl actu %}">{% trans "Lire plus" %} &gt;</a>
</div> </div>
@ -44,10 +44,10 @@
{% endfor %} {% endfor %}
{% if actus.has_previous %} {% if actus.has_previous %}
<a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a> <a class="block prev-actus" href="?page={{ actus.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus récentes" %}</a>
{% endif %} {% endif %}
{% if actus.has_next %} {% if actus.has_next %}
<a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a> <a class="block next-actus" href="?page={{ actus.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&amp;{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">{% trans "Actualités plus anciennes" %}</a>
{% endif %} {% endif %}
</section> </section>
{% endblock %} {% endblock %}

Some files were not shown because too many files have changed in this diff Show more