Permet d'envoyer un mail à tous les votant·e·s avec leurs identifiants
This commit is contained in:
parent
b44f150cf9
commit
bab2629236
11 changed files with 202 additions and 14 deletions
|
@ -42,6 +42,11 @@ class UploadVotersForm(forms.Form):
|
||||||
return csv_file
|
return csv_file
|
||||||
|
|
||||||
|
|
||||||
|
class VoterMailForm(forms.Form):
|
||||||
|
objet = forms.CharField()
|
||||||
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
|
|
||||||
|
|
||||||
class QuestionForm(forms.ModelForm):
|
class QuestionForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
|
|
20
elections/migrations/0006_election_sent_mail.py
Normal file
20
elections/migrations/0006_election_sent_mail.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.17 on 2020-12-23 19:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("elections", "0005_user_full_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="election",
|
||||||
|
name="sent_mail",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="mail avec les identifiants envoyé"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,6 +20,10 @@ class Election(models.Model):
|
||||||
_("restreint le vote à une liste de personnes"), default=True
|
_("restreint le vote à une liste de personnes"), default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sent_mail = models.BooleanField(
|
||||||
|
_("mail avec les identifiants envoyé"), default=False
|
||||||
|
)
|
||||||
|
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
related_name="elections_created",
|
related_name="elections_created",
|
||||||
|
|
12
elections/staticdefs.py
Normal file
12
elections/staticdefs.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
MAIL_VOTERS = (
|
||||||
|
"Dear {full_name},\n"
|
||||||
|
"\n"
|
||||||
|
"\n"
|
||||||
|
"Election URL: {election_url}\n"
|
||||||
|
"\n"
|
||||||
|
"Your voter ID: {username}\n"
|
||||||
|
"Your password: {password}\n"
|
||||||
|
"\n"
|
||||||
|
"-- \n"
|
||||||
|
"Kadenios"
|
||||||
|
)
|
|
@ -40,7 +40,7 @@
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-file-import"></i>
|
<i class="fas fa-file-import"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{% trans "Importer une liste de votant·e·s" %}</span>
|
<span>{% trans "Gestion de la liste de votant·e·s" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
51
elections/templates/elections/mail_voters.html
Normal file
51
elections/templates/elections/mail_voters.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1 class="title">{% trans "Composition du mail aux votant·e·s" %}</h1>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="message is-warning">
|
||||||
|
<div class="message-body">
|
||||||
|
{% trans "Assurez-vous que la liste des votant·e·s est complète, en effet, une fois le mail envoyé, il ne sera plus possible de la modifier." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message is-primary">
|
||||||
|
<div class="message-body">
|
||||||
|
{% trans "Rajoutez le message à envoyer aux électeurs entre 'Dear {full_name}' et l'url de l'élection." %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
|
<form action="" method="post" enctype="multipart/form-data" id="import-voters">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% include "forms/form.html" %}
|
||||||
|
|
||||||
|
<div class="field is-grouped is-centered">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<button class="button is-fullwidth is-outlined is-primary is-light">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Envoyer" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-primary" href="{% url 'election.upload-voters' election.pk %}">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fas fa-undo-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Retour" %}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -19,9 +19,39 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1 class="title">{% trans "Importer une liste de votant·e·s" %}</h1>
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="item-level">
|
||||||
|
<h1 class="title">{% trans "Gestion de la liste de votant·e·s" %}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="level-right">
|
||||||
|
{% if not election.sent_mail %}
|
||||||
|
<div class="level-item">
|
||||||
|
<a class="button is-light is-outlined is-primary" href="{% url 'election.mail-voters' election.pk %}">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-envelope-open"></i>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Envoyer le mail d'annonce" %}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="level-item">
|
||||||
|
<a class="button is-primary" href="{% url 'election.admin' election.pk %}">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fas fa-undo-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span>{% trans "Retour" %}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
{# Si on a déjà envoyé le mail avec les identifiants, on ne peut plus changer la liste #}
|
||||||
|
{% if not election.sent_mail %}
|
||||||
<div class="message is-warning">
|
<div class="message is-warning">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
{% trans "Importez un fichier au format CSV, avec sur la première colonne le login, sur la deuxième, le nom et prénom et enfin l'adresse email sur la troisième. Soit :<br><br><pre>Login_1,Prénom/Nom_1,mail_1@machin.test<br>Login_2,Prénom/Nom_2,mail_2@bidule.test<br>...</pre>" %}
|
{% trans "Importez un fichier au format CSV, avec sur la première colonne le login, sur la deuxième, le nom et prénom et enfin l'adresse email sur la troisième. Soit :<br><br><pre>Login_1,Prénom/Nom_1,mail_1@machin.test<br>Login_2,Prénom/Nom_2,mail_2@bidule.test<br>...</pre>" %}
|
||||||
|
@ -50,23 +80,16 @@
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{% trans "Enregistrer" %}</span>
|
<span>{% trans "Importer" %}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control">
|
|
||||||
<a class="button is-primary" href="{% url 'election.admin' election.pk %}">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<i class="fas fa-undo-alt"></i>
|
|
||||||
</span>
|
|
||||||
<span>{% trans "Retour" %}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Liste des votant·e·s #}
|
{# Liste des votant·e·s #}
|
||||||
{% if voters %}
|
{% if voters %}
|
||||||
|
|
|
@ -7,6 +7,11 @@ urlpatterns = [
|
||||||
path("create/", views.ElectionCreateView.as_view(), name="election.create"),
|
path("create/", views.ElectionCreateView.as_view(), name="election.create"),
|
||||||
path("created/", views.ElectionListView.as_view(), name="election.created"),
|
path("created/", views.ElectionListView.as_view(), name="election.created"),
|
||||||
path("admin/<int:pk>", views.ElectionAdminView.as_view(), name="election.admin"),
|
path("admin/<int:pk>", views.ElectionAdminView.as_view(), name="election.admin"),
|
||||||
|
path(
|
||||||
|
"mail-voters/<int:pk>",
|
||||||
|
views.ElectionMailVotersView.as_view(),
|
||||||
|
name="election.mail-voters",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"upload-voters/<int:pk>",
|
"upload-voters/<int:pk>",
|
||||||
views.ElectionUploadVotersView.as_view(),
|
views.ElectionUploadVotersView.as_view(),
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.mail import EmailMessage, get_connection
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
def create_users(election, csv_file):
|
def create_users(election, csv_file):
|
||||||
"""Crée les votant·e·s pour l'élection donnée, en remplissant les champs
|
"""Crée les votant·e·s pour l'élection donnée, en remplissant les champs
|
||||||
|
@ -72,8 +77,37 @@ def check_csv(csv_file):
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def send_mail(election):
|
def generate_password():
|
||||||
|
random.seed()
|
||||||
|
alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
password = ""
|
||||||
|
for i in range(15):
|
||||||
|
password += random.choice(alphabet)
|
||||||
|
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(election, mail_form):
|
||||||
"""Envoie le mail d'annonce de l'élection avec identifiants et mot de passe
|
"""Envoie le mail d'annonce de l'élection avec identifiants et mot de passe
|
||||||
aux votant·e·s, le mdp est généré en même temps que le mail est envoyé.
|
aux votant·e·s, le mdp est généré en même temps que le mail est envoyé.
|
||||||
"""
|
"""
|
||||||
pass
|
voters = list(election.registered_voters.all())
|
||||||
|
url = f"https://kadenios.eleves.ens.fr/elections/view/{election.id}"
|
||||||
|
messages = []
|
||||||
|
for v in voters:
|
||||||
|
password = generate_password()
|
||||||
|
v.password = make_password(password)
|
||||||
|
messages.append(
|
||||||
|
EmailMessage(
|
||||||
|
subject=mail_form.cleaned_data["objet"],
|
||||||
|
body=mail_form.cleaned_data["message"].format(
|
||||||
|
full_name=v.full_name,
|
||||||
|
election_url=url,
|
||||||
|
username=v.base_username,
|
||||||
|
password=password,
|
||||||
|
),
|
||||||
|
to=[v.email],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
get_connection(fail_silently=False).send_messages(messages)
|
||||||
|
User.objects.bulk_update(voters, ["password"])
|
||||||
|
|
|
@ -22,10 +22,12 @@ from .forms import (
|
||||||
OptionFormSet,
|
OptionFormSet,
|
||||||
QuestionForm,
|
QuestionForm,
|
||||||
UploadVotersForm,
|
UploadVotersForm,
|
||||||
|
VoterMailForm,
|
||||||
)
|
)
|
||||||
from .mixins import CreatorOnlyEditMixin, CreatorOnlyMixin, OpenElectionOnlyMixin
|
from .mixins import CreatorOnlyEditMixin, CreatorOnlyMixin, OpenElectionOnlyMixin
|
||||||
from .models import Election, Option, Question
|
from .models import Election, Option, Question
|
||||||
from .utils import create_users
|
from .staticdefs import MAIL_VOTERS
|
||||||
|
from .utils import create_users, send_mail
|
||||||
|
|
||||||
# TODO: access control *everywhere*
|
# TODO: access control *everywhere*
|
||||||
|
|
||||||
|
@ -116,6 +118,33 @@ class ElectionUploadVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormVi
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ElectionMailVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormView):
|
||||||
|
model = Election
|
||||||
|
form_class = VoterMailForm
|
||||||
|
success_message = _("Mail d'annonce envoyé avec succès !")
|
||||||
|
template_name = "elections/mail_voters.html"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("election.upload-voters", args=[self.object.pk])
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {"objet": f"Vote : {self.object.name}", "message": MAIL_VOTERS}
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object.sent_mail = True
|
||||||
|
send_mail(self.object, form)
|
||||||
|
self.object.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class ElectionListView(CreatorOnlyMixin, ListView):
|
class ElectionListView(CreatorOnlyMixin, ListView):
|
||||||
model = Election
|
model = Election
|
||||||
template_name = "elections/election_list.html"
|
template_name = "elections/election_list.html"
|
||||||
|
|
|
@ -109,6 +109,11 @@ USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Mail configuration
|
||||||
|
|
||||||
|
DEFAULT_FROM_EMAIL = "Kadenios <klub-dev@ens.fr>"
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue