diff --git a/kadenios/settings/common.py b/kadenios/settings/common.py index 9b2e001..4564409 100644 --- a/kadenios/settings/common.py +++ b/kadenios/settings/common.py @@ -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 " +SERVER_DOMAIN = "vote.eleves.ens.fr" + # ############################################################################# # Paramètres d'authentification # ############################################################################# diff --git a/kadenios/urls.py b/kadenios/urls.py index 4c81a39..0441d4c 100644 --- a/kadenios/urls.py +++ b/kadenios/urls.py @@ -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")), diff --git a/petitions/__init__.py b/petitions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petitions/apps.py b/petitions/apps.py new file mode 100644 index 0000000..7e2f72d --- /dev/null +++ b/petitions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PetitionsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "petitions" diff --git a/petitions/forms.py b/petitions/forms.py new file mode 100644 index 0000000..414095b --- /dev/null +++ b/petitions/forms.py @@ -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"] diff --git a/petitions/migrations/0001_initial.py b/petitions/migrations/0001_initial.py new file mode 100644 index 0000000..220980e --- /dev/null +++ b/petitions/migrations/0001_initial.py @@ -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" + ), + ), + ] diff --git a/petitions/migrations/0002_alter_petition_options.py b/petitions/migrations/0002_alter_petition_options.py new file mode 100644 index 0000000..df40874 --- /dev/null +++ b/petitions/migrations/0002_alter_petition_options.py @@ -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")], + }, + ), + ] diff --git a/petitions/migrations/0003_auto_20210530_1740.py b/petitions/migrations/0003_auto_20210530_1740.py new file mode 100644 index 0000000..8058a15 --- /dev/null +++ b/petitions/migrations/0003_auto_20210530_1740.py @@ -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"), + ), + ] diff --git a/petitions/migrations/0004_signature_token.py b/petitions/migrations/0004_signature_token.py new file mode 100644 index 0000000..50ec15b --- /dev/null +++ b/petitions/migrations/0004_signature_token.py @@ -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", + ), + ), + ] diff --git a/petitions/migrations/__init__.py b/petitions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petitions/mixins.py b/petitions/mixins.py new file mode 100644 index 0000000..8a982c6 --- /dev/null +++ b/petitions/mixins.py @@ -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) diff --git a/petitions/models.py b/petitions/models.py new file mode 100644 index 0000000..3217490 --- /dev/null +++ b/petitions/models.py @@ -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" + ) + ] diff --git a/petitions/staticdefs.py b/petitions/staticdefs.py new file mode 100644 index 0000000..4548bcf --- /dev/null +++ b/petitions/staticdefs.py @@ -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" +) diff --git a/petitions/templates/petitions/delete_signature.html b/petitions/templates/petitions/delete_signature.html new file mode 100644 index 0000000..ab5b8b8 --- /dev/null +++ b/petitions/templates/petitions/delete_signature.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load i18n string %} + + +{% block content %} + +

{% trans "Supprimer une signature" %}

