Module de pétitions inspiré de degette
This commit is contained in:
parent
1353c4e702
commit
7068c6dd18
24 changed files with 1587 additions and 3 deletions
|
@ -55,6 +55,7 @@ INSTALLED_APPS = [
|
|||
"kadenios.apps.IgnoreSrcStaticFilesConfig",
|
||||
"shared",
|
||||
"elections",
|
||||
"petitions",
|
||||
"authens",
|
||||
]
|
||||
|
||||
|
@ -93,6 +94,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
|||
|
||||
DEFAULT_FROM_EMAIL = "Kadenios <klub-dev@ens.fr>"
|
||||
|
||||
SERVER_DOMAIN = "vote.eleves.ens.fr"
|
||||
|
||||
# #############################################################################
|
||||
# Paramètres d'authentification
|
||||
# #############################################################################
|
||||
|
|
|
@ -8,6 +8,7 @@ urlpatterns = [
|
|||
path("", HomeView.as_view(), name="kadenios"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("elections/", include("elections.urls")),
|
||||
path("petitions/", include("petitions.urls")),
|
||||
path("auth/", include("shared.auth.urls")),
|
||||
path("authens/", include("authens.urls")),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
|
|
0
petitions/__init__.py
Normal file
0
petitions/__init__.py
Normal file
6
petitions/apps.py
Normal file
6
petitions/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PetitionsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "petitions"
|
68
petitions/forms.py
Normal file
68
petitions/forms.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from translated_fields import language_code_formfield_callback
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Petition, Signature
|
||||
|
||||
|
||||
class PetitionForm(forms.ModelForm):
|
||||
formfield_callback = language_code_formfield_callback
|
||||
|
||||
class Meta:
|
||||
model = Petition
|
||||
fields = [
|
||||
*Petition.title.fields,
|
||||
*Petition.text.fields,
|
||||
*Petition.letter.fields,
|
||||
"launch_date",
|
||||
]
|
||||
widgets = {
|
||||
"text_en": forms.Textarea(attrs={"rows": 3}),
|
||||
"text_fr": forms.Textarea(attrs={"rows": 4}),
|
||||
"letter_en": forms.Textarea(attrs={"rows": 3}),
|
||||
"letter_fr": forms.Textarea(attrs={"rows": 4}),
|
||||
}
|
||||
|
||||
|
||||
class DeleteForm(forms.Form):
|
||||
def __init__(self, **kwargs):
|
||||
signature = kwargs.pop("signature", None)
|
||||
super().__init__(**kwargs)
|
||||
if signature is not None:
|
||||
self.fields["delete"].label = _("Supprimer la signature de {} ?").format(
|
||||
signature.full_name
|
||||
)
|
||||
|
||||
delete = forms.ChoiceField(
|
||||
label=_("Supprimer"), choices=(("non", _("Non")), ("oui", _("Oui")))
|
||||
)
|
||||
|
||||
|
||||
class ValidateForm(forms.Form):
|
||||
def __init__(self, **kwargs):
|
||||
signature = kwargs.pop("signature", None)
|
||||
super().__init__(**kwargs)
|
||||
if signature is not None:
|
||||
self.fields["validate"].label = _("Valider la signature de {} ?").format(
|
||||
signature.full_name
|
||||
)
|
||||
|
||||
validate = forms.ChoiceField(
|
||||
label=_("Valider"), choices=(("non", _("Non")), ("oui", _("Oui")))
|
||||
)
|
||||
|
||||
|
||||
class SignatureForm(forms.ModelForm):
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data["email"]
|
||||
if self.instance.petition.signatures.filter(email__iexact=email):
|
||||
self.add_error(
|
||||
"email",
|
||||
_("Une personne a déjà signé la pétition avec cette adresse mail."),
|
||||
)
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
model = Signature
|
||||
fields = ["full_name", "email", "status", "department", "elected"]
|
262
petitions/migrations/0001_initial.py
Normal file
262
petitions/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,262 @@
|
|||
# Generated by Django 3.2.3 on 2021-05-29 20:34
|
||||
|
||||
import datetime
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Petition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title_fr", models.CharField(max_length=255, verbose_name="titre")),
|
||||
(
|
||||
"title_en",
|
||||
models.CharField(blank=True, max_length=255, verbose_name="titre"),
|
||||
),
|
||||
("text_fr", models.TextField(blank=True, verbose_name="texte")),
|
||||
("text_en", models.TextField(blank=True, verbose_name="texte")),
|
||||
("letter_fr", models.TextField(blank=True, verbose_name="lettre")),
|
||||
("letter_en", models.TextField(blank=True, verbose_name="lettre")),
|
||||
(
|
||||
"launch_date",
|
||||
models.DateField(
|
||||
default=datetime.date.today, verbose_name="date d'ouverture"
|
||||
),
|
||||
),
|
||||
(
|
||||
"archived",
|
||||
models.BooleanField(default=False, verbose_name="archivée"),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="petitions_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-launch_date"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Signature",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"full_name",
|
||||
models.CharField(max_length=255, verbose_name="nom complet"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(max_length=254, verbose_name="adresse mail"),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("normalien-license", "Normalien·ne en licence"),
|
||||
("normalien-master", "Normalien·ne en master"),
|
||||
("normalien-cesure", "Normalien·ne en césure"),
|
||||
("normalien-pre-these", "Normalien·ne en pré-thèse"),
|
||||
(
|
||||
"normalien-concours",
|
||||
"Normalien·ne préparant un concours (Agrégation, ENA...)",
|
||||
),
|
||||
(
|
||||
"normalien-stage",
|
||||
"Normalien·ne en stage ou en année de formation complémentaire",
|
||||
),
|
||||
(
|
||||
"normalien-administration",
|
||||
"Normalien·ne dans l'administration publique",
|
||||
),
|
||||
("normalien-entreprise", "Normalien·ne dans l'entreprise"),
|
||||
(
|
||||
"normalien-chercheur",
|
||||
"Normalien·ne et chercheur·se en Université",
|
||||
),
|
||||
("masterien", "Mastérien·ne"),
|
||||
("these", "Doctorant·e"),
|
||||
("postdoc", "Post-doctorant·e"),
|
||||
("archicube", "Ancien·ne élève ou étudiant·e"),
|
||||
("chercheur-ens", "Chercheur·se à l’ENS"),
|
||||
("enseignant-ens", "Enseignant·e à l’ENS"),
|
||||
(
|
||||
"enseignant-chercheur",
|
||||
"Enseignant·e et chercheur·se à l’ENS",
|
||||
),
|
||||
("enseignant-cpge", "Enseignant·e en classe préparatoire"),
|
||||
("charge-td", "Chargé·e de TD"),
|
||||
("direction-ens", "Membre de la direction de l'ENS"),
|
||||
(
|
||||
"direction-departement",
|
||||
"Membre de la direction d'un département",
|
||||
),
|
||||
(
|
||||
"directeur",
|
||||
"Directeur·rice de l'Ecole Normale Supérieure",
|
||||
),
|
||||
(
|
||||
"employe-cost",
|
||||
"Employé·e du Service des Concours, de la Scolarité et des Thèses",
|
||||
),
|
||||
(
|
||||
"employe-srh",
|
||||
"Employé·e du Service des Ressources Humaines",
|
||||
),
|
||||
(
|
||||
"employe-spr",
|
||||
"Employé·e du Service Partenariat de la Recherche",
|
||||
),
|
||||
(
|
||||
"employe-sfc",
|
||||
"Employé·e du Service Financier et Comptable",
|
||||
),
|
||||
(
|
||||
"employe-cri",
|
||||
"Employé·e du Centre de Ressources Informatiques",
|
||||
),
|
||||
("employe-sp", "Employé·e du Service Patrimoine"),
|
||||
(
|
||||
"employe-sps",
|
||||
"Employé·e du Service Prévention et Sécurité",
|
||||
),
|
||||
("employe-sl", "Employé·e du Service Logistique"),
|
||||
("employe-sr", "Employé·e du Service de la Restauration"),
|
||||
("employe-ps", "Employé·e du Pôle Santé"),
|
||||
(
|
||||
"employe-spi",
|
||||
"Employé·e du Service de Prestations Informatiques",
|
||||
),
|
||||
(
|
||||
"employe-bibliotheque",
|
||||
"Employé·e d'une des bibliothèques",
|
||||
),
|
||||
(
|
||||
"employe-exterieur",
|
||||
"Employé·e d'une société prestataire de service à l'ENS",
|
||||
),
|
||||
("pei", "Élève du PEI"),
|
||||
("autre", "Autre"),
|
||||
],
|
||||
max_length=24,
|
||||
verbose_name="statut",
|
||||
),
|
||||
),
|
||||
(
|
||||
"elected",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("", "Aucun"),
|
||||
("dg", "Membre de la Délégation Générale"),
|
||||
("cof", "Membre du bureau du COF"),
|
||||
("bda", "Membre du bureau du BdA"),
|
||||
("bds", "Membre du bureau du BDS"),
|
||||
("cs", "Membre du Conseil Scientifique"),
|
||||
("ca", "Membre du Conseil d'Administration"),
|
||||
("ce", "Membre de la Commission des Études"),
|
||||
("chsct", "Membre du CHSCT"),
|
||||
],
|
||||
default="",
|
||||
max_length=5,
|
||||
verbose_name="poste d'élu",
|
||||
),
|
||||
),
|
||||
(
|
||||
"department",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("", "Aucun département"),
|
||||
("arts", "Département Arts"),
|
||||
("litteratures", "Département Littératures et langage"),
|
||||
("histoire", "Département d’Histoire"),
|
||||
("economie", "Département d’Économie"),
|
||||
("philosophie", "Département de Philosophie"),
|
||||
("sciences-sociales", "Département de Sciences Sociales"),
|
||||
("antiquite", "Département des Sciences de l’Antiquité"),
|
||||
("ecla", "Espace des cultures et langues d’ailleurs"),
|
||||
("geographie", "Département Géographie et Territoires"),
|
||||
("di", "Département d’Informatique"),
|
||||
("cognition", "Département d'Études cognitives"),
|
||||
("biologie", "Département de Biologie"),
|
||||
("chimie", "Département de Chimie"),
|
||||
("geosciences", "Département de Géosciences"),
|
||||
("math", "Département de Mathématiques et applications"),
|
||||
("phys", "Département de Physique"),
|
||||
(
|
||||
"environnement",
|
||||
"Centre de formation sur l’Environnement et la Société",
|
||||
),
|
||||
],
|
||||
default="",
|
||||
max_length=17,
|
||||
verbose_name="département",
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="adresse mail vérifiée"
|
||||
),
|
||||
),
|
||||
(
|
||||
"valid",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="signature vérifiée"
|
||||
),
|
||||
),
|
||||
(
|
||||
"timestamp",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="jour de signature"
|
||||
),
|
||||
),
|
||||
(
|
||||
"petition",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="signatures",
|
||||
to="petitions.petition",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="signature",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("petition", "email"), name="unique_signature"
|
||||
),
|
||||
),
|
||||
]
|
20
petitions/migrations/0002_alter_petition_options.py
Normal file
20
petitions/migrations/0002_alter_petition_options.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.3 on 2021-05-29 20:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("petitions", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="petition",
|
||||
options={
|
||||
"ordering": ["-launch_date"],
|
||||
"permissions": [("is_admin", "Peut administrer des pétitions")],
|
||||
},
|
||||
),
|
||||
]
|
143
petitions/migrations/0003_auto_20210530_1740.py
Normal file
143
petitions/migrations/0003_auto_20210530_1740.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
# Generated by Django 3.2.3 on 2021-05-30 15:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("petitions", "0002_alter_petition_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="signature",
|
||||
name="department",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Aucun département"),
|
||||
("arts", "Département Arts"),
|
||||
("litteratures", "Département Littératures et langage"),
|
||||
("histoire", "Département d’Histoire"),
|
||||
("economie", "Département d’Économie"),
|
||||
("philosophie", "Département de Philosophie"),
|
||||
("sciences-sociales", "Département de Sciences Sociales"),
|
||||
("antiquite", "Département des Sciences de l’Antiquité"),
|
||||
("ecla", "Espace des cultures et langues d’ailleurs"),
|
||||
("geographie", "Département Géographie et Territoires"),
|
||||
("di", "Département d’Informatique"),
|
||||
("cognition", "Département d'Études cognitives"),
|
||||
("biologie", "Département de Biologie"),
|
||||
("chimie", "Département de Chimie"),
|
||||
("geosciences", "Département de Géosciences"),
|
||||
("math", "Département de Mathématiques et applications"),
|
||||
("phys", "Département de Physique"),
|
||||
(
|
||||
"environnement",
|
||||
"Centre de formation sur l’Environnement et la Société",
|
||||
),
|
||||
],
|
||||
default="",
|
||||
max_length=17,
|
||||
verbose_name="département",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="signature",
|
||||
name="elected",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Aucun"),
|
||||
("dg", "Membre de la Délégation Générale"),
|
||||
("cof", "Membre du bureau du COF"),
|
||||
("bda", "Membre du bureau du BdA"),
|
||||
("bds", "Membre du bureau du BDS"),
|
||||
("cs", "Membre du Conseil Scientifique"),
|
||||
("ca", "Membre du Conseil d'Administration"),
|
||||
("ce", "Membre de la Commission des Études"),
|
||||
("chsct", "Membre du CHSCT"),
|
||||
],
|
||||
default="",
|
||||
max_length=5,
|
||||
verbose_name="poste d'élu",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="signature",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("autre", "Autre"),
|
||||
("normalien-license", "Normalien·ne en licence"),
|
||||
("normalien-master", "Normalien·ne en master"),
|
||||
("normalien-cesure", "Normalien·ne en césure"),
|
||||
("normalien-pre-these", "Normalien·ne en pré-thèse"),
|
||||
(
|
||||
"normalien-concours",
|
||||
"Normalien·ne préparant un concours (Agrégation, ENA...)",
|
||||
),
|
||||
(
|
||||
"normalien-stage",
|
||||
"Normalien·ne en stage ou en année de formation complémentaire",
|
||||
),
|
||||
(
|
||||
"normalien-administration",
|
||||
"Normalien·ne dans l'administration publique",
|
||||
),
|
||||
("normalien-entreprise", "Normalien·ne dans l'entreprise"),
|
||||
(
|
||||
"normalien-chercheur",
|
||||
"Normalien·ne et chercheur·se en Université",
|
||||
),
|
||||
("masterien", "Mastérien·ne"),
|
||||
("these", "Doctorant·e"),
|
||||
("postdoc", "Post-doctorant·e"),
|
||||
("archicube", "Ancien·ne élève ou étudiant·e"),
|
||||
("chercheur-ens", "Chercheur·se à l’ENS"),
|
||||
("enseignant-ens", "Enseignant·e à l’ENS"),
|
||||
("enseignant-chercheur", "Enseignant·e et chercheur·se à l’ENS"),
|
||||
("enseignant-cpge", "Enseignant·e en classe préparatoire"),
|
||||
("charge-td", "Chargé·e de TD"),
|
||||
("direction-ens", "Membre de la direction de l'ENS"),
|
||||
(
|
||||
"direction-departement",
|
||||
"Membre de la direction d'un département",
|
||||
),
|
||||
("directeur", "Directeur·rice de l'Ecole Normale Supérieure"),
|
||||
(
|
||||
"employe-cost",
|
||||
"Employé·e du Service des Concours, de la Scolarité et des Thèses",
|
||||
),
|
||||
("employe-srh", "Employé·e du Service des Ressources Humaines"),
|
||||
("employe-spr", "Employé·e du Service Partenariat de la Recherche"),
|
||||
("employe-sfc", "Employé·e du Service Financier et Comptable"),
|
||||
("employe-cri", "Employé·e du Centre de Ressources Informatiques"),
|
||||
("employe-sp", "Employé·e du Service Patrimoine"),
|
||||
("employe-sps", "Employé·e du Service Prévention et Sécurité"),
|
||||
("employe-sl", "Employé·e du Service Logistique"),
|
||||
("employe-sr", "Employé·e du Service de la Restauration"),
|
||||
("employe-ps", "Employé·e du Pôle Santé"),
|
||||
(
|
||||
"employe-spi",
|
||||
"Employé·e du Service de Prestations Informatiques",
|
||||
),
|
||||
("employe-bibliotheque", "Employé·e d'une des bibliothèques"),
|
||||
(
|
||||
"employe-exterieur",
|
||||
"Employé·e d'une société prestataire de service à l'ENS",
|
||||
),
|
||||
("pei", "Élève du PEI"),
|
||||
],
|
||||
default="autre",
|
||||
max_length=24,
|
||||
verbose_name="statut",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="signature",
|
||||
name="timestamp",
|
||||
field=models.DateTimeField(auto_now=True, verbose_name="horodatage"),
|
||||
),
|
||||
]
|
24
petitions/migrations/0004_signature_token.py
Normal file
24
petitions/migrations/0004_signature_token.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.3 on 2021-05-30 18:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import shared.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("petitions", "0003_auto_20210530_1740"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="signature",
|
||||
name="token",
|
||||
field=models.SlugField(
|
||||
default=shared.utils.token_generator,
|
||||
editable=False,
|
||||
verbose_name="token",
|
||||
),
|
||||
),
|
||||
]
|
0
petitions/migrations/__init__.py
Normal file
0
petitions/migrations/__init__.py
Normal file
18
petitions/mixins.py
Normal file
18
petitions/mixins.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class AdminOnlyMixin(PermissionRequiredMixin):
|
||||
"""Restreint l'accès aux admins"""
|
||||
|
||||
permission_required = "petitions.is_admin"
|
||||
|
||||
|
||||
class CreatorOnlyMixin(AdminOnlyMixin):
|
||||
"""Restreint l'accès au créateurice de l'élection"""
|
||||
|
||||
def get_next_url(self):
|
||||
return reverse("kadenios")
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(created_by=self.request.user)
|
85
petitions/models.py
Normal file
85
petitions/models.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
from datetime import date
|
||||
|
||||
from translated_fields import TranslatedFieldWithFallback
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared.utils import choices_length, token_generator
|
||||
|
||||
from .staticdefs import DEPARTMENTS, ELECTED, STATUSES
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# #############################################################################
|
||||
# Petition related models
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class Petition(models.Model):
|
||||
title = TranslatedFieldWithFallback(models.CharField(_("titre"), max_length=255))
|
||||
text = TranslatedFieldWithFallback(models.TextField(_("texte"), blank=True))
|
||||
letter = TranslatedFieldWithFallback(models.TextField(_("lettre"), blank=True))
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
related_name="petitions_created",
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
launch_date = models.DateField(_("date d'ouverture"), default=date.today)
|
||||
|
||||
archived = models.BooleanField(_("archivée"), default=False)
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("is_admin", _("Peut administrer des pétitions")),
|
||||
]
|
||||
ordering = ["-launch_date"]
|
||||
|
||||
|
||||
class Signature(models.Model):
|
||||
petition = models.ForeignKey(
|
||||
Petition, related_name="signatures", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
full_name = models.CharField(_("nom complet"), max_length=255)
|
||||
email = models.EmailField(_("adresse mail"))
|
||||
|
||||
status = models.CharField(
|
||||
_("statut"),
|
||||
choices=STATUSES,
|
||||
max_length=choices_length(STATUSES),
|
||||
default="autre",
|
||||
)
|
||||
elected = models.CharField(
|
||||
_("poste d'élu"),
|
||||
choices=ELECTED,
|
||||
max_length=choices_length(ELECTED),
|
||||
default="",
|
||||
blank=True,
|
||||
)
|
||||
department = models.CharField(
|
||||
_("département"),
|
||||
choices=DEPARTMENTS,
|
||||
max_length=choices_length(DEPARTMENTS),
|
||||
default="",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
verified = models.BooleanField(_("adresse mail vérifiée"), default=False)
|
||||
valid = models.BooleanField(_("signature vérifiée"), default=False)
|
||||
|
||||
token = models.SlugField(_("token"), editable=False, default=token_generator)
|
||||
|
||||
timestamp = models.DateTimeField(_("horodatage"), auto_now=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["petition", "email"], name="unique_signature"
|
||||
)
|
||||
]
|
118
petitions/staticdefs.py
Normal file
118
petitions/staticdefs.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
DEPARTMENTS = [
|
||||
("", _("Aucun département")),
|
||||
("arts", _("Département Arts")),
|
||||
("litteratures", _("Département Littératures et langage")),
|
||||
("histoire", _("Département d’Histoire")),
|
||||
("economie", _("Département d’Économie")),
|
||||
("philosophie", _("Département de Philosophie")),
|
||||
("sciences-sociales", _("Département de Sciences Sociales")),
|
||||
("antiquite", _("Département des Sciences de l’Antiquité")),
|
||||
("ecla", _("Espace des cultures et langues d’ailleurs")),
|
||||
("geographie", _("Département Géographie et Territoires")),
|
||||
("di", _("Département d’Informatique")),
|
||||
("cognition", "Département d'Études cognitives"),
|
||||
("biologie", _("Département de Biologie")),
|
||||
("chimie", _("Département de Chimie")),
|
||||
("geosciences", _("Département de Géosciences")),
|
||||
("math", _("Département de Mathématiques et applications")),
|
||||
("phys", _("Département de Physique")),
|
||||
("environnement", _("Centre de formation sur l’Environnement et la Société")),
|
||||
]
|
||||
|
||||
STATUSES = [
|
||||
("autre", _("Autre")),
|
||||
("normalien-license", _("Normalien·ne en licence")),
|
||||
("normalien-master", _("Normalien·ne en master")),
|
||||
("normalien-cesure", _("Normalien·ne en césure")),
|
||||
("normalien-pre-these", _("Normalien·ne en pré-thèse")),
|
||||
(
|
||||
"normalien-concours",
|
||||
_("Normalien·ne préparant un concours (Agrégation, ENA...)"),
|
||||
),
|
||||
(
|
||||
"normalien-stage",
|
||||
_("Normalien·ne en stage ou en année de formation complémentaire"),
|
||||
),
|
||||
("normalien-administration", _("Normalien·ne dans l'administration publique")),
|
||||
("normalien-entreprise", _("Normalien·ne dans l'entreprise")),
|
||||
("normalien-chercheur", _("Normalien·ne et chercheur·se en Université")),
|
||||
("masterien", _("Mastérien·ne")),
|
||||
("these", _("Doctorant·e")),
|
||||
("postdoc", _("Post-doctorant·e")),
|
||||
("archicube", _("Ancien·ne élève ou étudiant·e")),
|
||||
("chercheur-ens", _("Chercheur·se à l’ENS")),
|
||||
("enseignant-ens", _("Enseignant·e à l’ENS")),
|
||||
("enseignant-chercheur", _("Enseignant·e et chercheur·se à l’ENS")),
|
||||
("enseignant-cpge", _("Enseignant·e en classe préparatoire")),
|
||||
("charge-td", _("Chargé·e de TD")),
|
||||
("direction-ens", _("Membre de la direction de l'ENS")),
|
||||
("direction-departement", _("Membre de la direction d'un département")),
|
||||
("directeur", _("Directeur·rice de l'Ecole Normale Supérieure")),
|
||||
(
|
||||
"employe-cost",
|
||||
_("Employé·e du Service des Concours, de la Scolarité et des Thèses"),
|
||||
),
|
||||
("employe-srh", _("Employé·e du Service des Ressources Humaines")),
|
||||
("employe-spr", _("Employé·e du Service Partenariat de la Recherche")),
|
||||
("employe-sfc", _("Employé·e du Service Financier et Comptable")),
|
||||
("employe-cri", _("Employé·e du Centre de Ressources Informatiques")),
|
||||
("employe-sp", _("Employé·e du Service Patrimoine")),
|
||||
("employe-sps", _("Employé·e du Service Prévention et Sécurité")),
|
||||
("employe-sl", _("Employé·e du Service Logistique")),
|
||||
("employe-sr", _("Employé·e du Service de la Restauration")),
|
||||
("employe-ps", _("Employé·e du Pôle Santé")),
|
||||
("employe-spi", _("Employé·e du Service de Prestations Informatiques")),
|
||||
("employe-bibliotheque", _("Employé·e d'une des bibliothèques")),
|
||||
("employe-exterieur", _("Employé·e d'une société prestataire de service à l'ENS")),
|
||||
("pei", _("Élève du PEI")),
|
||||
]
|
||||
|
||||
ELECTED = [
|
||||
("", _("Aucun")),
|
||||
("dg", _("Membre de la Délégation Générale")),
|
||||
("cof", _("Membre du bureau du COF")),
|
||||
("bda", _("Membre du bureau du BdA")),
|
||||
("bds", _("Membre du bureau du BDS")),
|
||||
("cs", _("Membre du Conseil Scientifique")),
|
||||
("ca", _("Membre du Conseil d'Administration")),
|
||||
("ce", _("Membre de la Commission des Études")),
|
||||
("chsct", _("Membre du CHSCT")),
|
||||
]
|
||||
|
||||
MAIL_SIGNATURE_DELETED = (
|
||||
"Bonjour {full_name},\n"
|
||||
"\n"
|
||||
"Votre signature pour la pétition « {petition_name_fr} » a été supprimée.\n"
|
||||
"\n"
|
||||
"----------"
|
||||
"\n"
|
||||
"Dear {full_name},\n"
|
||||
"\n"
|
||||
"Your signature for the petition “{petition_name_en}” has been removed.\n"
|
||||
"\n"
|
||||
"-- \n"
|
||||
"Kadenios"
|
||||
)
|
||||
|
||||
MAIL_SIGNATURE_VERIFICATION = (
|
||||
"Bonjour {full_name},\n"
|
||||
"\n"
|
||||
"Merci d'avoir signé la pétition « {petition_name_fr} », pour confirmer votre "
|
||||
"signature, merci de cliquer sur le lien suivant :\n"
|
||||
"\n"
|
||||
"{confirm_url}\n"
|
||||
"\n"
|
||||
"----------"
|
||||
"\n"
|
||||
"Dear {full_name},\n"
|
||||
"\n"
|
||||
"Thank you for signing the petition “{petition_name_en}”, to confirm your "
|
||||
"signature, please click on the following link:\n"
|
||||
"\n"
|
||||
"{confirm_url}\n"
|
||||
"\n"
|
||||
"-- \n"
|
||||
"Kadenios"
|
||||
)
|
13
petitions/templates/petitions/delete_signature.html
Normal file
13
petitions/templates/petitions/delete_signature.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n string %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="title">{% trans "Supprimer une signature" %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'election.voters' election.pk as r_url %}
|
||||
{% include "forms/common-form.html" with c_size="is-half" r_anchor="v_"|concatenate:anchor %}
|
||||
|
||||
{% endblock %}
|
132
petitions/templates/petitions/petition.html
Normal file
132
petitions/templates/petitions/petition.html
Normal file
|
@ -0,0 +1,132 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level">
|
||||
{# Titre de la pétition #}
|
||||
<div class="level-left is-flex-shrink-1">
|
||||
<h1 class="title">{{ petition.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
{# Lien vers la page d'administration #}
|
||||
{% if petition.created_by == user %}
|
||||
<div class="level-item">
|
||||
<a class="button has-tooltip-primary" href="{% url 'petition.admin' petition.pk %}" data-tooltip="{% trans "Administrer" %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level">
|
||||
{# Date d'ouverture de la pétition #}
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<span class="tag is-medium is-primary">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fas fa-calendar-week"></i>
|
||||
</span>
|
||||
<span>{{ petition.launch_date|date:"d/m/Y" }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Créateurice de la pétition #}
|
||||
<div class="level-item">
|
||||
<span class="tag is-primary is-light is-outlined">{% blocktrans with creator=petition.created_by.full_name %}Créé par {{ creator }}{% endblocktrans %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{# Signature #}
|
||||
{% if petition.launch_date <= today and not petition.archived %}
|
||||
<div class="columns is-centered tile is-ancestor">
|
||||
<div class="column is-6 tile is-parent">
|
||||
<a class="tile is-child notification is-primary" href="{% url 'petition.sign' petition.pk %}">
|
||||
<div class="subtitle has-text-centered">
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-white">
|
||||
<i class="fas fa-signature"></i>
|
||||
</span>
|
||||
<span class="ml-3">{% trans "Signer cette pétition" %}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Description de la pétition #}
|
||||
{% if petition.text %}
|
||||
<div class="message is-primary">
|
||||
<div class="message-header">{% trans "Texte de la pétition" %}</div>
|
||||
<div class="message-body">{{ petition.text|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Lettre aux signataires #}
|
||||
{% if petition.letter %}
|
||||
<div class="message is-primary">
|
||||
<div class="message-header">{% trans "Lettre de la pétition" %}</div>
|
||||
<div class="message-body">{{ petition.letter|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Liste des signataires #}
|
||||
<br>
|
||||
<h3 class="subtitle">{% trans "Liste des signataires" %}</h3>
|
||||
<hr>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Nom" %}</th>
|
||||
<th>{% trans "Statut" %}</th>
|
||||
<th>{% trans "Département" %}</th>
|
||||
<th>{% trans "Poste élu" %}</th>
|
||||
<th class="has-text-centered">{% trans "Vérifié" %}</th>
|
||||
<th class="has-text-centered">{% trans "Validé" %}</th>
|
||||
<tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for s in signatures %}
|
||||
<tr>
|
||||
<td>{{ s.full_name }}</td>
|
||||
<td>{{ s.get_status_display }}</td>
|
||||
<td>{{ s.get_department_display }}</td>
|
||||
<td>{{ s.get_elected_display }}</td>
|
||||
<td class="has-text-centered">
|
||||
<span class="icon">
|
||||
{% if s.verified %}
|
||||
<i class="has-text-success fas fa-check"></i>
|
||||
{% else %}
|
||||
<i class="has-text-danger fas fa-times"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="has-text-centered">
|
||||
<span class="icon">
|
||||
{% if s.valid %}
|
||||
<i class="has-text-success fas fa-check"></i>
|
||||
{% else %}
|
||||
<i class="has-text-danger fas fa-times"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
216
petitions/templates/petitions/petition_admin.html
Normal file
216
petitions/templates/petitions/petition_admin.html
Normal file
|
@ -0,0 +1,216 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $del_modal = document.getElementById('modal-delete');
|
||||
const $del_title = $del_modal.querySelector('.modal-card-title');
|
||||
const $del_form = $del_modal.querySelector('form');
|
||||
|
||||
const $val_modal = document.getElementById('modal-validate');
|
||||
const $val_title = $val_modal.querySelector('.modal-card-title');
|
||||
const $val_form = $val_modal.querySelector('form');
|
||||
|
||||
$del_buttons = document.querySelectorAll('.modal-button.delete-signature')
|
||||
$val_buttons = document.querySelectorAll('.modal-button.validate-signature')
|
||||
|
||||
$del_buttons.forEach($del => {
|
||||
$del.addEventListener('click', () => {
|
||||
$del_form.action = $del.dataset.post_url;
|
||||
$del_title.innerHTML = $del.dataset.tooltip;
|
||||
});
|
||||
});
|
||||
|
||||
$val_buttons.forEach($val => {
|
||||
$val.addEventListener('click', () => {
|
||||
$val_form.action = $val.dataset.post_url;
|
||||
$val_title.innerHTML = $val.dataset.tooltip;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level is-block-tablet is-block-desktop is-flex-fullhd">
|
||||
{# Titre de la pétition #}
|
||||
<div class="level-left is-flex-shrink-1">
|
||||
<h1 class="title">{{ petition.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<div class="dropdown is-right">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{% trans "Actions" %}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
{# Vue classique #}
|
||||
<a class="dropdown-item" href="{% url 'petition.view' petition.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</span>
|
||||
<span>{% trans "Vue classique" %}
|
||||
</a>
|
||||
|
||||
{# Téléchargement de la liste des signataires #}
|
||||
<a class="dropdown-item" href="{% url 'election.export-voters' petition.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-download"></i>
|
||||
</span>
|
||||
<span>{% trans "Exporter les signataires" %}
|
||||
</a>
|
||||
|
||||
{# Modification de la pétition #}
|
||||
{% if petition.launch_date > today %}
|
||||
<a class="dropdown-item" href="{% url 'petition.update' petition.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
<span>{% trans "Modifier" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# Archivage #}
|
||||
{% if not petition.archived %}
|
||||
<a class="dropdown-item" href="{% url 'petition.archive' petition.pk %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-archive"></i>
|
||||
</span>
|
||||
<span>{% trans "Archiver" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level">
|
||||
{# Dates d'ouverture de la pétition #}
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<span class="tag is-medium is-primary">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fas fa-calendar-week"></i>
|
||||
</span>
|
||||
<span>{{ petition.launch_date|date:"d/m/Y" }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{# Description de la pétition #}
|
||||
{% if petition.text %}
|
||||
<div class="message is-primary">
|
||||
<div class="message-header">{% trans "Texte de la pétition" %}</div>
|
||||
<div class="message-body">{{ petition.text|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Lettre aux signataires #}
|
||||
{% if petition.letter %}
|
||||
<div class="message is-primary">
|
||||
<div class="message-header">{% trans "Lettre de la pétition" %}</div>
|
||||
<div class="message-body">{{ petition.letter|linebreaksbr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Liste des signataires #}
|
||||
<br>
|
||||
<h3 class="subtitle">{% trans "Liste des signataires" %}</h3>
|
||||
<hr>
|
||||
|
||||
{% include "forms/modal-form.html" with modal_id="delete" form=d_form %}
|
||||
{% include "forms/modal-form.html" with modal_id="validate" form=v_form %}
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Nom" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Statut" %}</th>
|
||||
<th>{% trans "Département" %}</th>
|
||||
<th>{% trans "Poste élu" %}</th>
|
||||
<th class="has-text-centered">{% trans "Vérifié" %}</th>
|
||||
<th class="has-text-centered">{% trans "Valide" %}</th>
|
||||
{% if not petition.archived %}
|
||||
<th class="has-text-centered">{% trans "Action" %}</th>
|
||||
{% endif %}
|
||||
<tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for s in petition.signatures.all %}
|
||||
<tr id="s_{{ forloop.counter }}">
|
||||
<td>{{ s.full_name }}</td>
|
||||
<td>{{ s.email }}</td>
|
||||
<td>{{ s.get_status_display }}</td>
|
||||
<td>{{ s.get_department_display }}</td>
|
||||
<td>{{ s.get_elected_display }}</td>
|
||||
<td class="has-text-centered">
|
||||
<span class="icon">
|
||||
{% if s.verified %}
|
||||
<i class="has-text-success fas fa-check"></i>
|
||||
{% else %}
|
||||
<i class="has-text-danger fas fa-times"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="has-text-centered">
|
||||
<span class="icon">
|
||||
{% if s.valid %}
|
||||
<i class="has-text-success fas fa-check"></i>
|
||||
{% else %}
|
||||
<i class="has-text-danger fas fa-times"></i>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
{% if not petition.archived %}
|
||||
<td>
|
||||
<span class="tags is-centered">
|
||||
{% if not s.valid %}
|
||||
{% blocktrans with s_name=s.full_name asvar s_validate %}Valider la signature de {{ s_name }}{% endblocktrans %}
|
||||
<a class="tag is-success has-tooltip-primary has-tooltip-left modal-button validate-signature" data-target="modal-validate" data-post_url="{% url 'petition.validate' petition.pk s.pk forloop.counter %}" data-tooltip="{{ s_validate }}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% blocktrans with s_name=s.full_name asvar s_delete %}Supprimer la signature de {{ s_name }}{% endblocktrans %}
|
||||
<a class="tag is-danger has-tooltip-primary has-tooltip-left modal-button delete-signature" data-target="modal-delete" data-post_url="{% url 'petition.delete-signature' petition.pk s.pk forloop.counter %}" data-tooltip="{{ s_delete }}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
35
petitions/templates/petitions/petition_create.html
Normal file
35
petitions/templates/petitions/petition_create.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
{# DateTimePicker #}
|
||||
<script src={% static 'vendor/datepicker/datetimepicker.js' %}></script>
|
||||
<link rel="stylesheet" href="{% static 'vendor/datepicker/datetimepicker.css' %}">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$('#id_launch_date').datetimepicker({
|
||||
format: 'Y-m-d',
|
||||
timepicker: false,
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h1 class="title">{% trans "Création d'une pétition" %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'election.list' as r_url %}
|
||||
{% include "forms/common-form.html" with c_size="is-12" errors=False %}
|
||||
|
||||
{% endblock %}
|
69
petitions/templates/petitions/petition_list.html
Normal file
69
petitions/templates/petitions/petition_list.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h1 class="title">{% trans "Liste des pétitions" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if perms.petitions.is_admin %}
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href={% url 'petition.create' %}>
|
||||
<span class="icon">
|
||||
<i class="fas fa-plus"></i>
|
||||
</span>
|
||||
<span>{% trans "Créer une pétition" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% for p in petition_list %}
|
||||
<div class="panel is-primary">
|
||||
<div class="panel-heading is-size-6 is-radiusles">
|
||||
<div class="level">
|
||||
<div class="level-left is-flex-shrink-1">
|
||||
<div class="level-item">
|
||||
<span class="tag is-primary is-light">{{ p.launch_date|date:"d/m/Y" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="level-item is-flex-shrink-1">
|
||||
<a class="has-text-primary-light" href="{% url 'petition.view' p.pk %}"><u>{{ p.title }}</u></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
{% if p.archived %}
|
||||
<div class="level-item">
|
||||
<span class="tag is-danger is-light">{% trans "Pétition archivée" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if p.created_by == user %}
|
||||
<div class="level-item">
|
||||
<a class="has-text-primary-light ml-3 has-tooltip-light" href="{% url 'petition.admin' p.pk %}" data-tooltip="{% trans "Administrer" %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="panel-block">
|
||||
{{ p.text|linebreaksbr }}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
19
petitions/templates/petitions/petition_sign.html
Normal file
19
petitions/templates/petitions/petition_sign.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h1 class="title">{% blocktrans with p_title=petition.title %}Signature de la pétition {{ p_title }}{% endblocktrans %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'petition.view' petition.pk as r_url %}
|
||||
{% include "forms/common-form.html" with errors=False %}
|
||||
|
||||
{% endblock %}
|
34
petitions/templates/petitions/petition_update.html
Normal file
34
petitions/templates/petitions/petition_update.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
{# DateTimePicker #}
|
||||
<script src={% static 'vendor/datepicker/datetimepicker.js' %}></script>
|
||||
<link rel="stylesheet" href="{% static 'vendor/datepicker/datetimepicker.css' %}">
|
||||
|
||||
<script>
|
||||
$(document).ready(function($) {
|
||||
$('#id_launch_date').datetimepicker({
|
||||
format: 'Y-m-d'
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h1 class="title">{% trans "Modification d'une pétition" %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'petition.admin' petition.pk as r_url %}
|
||||
{% include "forms/common-form.html" with errors=False %}
|
||||
|
||||
{% endblock %}
|
33
petitions/urls.py
Normal file
33
petitions/urls.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# Admin views
|
||||
path("create", views.PetitionCreateView.as_view(), name="petition.create"),
|
||||
path("admin/<int:pk>", views.PetitionAdminView.as_view(), name="petition.admin"),
|
||||
path("update/<int:pk>", views.PetitionUpdateView.as_view(), name="petition.update"),
|
||||
path(
|
||||
"archive/<int:pk>", views.PetitionArchiveView.as_view(), name="petition.archive"
|
||||
),
|
||||
path(
|
||||
"delete/<int:pk>/<int:signature_pk>/<int:anchor>",
|
||||
views.DeleteSignatureView.as_view(),
|
||||
name="petition.delete-signature",
|
||||
),
|
||||
path(
|
||||
"validate/<int:pk>/<int:signature_pk>/<int:anchor>",
|
||||
views.ValidateSignatureView.as_view(),
|
||||
name="petition.validate",
|
||||
),
|
||||
# Verification views
|
||||
path(
|
||||
"email/<slug:token>",
|
||||
views.EmailValidationView.as_view(),
|
||||
name="petition.confirm-email",
|
||||
),
|
||||
# Public views
|
||||
path("", views.PetitionListView.as_view(), name="petition.list"),
|
||||
path("view/<int:pk>", views.PetitionView.as_view(), name="petition.view"),
|
||||
path("sign/<int:pk>", views.PetitionSignView.as_view(), name="petition.sign"),
|
||||
]
|
264
petitions/views.py
Normal file
264
petitions/views.py
Normal file
|
@ -0,0 +1,264 @@
|
|||
from datetime import date
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.mail import EmailMessage
|
||||
from django.http import Http404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, FormView, ListView, UpdateView
|
||||
from django.views.generic.edit import SingleObjectMixin
|
||||
|
||||
from shared.utils import full_url
|
||||
from shared.views import BackgroundUpdateView
|
||||
|
||||
from .forms import DeleteForm, PetitionForm, SignatureForm, ValidateForm
|
||||
from .mixins import AdminOnlyMixin, CreatorOnlyMixin
|
||||
from .models import Petition, Signature
|
||||
from .staticdefs import MAIL_SIGNATURE_DELETED, MAIL_SIGNATURE_VERIFICATION
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# #############################################################################
|
||||
# Administration views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class PetitionCreateView(AdminOnlyMixin, CreateView):
|
||||
model = Petition
|
||||
form_class = PetitionForm
|
||||
success_message = _("Pétition créée avec succès")
|
||||
template_name = "petitions/petition_create.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("petition.admin", args=[self.object.pk])
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class PetitionAdminView(CreatorOnlyMixin, DetailView):
|
||||
model = Petition
|
||||
template_name = "petitions/petition_admin.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["d_form"] = DeleteForm()
|
||||
context["v_form"] = ValidateForm()
|
||||
context["today"] = date.today()
|
||||
return context
|
||||
|
||||
|
||||
class PetitionUpdateView(CreatorOnlyMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Petition
|
||||
form_class = PetitionForm
|
||||
success_message = _("Pétition modifiée avec succès !")
|
||||
template_name = "petitions/petition_update.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("election.admin", args=[self.object.pk])
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super().get_queryset().filter(launch_date__gt=date.today(), archived=False)
|
||||
)
|
||||
|
||||
|
||||
class PetitionArchiveView(CreatorOnlyMixin, SingleObjectMixin, BackgroundUpdateView):
|
||||
model = Petition
|
||||
pattern_name = "petition.admin"
|
||||
success_message = _("Élection archivée avec succès !")
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(archived=False)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
petition = self.get_object()
|
||||
petition.archived = True
|
||||
petition.save()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DeleteSignatureView(CreatorOnlyMixin, SingleObjectMixin, FormView):
|
||||
model = Petition
|
||||
template_name = "petitions/delete_signature.html"
|
||||
form_class = DeleteForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("petition.admin", args=[self.object.pk]) + "#s_{anchor}".format(
|
||||
**self.kwargs
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["signature"] = self.signature
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["anchor"] = self.kwargs["anchor"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(archived=False)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = super().get_object()
|
||||
self.signature = self.object.signatures.get(pk=self.kwargs["signature_pk"])
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = super().get_object()
|
||||
self.signature = self.object.signatures.get(pk=self.kwargs["signature_pk"])
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data["delete"] == "oui":
|
||||
# On envoie un mail à la personne lui indiquant que la signature est supprimée
|
||||
# si l'adresse mail est validée
|
||||
if self.signature.verified:
|
||||
EmailMessage(
|
||||
subject="Signature removed",
|
||||
body=MAIL_SIGNATURE_DELETED.format(
|
||||
full_name=self.signature.full_name,
|
||||
petition_name_fr=self.object.title_fr,
|
||||
petition_name_en=self.object.title_en,
|
||||
),
|
||||
to=[self.signature.email],
|
||||
).send()
|
||||
|
||||
# On supprime la signature
|
||||
self.signature.delete()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ValidateSignatureView(CreatorOnlyMixin, SingleObjectMixin, FormView):
|
||||
model = Petition
|
||||
template_name = "petitions/validate_signature.html"
|
||||
form_class = ValidateForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("petition.admin", args=[self.object.pk]) + "#s_{anchor}".format(
|
||||
**self.kwargs
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["signature"] = self.signature
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["anchor"] = self.kwargs["anchor"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(archived=False)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = super().get_object()
|
||||
self.signature = self.object.signatures.get(pk=self.kwargs["signature_pk"])
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = super().get_object()
|
||||
self.signature = self.object.signatures.get(pk=self.kwargs["signature_pk"])
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data["validate"] == "oui":
|
||||
# On valide la signature
|
||||
self.signature.valid = True
|
||||
self.signature.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Email validation Views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class EmailValidationView(SingleObjectMixin, BackgroundUpdateView):
|
||||
model = Signature
|
||||
slug_url_kwarg = "token"
|
||||
slug_field = "token"
|
||||
success_message = _("Adresse email vérifiée avec succès")
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(verified=False)
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse("petition.view", args=[self.object.petition.pk])
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.object.verified = True
|
||||
self.object.save()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Public Views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class PetitionListView(ListView):
|
||||
model = Petition
|
||||
template_name = "petitions/petition_list.html"
|
||||
|
||||
|
||||
class PetitionView(DetailView):
|
||||
model = Petition
|
||||
template_name = "petitions/petition.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["today"] = date.today()
|
||||
context["signatures"] = self.object.signatures.filter(verified=True)
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related("created_by")
|
||||
|
||||
|
||||
class PetitionSignView(CreateView):
|
||||
model = Signature
|
||||
form_class = SignatureForm
|
||||
template_name = "petitions/petition_sign.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("petition.view", args=[self.petition.pk])
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.petition = Petition.objects.get(pk=self.kwargs["pk"])
|
||||
if self.petition.launch_date > date.today():
|
||||
raise Http404
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["petition"] = self.petition
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.instance.petition = self.petition
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
# On envoie un mail à l'adresse indiquée
|
||||
EmailMessage(
|
||||
subject="Confirmation de la signature",
|
||||
body=MAIL_SIGNATURE_VERIFICATION.format(
|
||||
full_name=self.object.full_name,
|
||||
petition_name_fr=self.object.petition.title_fr,
|
||||
petition_name_en=self.object.petition.title_en,
|
||||
confirm_url=full_url("petition.confirm-email", self.object.token),
|
||||
),
|
||||
to=[self.object.email],
|
||||
).send()
|
||||
return super().form_valid(form)
|
|
@ -114,16 +114,22 @@
|
|||
<nav class="level has-background-primary">
|
||||
<div class="level-left px-4">
|
||||
<div class="level-item">
|
||||
<a href="{% url "kadenios" %}">
|
||||
<a href="{% url 'kadenios' %}">
|
||||
<h1 class="has-text-primary-light is-size-1 is-family-secondary">Kadenios</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="level-item px-4">
|
||||
<a href="{% url "election.list" %}">
|
||||
<div class="level-item px-5">
|
||||
<a href="{% url 'election.list' %}">
|
||||
<h3 class="has-text-primary-light has-text-weight-semibold is-size-3">{% trans "Élections" %}</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="level-item px-5">
|
||||
<a href="{% url 'petition.list' %}">
|
||||
<h3 class="has-text-primary-light has-text-weight-semibold is-size-3">{% trans "Pétitions" %}</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right px-5">
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
from secrets import token_urlsafe
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
# #############################################################################
|
||||
# Fonctions universelles
|
||||
# #############################################################################
|
||||
|
@ -9,3 +14,13 @@ def choices_length(choices):
|
|||
for c in choices:
|
||||
m = max(m, len(c[0]))
|
||||
return m
|
||||
|
||||
|
||||
def token_generator():
|
||||
"""Renvoie un token aléatoire pouvant être utilisé dans une url"""
|
||||
return token_urlsafe(36)
|
||||
|
||||
|
||||
def full_url(name, *args, **kwargs):
|
||||
url = reverse(name, args=args, kwargs=kwargs)
|
||||
return f"https://{settings.SERVER_DOMAIN}{url}"
|
||||
|
|
Loading…
Reference in a new issue