@ -2,10 +2,8 @@ import json
@@ -2,10 +2,8 @@ import json
import uuid
from itertools import groupby
from django import forms
from django . core . exceptions import ValidationError
from django . core . serializers . json import DjangoJSONEncoder
from django . core . validators import MaxLengthValidator , MinLengthValidator
from django . db import models
from django . db . models import F , Q
from django . http import Http404 , HttpResponseGone
@ -13,7 +11,6 @@ from django.shortcuts import get_object_or_404, render
@@ -13,7 +11,6 @@ from django.shortcuts import get_object_or_404, render
from django . template import Engine
from django . urls import reverse
from django . urls . converters import UUIDConverter
from django . utils import timezone
from django . utils . text import slugify
from modelcluster . fields import ParentalKey
@ -28,7 +25,6 @@ from wagtail.admin.edit_handlers import (
@@ -28,7 +25,6 @@ from wagtail.admin.edit_handlers import (
TabbedInterface ,
)
from wagtail . contrib . forms . edit_handlers import FormSubmissionsPanel
from wagtail . contrib . forms . forms import FormBuilder
from wagtail . contrib . forms . models import (
FORM_FIELD_CHOICES ,
AbstractEmailForm ,
@ -40,7 +36,7 @@ from wagtail.core.fields import RichTextField, StreamField
@@ -40,7 +36,7 @@ from wagtail.core.fields import RichTextField, StreamField
from wagtail . core . models import Page
from wagtail . search import index
from . import blocks , emails , validators
from . import blocks , emails , form_builder , validators
from . tapeforms import BigLabelTapeformMixin
@ -103,7 +99,10 @@ FORM_FIELD_CHOICES = [
@@ -103,7 +99,10 @@ FORM_FIELD_CHOICES = [
( c [ 0 ] , ' Choix multiples ' ) if c [ 0 ] == ' checkboxes ' else c
for c in FORM_FIELD_CHOICES
if c [ 0 ] not in [ ' datetime ' , ' hidden ' ]
] + [ ( ' lim_multiselect ' , ' Sélection multiple bornée ' ) ]
] + [
( ' lim_multiselect ' , ' Sélection multiple bornée ' ) ,
( ' ponderated ' , ' Choix pondérés ' ) ,
]
class FormField ( AbstractFormField ) :
@ -162,39 +161,6 @@ class ClosedScrutin(Exception):
@@ -162,39 +161,6 @@ class ClosedScrutin(Exception):
pass
class LimitedSelectMultiple ( forms . SelectMultiple ) :
class Media :
css = { ' all ' : ( ' css/multi-select.css ' , ) }
js = ( ' js/multiselect.js ' , ' js/jquery.multi-select.js ' )
def __init__ ( self , min_values , max_values , attrs = { } , choices = ( ) ) :
attrs . update (
{
' class ' : ' limited-multiselect ' ,
' data-min ' : min_values ,
' data-max ' : max_values ,
}
)
super ( ) . __init__ ( attrs = attrs , choices = choices )
class MyFormBuilder ( FormBuilder ) :
def create_lim_multiselect_field ( self , field , options ) :
options [ ' choices ' ] = map (
lambda x : ( x . strip ( ) , x . strip ( ) ) , field . choices . split ( ' , ' )
)
validators = [ ]
if field . min_values :
validators . append ( MinLengthValidator ( field . min_values ) )
if field . max_values :
validators . append ( MaxLengthValidator ( field . max_values ) )
return forms . MultipleChoiceField (
* * options ,
validators = validators ,
widget = LimitedSelectMultiple ( field . min_values , field . max_values )
)
# 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
@ -341,27 +307,117 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
@@ -341,27 +307,117 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
)
return render ( request , self . get_template ( request ) , context )
form_builder = MyFormBuilder
form_builder = form_builder . MyFormBuilder
def get_form_class ( self ) :
def get_form_class ( self , pouvoir ) :
form_builder = self . form_builder (
self . get_form_fields ( ) , pouvoir = pouvoir
)
# Dynamically inherit Tapeform properties
return type (
' DynForm ' , ( BigLabelTapeformMixin , super ( ) . get_form_class ( ) ) , { }
' DynForm ' ,
(
BigLabelTapeformMixin ,
form_builder . get_form_class ( ) ,
) ,
{ } ,
)
def get_form_parameters ( self ) :
return { }
def get_form ( self , * args , * * kwargs ) :
form_class = self . get_form_class ( )
pouvoir = kwargs . pop ( ' pouvoir ' , None )
vote = (
self . get_submission_class ( )
. objects . filter ( pouvoir = pouvoir , page = self )
. first ( )
form_class = self . get_form_class ( pouvoir )
votes = self . get_submission_class ( ) . objects . filter (
pouvoir = pouvoir , page = self
)
if vote :
initial = json . loads ( vote . form_data )
if votes . exists ( ) :
initial = self . aggregate_from_storage ( pouvoir , votes )
return form_class ( * args , initial = initial , * * kwargs )
return form_class ( * args , * * kwargs )
def aggregate_from_storage ( self , pouvoir , votes ) :
"""
Ponderated fields needs aggregated values .
We fetch ponderated fields in each vote and aggregate them .
"""
form_class = self . get_form_class ( pouvoir )
votes_datas = [
json . loads ( vote )
for vote in votes . values_list ( ' form_data ' , flat = True )
]
datas = votes_datas [ 0 ]
ponderated_fields = [
( name , field )
for name , field in form_class ( ) . fields . items ( )
if isinstance ( field , form_builder . PonderatedField )
]
for name , field in ponderated_fields :
labels = [ f . label for f in field . fields ]
datas [ name ] = [
len ( [ v for v in votes_datas if v [ name ] == label ] )
for label in labels
]
return datas
def desaggregate_form_data ( self , form , ponderation ) :
"""
Ponderated fields return aggregated values .
We store ponderated fields in votes as separated values .
"""
ponderated_fields = [
( name , field )
for name , field in form . fields . items ( )
if isinstance ( field , form_builder . PonderatedField )
]
ponderated_values = [
form_builder . PonderatedWidget ( ) . decompress ( form . cleaned_data [ name ] )
for name , field in ponderated_fields
]
datas = [
{
k : v
for k , v in form . cleaned_data . items ( )
if k not in [ f for f , _ in ponderated_fields ]
}
for _ in range ( ponderation )
]
for field_item , values in zip ( ponderated_fields , ponderated_values ) :
name , field = field_item
labels = [ f . label for f in field . fields ]
if field . required and ponderation != sum ( values ) :
raise ValueError (
" La somme des valeurs contrarie la ponderation. "
)
if not field . required and ponderation < sum ( values ) :
raise ValueError ( " La somme des valeurs épuise la ponderation. " )
desaggregated = [
[ labels [ idx ] ] * count for idx , count in enumerate ( values )
]
flat_desaggregated = [
item for sublist in desaggregated for item in sublist
]
for idx , value in enumerate ( flat_desaggregated ) :
datas [ idx ] [ name ] = value
# Si le champ n'est pas requis on complete les champs à None
if not field . required and ponderation > len ( flat_desaggregated ) :
for idx in range ( len ( flat_desaggregated ) , ponderation ) :
datas [ idx ] [ name ] = None
if ponderation != len ( datas ) :
raise ValueError ( " Situation non permise. " )
return datas
def process_form_submission ( self , request , form , pouvoir ) :
# FIXME: documentation :
# compte tenu que les types des questions/réponses ne sont pas
@ -374,21 +430,25 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
@@ -374,21 +430,25 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
# mauvaise idée) on considère seule légitime la valeur initiale
# s'il s'agit d'une MaJ du vote.
if self . ouvert :
form_data = json . dumps ( form . cleaned_data , cls = DjangoJSONEncoder )
vote_datas = self . desaggregate_form_data ( form , pouvoir . ponderation )
form_datas = [
json . dumps ( form_data , cls = DjangoJSONEncoder )
for form_data in vote_datas
]
votes = self . get_submission_class ( ) . objects . filter ( pouvoir = pouvoir )
if not votes :
# Création
votes . bulk_create (
pouvoir . ponderation
* [
votes . model (
pouvoir = pouvoir , page = self , form_data = form_data
)
]
)
else :
# Mise à jour
votes . update ( form_data = form_data , submit_time = timezone . now ( ) )
if votes :
# On supprime les votes existants
votes . delete ( )
# Création des enregistrements du vote (1 par pondération)
votes . bulk_create (
[
votes . model (
pouvoir = pouvoir , page = self , form_data = form_data
)
for form_data in form_datas
]
)
pouvoir . notify_vote ( request )
elif not self . vote_set . exists ( ) :
@ -536,7 +596,7 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
@@ -536,7 +596,7 @@ class Scrutin(RoutablePageMixin, AbstractEmailForm):
def results_distribution ( self , filtre = None ) :
votes = self . vote_set . all ( )
#if filter:
# if filter:
# votes = votes.filter(**filtre)
compound_fields = self . get_data_fields ( ) [ 1 : ]