+
+ +{% url 'election.voters' election.pk as r_url %} +{% include "forms/common-form.html" with c_size="is-half" r_anchor="v_"|concatenate:anchor %} + +{% endblock %} diff --git a/petitions/templates/petitions/petition.html b/petitions/templates/petitions/petition.html new file mode 100644 index 0000000..5ce416f --- /dev/null +++ b/petitions/templates/petitions/petition.html @@ -0,0 +1,132 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block content %} + +
+ {# Titre de la pétition #} +
+

{{ petition.title }}

+
+ +
+ {# Lien vers la page d'administration #} + {% if petition.created_by == user %} + + {% endif %} +
+
+ +
+ {# Date d'ouverture de la pétition #} +
+
+ + + + + + {{ petition.launch_date|date:"d/m/Y" }} + + +
+ + {# Créateurice de la pétition #} +
+ {% blocktrans with creator=petition.created_by.full_name %}Créé par {{ creator }}{% endblocktrans %} +
+
+
+
+ +{# Signature #} +{% if petition.launch_date <= today and not petition.archived %} +
+ +
+{% endif %} + +{# Description de la pétition #} +{% if petition.text %} +
+
{% trans "Texte de la pétition" %}
+
{{ petition.text|linebreaksbr }}
+
+{% endif %} + +{# Lettre aux signataires #} +{% if petition.letter %} +
+
{% trans "Lettre de la pétition" %}
+
{{ petition.letter|linebreaksbr }}
+
+{% endif %} + +{# Liste des signataires #} +
+

{% trans "Liste des signataires" %}

+
+ +
+ + + + + + + + + + + + + + {% for s in signatures %} + + + + + + + + + {% endfor %} + +
{% trans "Nom" %}{% trans "Statut" %}{% trans "Département" %}{% trans "Poste élu" %}{% trans "Vérifié" %}{% trans "Validé" %}
{{ s.full_name }}{{ s.get_status_display }}{{ s.get_department_display }}{{ s.get_elected_display }} + + {% if s.verified %} + + {% else %} + + {% endif %} + + + + {% if s.valid %} + + {% else %} + + {% endif %} + +
+
+ +{% endblock %} diff --git a/petitions/templates/petitions/petition_admin.html b/petitions/templates/petitions/petition_admin.html new file mode 100644 index 0000000..f94240d --- /dev/null +++ b/petitions/templates/petitions/petition_admin.html @@ -0,0 +1,216 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block extra_head %} + + +{% endblock %} + +{% block content %} + +
+ {# Titre de la pétition #} +
+

{{ petition.title }}

+
+ +
+
+ +
+
+
+ +
+ {# Dates d'ouverture de la pétition #} +
+
+ + + + + + {{ petition.launch_date|date:"d/m/Y" }} + + +
+
+
+
+ +{# Description de la pétition #} +{% if petition.text %} +
+
{% trans "Texte de la pétition" %}
+
{{ petition.text|linebreaksbr }}
+
+{% endif %} + +{# Lettre aux signataires #} +{% if petition.letter %} +
+
{% trans "Lettre de la pétition" %}
+
{{ petition.letter|linebreaksbr }}
+
+{% endif %} + +{# Liste des signataires #} +
+

{% trans "Liste des signataires" %}

+
+ +{% include "forms/modal-form.html" with modal_id="delete" form=d_form %} +{% include "forms/modal-form.html" with modal_id="validate" form=v_form %} + +
+ + + + + + + + + + + {% if not petition.archived %} + + {% endif %} + + + + + {% for s in petition.signatures.all %} + + + + + + + + + {% if not petition.archived %} + + {% endif %} + + {% endfor %} + +
{% trans "Nom" %}{% trans "Email" %}{% trans "Statut" %}{% trans "Département" %}{% trans "Poste élu" %}{% trans "Vérifié" %}{% trans "Valide" %}{% trans "Action" %}
{{ s.full_name }}{{ s.email }}{{ s.get_status_display }}{{ s.get_department_display }}{{ s.get_elected_display }} + + {% if s.verified %} + + {% else %} + + {% endif %} + + + + {% if s.valid %} + + {% else %} + + {% endif %} + + + + {% if not s.valid %} + {% blocktrans with s_name=s.full_name asvar s_validate %}Valider la signature de {{ s_name }}{% endblocktrans %} + + + + + + {% endif %} + + {% blocktrans with s_name=s.full_name asvar s_delete %}Supprimer la signature de {{ s_name }}{% endblocktrans %} + + + + + + +
+
+
+ +{% endblock %} diff --git a/petitions/templates/petitions/petition_create.html b/petitions/templates/petitions/petition_create.html new file mode 100644 index 0000000..cb31dbe --- /dev/null +++ b/petitions/templates/petitions/petition_create.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load i18n static %} + + +{% block extra_head %} +{# DateTimePicker #} + + + + +{% endblock %} + +{% block content %} + +{% for error in form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} + +

{% trans "Création d'une pétition" %}

+
+ +{% url 'election.list' as r_url %} +{% include "forms/common-form.html" with c_size="is-12" errors=False %} + +{% endblock %} diff --git a/petitions/templates/petitions/petition_list.html b/petitions/templates/petitions/petition_list.html new file mode 100644 index 0000000..c88675e --- /dev/null +++ b/petitions/templates/petitions/petition_list.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block content %} + +
+
+
+

{% trans "Liste des pétitions" %}

+
+
+ + {% if perms.petitions.is_admin %} + + {% endif %} +
+
+ +{% for p in petition_list %} +
+
+
+
+
+ {{ p.launch_date|date:"d/m/Y" }} +
+ + +
+ +
+ {% if p.archived %} +
+ {% trans "Pétition archivée" %} +
+ {% endif %} + + {% if p.created_by == user %} + + {% endif %} +
+
+
+ +

+ {{ p.text|linebreaksbr }} +

+
+{% endfor %} + +{% endblock %} diff --git a/petitions/templates/petitions/petition_sign.html b/petitions/templates/petitions/petition_sign.html new file mode 100644 index 0000000..730d736 --- /dev/null +++ b/petitions/templates/petitions/petition_sign.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load i18n static %} + + +{% block content %} + +{% for error in form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} + +

{% blocktrans with p_title=petition.title %}Signature de la pétition {{ p_title }}{% endblocktrans %}

+
+ +{% url 'petition.view' petition.pk as r_url %} +{% include "forms/common-form.html" with errors=False %} + +{% endblock %} diff --git a/petitions/templates/petitions/petition_update.html b/petitions/templates/petitions/petition_update.html new file mode 100644 index 0000000..11c963a --- /dev/null +++ b/petitions/templates/petitions/petition_update.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% load i18n static %} + + +{% block extra_head %} +{# DateTimePicker #} + + + + +{% endblock %} + +{% block content %} + +{% for error in form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} + +

{% trans "Modification d'une pétition" %}

+
+ +{% url 'petition.admin' petition.pk as r_url %} +{% include "forms/common-form.html" with errors=False %} + +{% endblock %} diff --git a/petitions/urls.py b/petitions/urls.py new file mode 100644 index 0000000..ac4cc35 --- /dev/null +++ b/petitions/urls.py @@ -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/", views.PetitionAdminView.as_view(), name="petition.admin"), + path("update/", views.PetitionUpdateView.as_view(), name="petition.update"), + path( + "archive/", views.PetitionArchiveView.as_view(), name="petition.archive" + ), + path( + "delete///", + views.DeleteSignatureView.as_view(), + name="petition.delete-signature", + ), + path( + "validate///", + views.ValidateSignatureView.as_view(), + name="petition.validate", + ), + # Verification views + path( + "email/", + views.EmailValidationView.as_view(), + name="petition.confirm-email", + ), + # Public views + path("", views.PetitionListView.as_view(), name="petition.list"), + path("view/", views.PetitionView.as_view(), name="petition.view"), + path("sign/", views.PetitionSignView.as_view(), name="petition.sign"), +] diff --git a/petitions/views.py b/petitions/views.py new file mode 100644 index 0000000..d10b93e --- /dev/null +++ b/petitions/views.py @@ -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) diff --git a/shared/templates/base.html b/shared/templates/base.html index 7b53dfd..08aa9a9 100644 --- a/shared/templates/base.html +++ b/shared/templates/base.html @@ -114,16 +114,22 @@