diff --git a/docs/source/administration/index.rst b/docs/source/administration/index.rst index 200dd8a..5cb2994 100644 --- a/docs/source/administration/index.rst +++ b/docs/source/administration/index.rst @@ -133,7 +133,8 @@ Les colonnes « nom », « prenom » et « collectif » ne peuvent être v à la fois. Dit autrement le pouvoir doit au moins désigner un nom, un prénom ou un nom de collectif. -La colonne « courriel » ne peut être vide. +La colonne « courriel » ne peut être vide. Plusieurs colonnes « courriel » +peuvent être présentes. Une pondération absente sera interprétée à la valeur « 1 ». @@ -262,8 +263,12 @@ Expédition d'un mailing Vous pouvez démarrer un mailing d'annonce, directement depuis le panel « Pouvoirs ». +Il est possible de filtrer les destinataires concernés selon plusieurs +critères : ayant voté ou non, ayant tel attribut défini ou non, +à quelle valeur, etc. + Une fois les modalités d'envoi définies, une confirmation avec -prévisualisation du mailing vous sera présentée. +prévisualisation du mailing et de ses destinataires vous sera présentée. .. note:: diff --git a/gvot/base/forms.py b/gvot/base/forms.py index dc3512a..8d27edd 100644 --- a/gvot/base/forms.py +++ b/gvot/base/forms.py @@ -29,8 +29,30 @@ class MaillingForm(forms.Form): filter_key = forms.ChoiceField( choices=(), required=False, - help_text="Filtre optionnellement les pouvoirs dont " - "le champ personnalisé désigné est égal à :", + ) + + filter_ope = forms.ChoiceField( + choices=( + (None, "Choississez une opération de filtrage"), + ('icontains', "Contient"), + ('istartswith', "Commence par"), + ('iendswith', "Termine par"), + ('iexact', "Est"), + ('not_isempty', "Est défini"), + ('not_icontains', "Est défini et ne contient pas"), + ('not_istartswith', "Est défini et ne commence pas par"), + ('not_iendswith', "Est défini et ne termine pas par"), + ('not_iexact', "Est défini et est différent de"), + ('isempty', "N'est pas défini"), + ('empty_not_icontains', "N'est pas défini ou ne contient pas"), + ( + 'empty_not_istartswith', + "N'est pas défini ou ne commence pas par", + ), + ('empty_not_iendswith', "N'est pas défini ou ne termine pas par"), + ('empty_not_iexact', "N'est pas défini ou est différent de"), + ), + required=False, ) filter_val = forms.CharField( @@ -38,6 +60,28 @@ class MaillingForm(forms.Form): max_length=255, ) + def clean(self): + cleaned_data = super().clean() + if cleaned_data.get('filter_key') and not cleaned_data.get( + 'filter_ope' + ): + self.add_error( + 'filter_ope', + ValidationError( + "Veuillez définir l'opération de filtrage à appliquer" + ), + ) + if cleaned_data.get('filter_ope') and not cleaned_data.get( + 'filter_key' + ): + self.add_error( + 'filter_key', + ValidationError( + "Veuillez définir à quel champ s'applique le filtre" + ), + ) + return cleaned_data + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/gvot/base/views.py b/gvot/base/views.py index f004bc1..25f809e 100644 --- a/gvot/base/views.py +++ b/gvot/base/views.py @@ -2,6 +2,7 @@ import csv from django.conf import settings from django.core.exceptions import ValidationError +from django.db.models import Q from django.shortcuts import redirect from django.urls import reverse, reverse_lazy from django.views.generic import FormView, RedirectView, detail @@ -137,6 +138,7 @@ class MaillingIndex(FormInvalidMixin, FormView): self.request.session['dests'] = form.cleaned_data['dests'] self.request.session['template_id'] = form.cleaned_data['template'].id self.request.session['filter_key'] = form.cleaned_data['filter_key'] + self.request.session['filter_ope'] = form.cleaned_data['filter_ope'] self.request.session['filter_val'] = form.cleaned_data['filter_val'] return super().form_valid(form) @@ -154,6 +156,7 @@ class MaillingConfirm(FormInvalidMixin, FormView): self.dests = self.request.session.get('dests', None) self.template_id = self.request.session.get('template_id', None) self.filter_key = self.request.session.get('filter_key', None) + self.filter_ope = self.request.session.get('filter_ope', None) self.filter_val = self.request.session.get('filter_val', None) def dispatch(self, request, *args, **kwargs): @@ -174,10 +177,51 @@ class MaillingConfirm(FormInvalidMixin, FormView): self.qs = pouvoirs.filter(vote__isnull=True) if self.filter_key: - self.qs = pouvoirs.filter( - champ_perso__intitule=self.filter_key, - champ_perso__contenu=self.filter_val, - ).distinct() + if self.filter_ope in [ + 'icontains', + 'iendswith', + 'iexact', + 'istartswith', + ]: + filtre = Q(champ_perso__intitule=self.filter_key) & Q( + **{ + 'champ_perso__contenu__' + + self.filter_ope: self.filter_val + } + ) + elif self.filter_ope in [ + 'not_icontains', + 'not_iendswith', + 'not_iexact', + 'not_istartswith', + ]: + filtre = Q(champ_perso__intitule=self.filter_key) & ~Q( + **{ + 'champ_perso__contenu__' + + self.filter_ope[4:]: self.filter_val + } + ) + elif self.filter_ope in [ + 'empty_not_icontains', + 'empty_not_iendswith', + 'empty_not_iexact', + 'empty_not_istartswith', + ]: + filtre = ~Q(champ_perso__intitule=self.filter_key) | ( + Q(champ_perso__intitule=self.filter_key) + & ~Q( + **{ + 'champ_perso__contenu__' + + self.filter_ope[10:]: self.filter_val + } + ) + ) + elif self.filter_ope == 'isempty': + filtre = ~Q(champ_perso__intitule=self.filter_key) + elif self.filter_ope == 'not_isempty': + filtre = Q(champ_perso__intitule=self.filter_key) + + self.qs = pouvoirs.filter(filtre).distinct() return super().dispatch(request, *args, **kwargs) @@ -189,13 +233,19 @@ class MaillingConfirm(FormInvalidMixin, FormView): # drop now obsolete session data self.request.session.pop('dests', False) self.request.session.pop('template_id', False) + self.request.session.pop('filter_key', False) + self.request.session.pop('filter_ope', False) + self.request.session.pop('filter_val', False) return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['scrutin'] = self.template.scrutin - context['nb'] = self.qs.values_list('courriels__courriel').count() + context['qs'] = self.qs + context['nb_dests'] = self.qs.values_list( + 'courriels__courriel' + ).count() if self.dests == 'tous': context['dests'] = "tous les participants" elif self.dests == 'exprimes': @@ -203,6 +253,11 @@ class MaillingConfirm(FormInvalidMixin, FormView): elif self.dests == 'abstenus': context['dests'] = "tous les participants n'ayant pas encore voté" context['filter_key'] = self.filter_key + context['filter_ope'] = ( + dict(forms.MaillingForm.declared_fields['filter_ope'].choices) + .get(self.filter_ope) + .lower() + ) context['filter_val'] = self.filter_val context['preview'] = dict( zip( diff --git a/gvot/templates/mailing/confirm.html b/gvot/templates/mailing/confirm.html index e23bc67..3bb55bd 100644 --- a/gvot/templates/mailing/confirm.html +++ b/gvot/templates/mailing/confirm.html @@ -7,20 +7,19 @@ {% include "wagtailadmin/shared/header.html" with title="Démarrer un mailing" icon="mail" subtitle=" Analyse du mailing avant validation" %}
Vous êtes sur le point d'envoyer un mailing à {{ dests }} au scrutin « {{ scrutin.title }} »{% if filter_key %} - dont le champ « {{ filter_key }} » est égal à « {{ filter_val }} »{% endif %}. + dont le champ « {{ filter_key }} » {{ filter_ope }}{% if "défini" not in filter_ope %} « {{ filter_val }} »{% endif %}{% endif %}.