Created
June 1, 2018 07:53
-
-
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...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Application models for :mod:`inscription` application | |
:creationdate: 17/04/2018 11:22 | |
:moduleauthor: François GUÉRIN <[email protected]> | |
: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