Skip to content

Instantly share code, notes, and snippets.

@frague59
Created June 1, 2018 07:53
Show Gist options
  • Save frague59/9c366e0fd59668ceb3ca31ab8aa20748 to your computer and use it in GitHub Desktop.
Save frague59/9c366e0fd59668ceb3ca31ab8aa20748 to your computer and use it in GitHub Desktop.
Wagtail: Implementation of a limited choices RadioWidget. This is the models.py file of my project, it contains all the logic of the module, according to wagtail phylosophy...
# -*- coding: utf-8 -*-
"""
Application models for :mod:`inscription` application
:creationdate: 17/04/2018 11:22
:moduleauthor: François GUÉRIN <fguerin@ville-tourcoing.fr>
:modulename: inscription.models
"""
import json
import logging
import pprint
from copy import deepcopy
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.forms import ChoiceField, RadioSelect
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalKey
from slugify import slugify as uni_slugify
from wagtail.wagtailadmin import edit_handlers
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailforms.forms import FormBuilder
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission, FORM_FIELD_CHOICES
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtailmenus.models import MenuPage
from account.current_user import get_current_user
# noinspection PyUnresolvedReferences
from inscription.helpers import template_from_string
# noinspection PyUnresolvedReferences
from .conf import InscriptionAppConf
logger = logging.getLogger('inscription.models')
LIMITED_RADIO = 'limited_radio'
_FORM_FIELD_CHOICES = list(FORM_FIELD_CHOICES)
_FORM_FIELD_CHOICES.append((LIMITED_RADIO, _('Limited radio buttons')))
class LimitedChoiceField(ChoiceField):
"""
ChoiceField that limits the choices to a known count of possible registrations
"""
def __init__(self, *args, **kwargs):
self._choices_max_counts = kwargs.pop('_choices_max_counts', {})
self._choices_current_counts = kwargs.pop('_choices_current_counts', {})
self._form_page = kwargs.pop('_form_page')
self._inscription_field = None
logger.debug('LimitedChoiceField() self._choices_max_counts = %s',
pprint.pformat(self._choices_max_counts))
logger.debug('LimitedChoiceField() self._choices_current_counts = %s',
pprint.pformat(self._choices_current_counts))
# Creates a new widget class, based on :class:`LimitedRadioSelect`
label = kwargs.get('label')
_class_name = uni_slugify("X" + label, ok='', only_ascii=True, lower=False) + 'LimitedRadioSelect'
widget = type(_class_name, (LimitedRadioSelect,), {'_form_page': self.form_page,
'_choices_current_counts': self.choices_current_counts,
'_choices_max_counts': self.choices_max_counts,})
kwargs.update({'widget': widget})
super(LimitedChoiceField, self).__init__(*args, **kwargs)
def get_choices_max_counts(self):
return self._choices_max_counts
def get_choices_current_counts(self):
return self._choices_current_counts
def get_form_page(self):
return self._form_page
choices_max_counts = property(get_choices_max_counts, None, None, "Gets the max counts")
choices_current_counts = property(get_choices_current_counts, None, None, "Gets the current counts")
form_page = property(get_form_page, None, None, 'Gets the associated form page')
def _check_form_field_value(self, value):
max_count = self.get_choices_max_counts().get(value)
current_count = self.get_choices_current_counts().get(value, 0)
can_add = current_count < max_count
return {'value': value,
'can_add': can_add,
'current_count': current_count,
'max_count': max_count}
def validate(self, value):
super(LimitedChoiceField, self).validate(value=value)
result = self._check_form_field_value(value)
logger.debug('LimitedChoiceField::validate(%s) result = %s', value, pprint.pformat(result))
if not result['can_add']:
raise ValidationError(_('You cannot use the "%(value)s" value, '
'there are too many inscriptions : %(current_count)d registered / '
'%(max_count)d allowed.')
% result)
class LimitedRadioSelect(RadioSelect):
"""
RadioSelect that limits the choices to known count of inscriptions
"""
option_template_name = "inscription/widgets/radio_option.html"
_form_page = None
_choices_max_counts = {}
_choices_current_counts = {}
@classmethod
def set_form_page(cls, form_page):
cls._form_page = form_page
@classmethod
def get_choices_max_counts(cls):
return cls._choices_max_counts
@classmethod
def set_choices_max_counts(cls, choices_max_counts):
cls._choices_max_counts = choices_max_counts
@classmethod
def get_choices_current_counts(cls):
return cls._choices_current_counts
@classmethod
def set_choices_current_counts(cls, choices_current_counts):
cls._choices_current_counts = choices_current_counts
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
"""
Creates an option based on tis name / value / label...
.. note::
This method updates the attrs passed to the option,
adding "disabled" to the radio input when nobody can inscribe anymore.
:param name: field name
:param value: option value
:param label: option label
:param selected: option selected
:param index: index in list
:param subindex: sub-index in list
:param attrs: Additional attrs passed to the input widget
:return: option as a dict, used as template context
"""
option = super(LimitedRadioSelect, self).create_option(name=name, value=value, label=label, selected=selected,
index=index, subindex=subindex, attrs=attrs)
logger.debug('%s::create_option() name = %s / self.get_choices_max_counts() = %s',
self.__class__.__name__, name, pprint.pformat(self.get_choices_max_counts()))
logger.debug('%s::create_option() name = %s / self.choices_current_counts = %s',
self.__class__.__name__, name, pprint.pformat(self.get_choices_current_counts()))
max_count = self.get_choices_max_counts()[value]
current_count = self.get_choices_current_counts()[value]
logger.debug('%s::create_option() value: "%s" / max_count = %d / current_count = %d',
self.__class__.__name__, value, max_count, current_count)
if current_count > 0 and 0 < max_count <= current_count:
option['attrs'].update({'disabled': True})
logger.debug('%s::create_option() name="%s" / value: "%s" / option: \n%s',
self.__class__.__name__, name, value, pprint.pformat(option))
return option
def create_limited_radio_field(form_builder, field, options):
"""
Creates a limited radio field
:param form_builder: Form builder instance
:param field: Field
:param options: options
:return: new instance of field
"""
options['_form_page'] = form_builder.get_form_page()
options['_choices_max_counts'] = field.get_form_field_max_counts()
options['_choices_current_counts'] = form_builder.get_form_page().get_limited_form_field_current_counts(field)
options['choices'] = map(lambda x: (x.strip(), x.strip()), list(options['_choices_max_counts']))
logger.debug('create_limited_radio_field() options = %s', pprint.pformat(options))
return deepcopy(LimitedChoiceField(**options))
class InscriptionFormField(AbstractFormField):
"""
Form fields for inscription form pages
.. note:: this adds the `LimitedChoiceField` class to the available fields choice
"""
page = ParentalKey('InscriptionFormPage', related_name='form_fields')
#: Overrides the :attr:`wagtail:wagtail.wagtailforms.models.AbstractFormField.field_type` to update the choices list
field_type = models.CharField(verbose_name=_('field type'), max_length=16, choices=_FORM_FIELD_CHOICES)
def get_form_field_max_counts(self):
choice_list = map(lambda x: x.strip(), self.choices.split(','))
field_max_counts = {}
for choice in choice_list:
splited = choice.split(settings.INSCRIPTION_FIELD_DELIMITER)
if len(splited) > 2:
_choice = settings.INSCRIPTION_FIELD_DELIMITER.join(splited[0, -1]).strip()
else:
_choice = splited[0].strip()
field_max_counts.update({_choice: int(splited[-1].strip())})
logger.debug('InscriptionFormField::get_form_field_max_counts() field_max_counts = %s',
pprint.pformat(field_max_counts))
return field_max_counts
class InscriptionFormBuilder(FormBuilder):
"""
Form builder that adds the form page instance as attribute
"""
_form_page = NotImplemented
def __init__(self, *args, **kwargs):
super(InscriptionFormBuilder, self).__init__(*args, **kwargs)
self.FIELD_TYPES.update({LIMITED_RADIO: create_limited_radio_field})
def get_form_page(self):
return self._form_page
class InscriptionFormPage(AbstractEmailForm, MenuPage):
"""
Inscription form page, which provides :class:`inscription.models.LimitedChoiceField` fields
"""
#: Custom form builder, which takes the form page as attribute
form_builder = InscriptionFormBuilder
#: Heading text of the form
intro = RichTextField(verbose_name=_('Heading'), blank=True)
#: Text displays to thanks the user
thank_you_text = RichTextField(verbose_name=_('Thank you text'), blank=True)
#: Attached illustration for the form page
illustration = models.ForeignKey('wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
verbose_name=_('Illustration'))
#: Attached illustration credit for the form page
credit = models.CharField(max_length=255, default=_('All rights reserved'))
send_confirmation = models.BooleanField(verbose_name=_('send a confirmation email'), default=False)
#: Confirmation email subject template. `{foo}` formatting replacements are done while generating the email
confirm_subject = models.CharField(max_length=255, verbose_name=_('confirm email subject template'),
default=settings.INSCRIPTION_CONFIRM_SUBJECT, null=True, blank=True)
#: Confirmation email body template. `{bar}` formatting replacements are done while generating the email
confirm_body = RichTextField(verbose_name=_('confirm email body template'),
default=settings.INSCRIPTION_CONFIRM_BODY, null=True, blank=True)
class Meta:
verbose_name = _('Inscription form page')
verbose_name_plural = _('Inscription form pages')
def get_form_class(self):
"""
Adds the form_page to the the form builder, then use it to create the form
:return: form class
"""
fb = self.form_builder(self.get_form_fields())
fb._form_page = self
form_class = fb.get_form_class()
# Adds the clean method to form
return form_class
@property
def results_count(self):
submission_class = self.get_submission_class()
count = submission_class.objects.filter(page=self).count()
logger.debug('FormPage::results_count() count = %d', count)
return count
def process_form_submission(self, form):
submission = self.get_submission_class().objects.create(form_data=json.dumps(form.cleaned_data,
cls=DjangoJSONEncoder),
page=self, user=form.user)
# Sends the registration email
if self.to_address:
self.send_mail(form)
return submission
def get_submission_class(self):
"""
Returns submission class.
You can override this method to provide custom submission class.
Your class must be inherited from AbstractFormSubmission.
"""
return InscriptionFormSubmission
def get_limited_form_fields(self):
"""
Gets the limited limited radio fields from current form
:return: Yields the fields
"""
for form_field in self.form_fields.all():
if form_field.field_type == LIMITED_RADIO:
yield form_field
def get_limited_form_field_current_counts(self, form_field):
assert form_field.field_type == LIMITED_RADIO, "Only available `limited_radio` fields have a current count"
choices = [choice for choice in form_field.get_form_field_max_counts()]
field_max_counts = {}
for value in choices:
field_max_counts.update({value: self.get_choice_current_count(form_field, value)})
logger.debug('get_limited_form_field_current_counts() field_max_counts = \n%s',
pprint.pformat(field_max_counts))
return field_max_counts
def get_choice_current_count(self, form_field, value):
"""
Gets the counts for each available choices
:param form_field: form field instance
:param value: choice value
:return: Count of times the value has been chosen
"""
submissions = InscriptionFormSubmission.objects.filter(page=self)
if not submissions:
return 0
# Parses the form data
choice_used = 0
form_field_label = slugify(form_field.label)
for submission in submissions:
data = submission.get_data()
if form_field_label in data and data[form_field_label] == value:
choice_used += 1
return choice_used
content_panels = AbstractEmailForm.content_panels + [
edit_handlers.FieldPanel('intro', classname='full'),
edit_handlers.InlinePanel('form_fields', label=_('Form fields')),
edit_handlers.FieldPanel('thank_you_text', classname="full"),
edit_handlers.MultiFieldPanel([ImageChooserPanel('illustration'),
edit_handlers.FieldPanel('credit')],
heading=_('Illustration'), classname='collapsible collapsed'),
edit_handlers.MultiFieldPanel([edit_handlers.FieldPanel('send_confirmation'),
edit_handlers.FieldPanel('confirm_subject'),
edit_handlers.RichTextFieldPanel('confirm_body')],
heading=_('Confirmation email'), classname='collapsible collapsed'),
edit_handlers.MultiFieldPanel([edit_handlers.FieldRowPanel([edit_handlers.FieldPanel('from_address',
classname="col6"),
edit_handlers.FieldPanel('to_address',
classname="col6")]
),
edit_handlers.FieldPanel('subject'),
],
heading=_("Email")),
]
def clean(self):
if self.send_confirmation and not self.confirm_body:
raise ValidationError({'confirm_body': _('If you want a confirmation email, you have to provide '
'the confirm body.')})
if self.send_confirmation and not self.confirm_subject:
raise ValidationError({'confirm_subject': _('If you want a confirmation email, you have to provide '
'the confirm subject.')})
def get_data_fields(self):
data_fields = super(InscriptionFormPage, self).get_data_fields()
data_fields += [('username', _('Username')),
('last_name', _('last name')),
('first_name', _('first name')),
('email', _('email')), ]
return data_fields
class InscriptionFormSubmission(AbstractFormSubmission):
"""
Inscription form submission instances
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=get_current_user, on_delete=models.CASCADE)
class Meta:
verbose_name = _('Inscription for submission')
verbose_name_plural = _('Inscription for submissions')
ordering = ('page', 'submit_time')
def get_data(self):
form_data = super(InscriptionFormSubmission, self).get_data()
form_data.update({'username': self.user.username,
'last_name': self.user.last_name,
'first_name': self.user.first_name,
'email': self.user.email, })
return form_data
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment