Merge pull request 'feat(votes): apporte la possibilité de répondre à un ou des votants' (#66) from fpoulain/gvot:reponses into master
Reviewed-on: #66master
révision
4f13d89915
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 }}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
Chargement…
Référencer dans un nouveau ticket