Merge pull request 'feat(votes): apporte la possibilité de répondre à un ou des votants' (#66) from fpoulain/gvot:reponses into master

Reviewed-on: #66
master
François Poulain 2023-02-27 13:24:18 +01:00
commit 4f13d89915
8 changed files with 229 additions and 5 deletions

View File

@ -192,3 +192,18 @@ class ImportForm(forms.Form):
", ".join(["« {} »".format(c) for c in header])
)
)
class AnswerForm(forms.Form):
"""
Formulaire pour répondre à un participant.
"""
reply_to = forms.EmailField(
label="De",
help_text="Une adresse email valide qui permettra "
"une éventuelle réponse de la personne.",
)
message = forms.CharField(
widget=forms.widgets.Textarea, label="Votre message"
)

View File

@ -1,6 +1,7 @@
import uuid
from itertools import groupby
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
@ -23,12 +24,14 @@ from wagtail.admin.edit_handlers import (
TabbedInterface,
)
from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
from wagtail.contrib.forms.forms import SelectDateForm
from wagtail.contrib.forms.models import (
FORM_FIELD_CHOICES,
AbstractEmailForm,
AbstractFormField,
AbstractFormSubmission,
)
from wagtail.contrib.forms.views import SubmissionsListView
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
@ -160,6 +163,41 @@ class ClosedScrutin(Exception):
pass
class SearchForm(forms.Form):
q = forms.CharField(
required=False,
max_length=255,
label="Rechercher",
)
class SearchAndSelectDateForm(SearchForm, SelectDateForm):
pass
class ScrutinSubmissionsListView(SubmissionsListView):
"""
Elle sert à ajouter la recherche dans la présentation des résultats.
"""
select_date_form = SearchAndSelectDateForm()
def get_filtering(self):
"""Return filering as a dict for submissions queryset"""
result = super().get_filtering()
self.select_date_form = SearchAndSelectDateForm(self.request.GET)
if self.select_date_form.is_valid():
result[
'form_data__icontains'
] = self.select_date_form.cleaned_data['q']
return result
def get_context_data(self, **kwargs):
"""Return context for view"""
context = super().get_context_data(**kwargs)
return context
# TODO: afficher ouverture du scrutin dans la liste des scrutins
# FIXME: revoir le workflow ouvert / fermé ; c'est une mauvaise bidouille
# FIXME: ajouter un remerciement et une intro par défaut
@ -171,6 +209,7 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
parent_page_types = ['ScrutinIndex']
subpage_types = []
submissions_list_view_class = ScrutinSubmissionsListView
ouvert = models.BooleanField(
"Scrutin ouvert",

View File

@ -5,6 +5,7 @@ from . import views
app_name = 'scrutin'
urlpatterns = [
path('add/', views.ScrutinAdd.as_view(), name='add'),
path('<int:pk>/answer/', views.ScrutinAnswer.as_view(), name='answer'),
path(
'<int:pk>/results/',
views.ScrutinResults.as_view(),

View File

@ -5,7 +5,8 @@ from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import redirect
from django.shortcuts import get_object_or_404, redirect
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils.text import slugify
from django.views.generic import FormView, RedirectView, detail
@ -13,7 +14,7 @@ from django.views.generic import FormView, RedirectView, detail
import dns.resolver
from wagtail.admin import messages
from . import forms, models
from . import emails, forms, models
class AdminMixin:
@ -46,6 +47,85 @@ class ScrutinAdd(AdminMixin, RedirectView):
return reverse('wagtailadmin_pages:add_subpage', args=(index.id,))
class ScrutinAnswer(FormView):
"""
Répond aux soumissions sélectionnées.
Inspirée de wagtail.contrib.forms.views.DeleteSubmissionsView
"""
form_class = forms.AnswerForm
template_name = "mailing/answer.html"
success_url = "wagtailforms:list_submissions"
def get_queryset(self):
"""Returns a queryset for the selected submissions"""
submission_ids = self.request.GET.getlist("selected-submissions")
submission_class = self.page.get_submission_class()
return submission_class._default_manager.filter(id__in=submission_ids)
def get_success_url(self):
return reverse(self.success_url, args=(self.page.id,))
def dispatch(self, request, *args, **kwargs):
"""Check permissions, set the page and submissions, handle delete"""
page_id = kwargs.get("pk")
self.page = get_object_or_404(models.Scrutin, id=page_id).specific
self.submissions = self.get_queryset()
self.courriels = self.submissions.values_list(
'pouvoir__courriels__courriel', flat=True
)
if self.request.method == "POST":
self.form = self.form_class(self.request.POST)
if self.form.is_valid():
self.send_mailing(self.request)
messages.success(self.request, "Mailling démarré avec succès.")
return redirect(self.get_success_url(), page_id)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""Get the context for this view"""
context = super().get_context_data(**kwargs)
context.update(
{
"courriels": self.courriels,
"page": self.page,
"submissions": self.submissions,
}
)
return context
def send_mailing(self, request):
pouvoirs = models.Pouvoir.objects.filter(vote__in=self.submissions)
template = get_template('emails/reponse.txt').template.source
template = type(
"TmpTemplate",
(object,),
{
'sujet': 'Message en réponse à votre participation',
'texte': template,
'html': None,
},
)
datas = [
(
{'pouvoir': d, 'message': self.form.cleaned_data['message']},
d['courriels'],
)
for d in self.page.pouvoir_context_values(pouvoirs)
]
emails.send_mass_templated(
request,
template,
None,
datas,
reply_to=(self.form.cleaned_data['reply_to'],),
)
class PouvoirUUIDMixin(detail.SingleObjectMixin):
model = models.Pouvoir
slug_field = 'uuid'

View File

@ -0,0 +1,26 @@
Bonjour{% if pouvoir.prenom %} {{ pouvoir.prenom }}{% endif %}{% if pouvoir.nom %} {{ pouvoir.nom }}{% endif %},
L'équipe organisatrice vous envois le message qui suit, en lien avec le scrutin
« {{ pouvoir.scrutin.title }} ».
Vous pouvez répondre si vous souhaitez, néanmoins sachez que ça révèlera
l'identité de votre vote aux organisateurs.
----8<-------8<-------8<-------8<-------8<-------8<-------8<-------8<----
{{ message }}
----8<-------8<-------8<-------8<-------8<-------8<-------8<-------8<----
Sachez aussi que vous pouvez à tout moment jusqu'à la fin du scrutin retrouver
votre contribution et éventuellement la corriger à cette adresse :
{{ request.base_url }}{{ pouvoir.uri }}
Merci pour votre participation.
L'équipe organisatrice
---
Ce courriel a été envoyé via le logiciel GvoT.
Pour plus d'informations : {{ request.base_url }}
En cas de difficultés : {{ settings.assistance }}

View File

@ -0,0 +1,19 @@
{% extends "wagtailadmin/base.html" %}
{% load wagtailadmin_tags %}
{% block titletag %}Envoyer un message{% endblock %}
{% block bodyclass %}menu-explorer{% endblock %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Envoyer un message" subtitle=page.title icon="doc-empty-inverse" %}
<div class="nice-padding">
<p>
Le message sera envoyé pour {{ submissions.count }} participation{{ submissions.count | pluralize }}, à {{ courriels.count }} destinataire{{ courriels.count | pluralize }}.
</p>
<form action="{% url 'scrutin:answer' page.id %}?{{ request.GET.urlencode }}" method="POST">
{% csrf_token %}
{{ form.as_p }}
<button class="button button-longrunning" type="submit">Envoyer</button>
</form>
</div>
{% endblock %}

View File

@ -25,6 +25,7 @@
var selectAllCheckbox = document.getElementById('select-all');
var deleteButton = document.getElementById('delete-submissions');
var answerButton = document.getElementById('answer-submissions');
function updateActions() {
var submissionCheckboxes = $('input[type=checkbox].select-submission');
@ -50,9 +51,13 @@
if (someSubmissionsSelected) {
deleteButton.classList.remove('disabled')
deleteButton.style.visibility = "visible";
answerButton.classList.remove('disabled')
answerButton.style.visibility = "visible";
} else {
deleteButton.classList.add('disabled')
deleteButton.style.visibility = "hidden";
answerButton.classList.add('disabled')
answerButton.style.visibility = "hidden";
}
}
@ -104,10 +109,10 @@
<div class="report filterable">
<div class="report__results w-overflow-y-hidden w-overflow-x-scroll w-pb-6">
{% if submissions %}
<form action="{% url 'wagtailforms:delete_submissions' form_page.id %}" method="get">
{% include "wagtailforms/list_submissions.html" %}
<form method="get">
{% include "wagtailforms/submissions_index_list.html" %}
{% include "wagtailadmin/shared/pagination_nav.html" with items=page_obj %}
</form>
</form>
{% else %}
<p class="no-results-message">Il n'y a pas encore de réponse à ce scrutin.</p>
{% endif %}

View File

@ -0,0 +1,39 @@
{% load i18n %}
{# Took from wagtail v4.1.1 (/wagtail/contrib/forms/templates/wagtailforms/list_submissions.html) #}
<div class="overflow">
<table class="listing">
<col />
<col />
<col />
<thead>
<tr>
<th colspan="{{ data_headings|length|add:1 }}">
<button formaction="{% url 'wagtailforms:delete_submissions' form_page.id %}" class="button no" id="delete-submissions" style="visibility: hidden">{% trans "Delete selected submissions" %}</button>
<button formaction="{% url 'scrutin:answer' form_page.id %}" class="button button-secondary icon icon-mail" id="answer-submissions" style="visibility: hidden"> Envoyer un message</button>
</th>
</tr>
<tr>
<th><input type="checkbox" id="select-all" /></th>
{% for heading in data_headings %}
<th id="{{ heading.name }}" class="{% if heading.order %}ordered {{ heading.order }}{% endif %}">
{% if heading.order %}<a href="?order_by={% if heading.order == 'ascending' %}-{% endif %}{{ heading.name }}">{{ heading.label }}</a>{% else %}{{ heading.label }}{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in data_rows %}
<tr>
<td>
<input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" />
</td>
{% for cell in row.fields %}
<td>
{{ cell }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>