117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
from django import forms
|
|
from .models import SustainerOrder
|
|
|
|
|
|
class SustainerFormRenderer(forms.renderers.DjangoTemplates):
|
|
# Customised layout with labels on own row
|
|
field_template_name = 'supporters/field.html'
|
|
|
|
|
|
class ButtonRadioSelect(forms.widgets.RadioSelect):
|
|
"""Radio button styled like a button."""
|
|
|
|
# Extra <span> wrappers to support CSS
|
|
option_template_name = 'supporters/buttonradio_option.html'
|
|
use_fieldset = False
|
|
|
|
class Media:
|
|
css = {
|
|
'all': ['css/buttonradio.css'],
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.attrs['class'] = 'button-select'
|
|
|
|
|
|
class SustainerForm(forms.ModelForm):
|
|
"""Sustainer sign-up
|
|
|
|
The logic for this form is somewhat spread between this Django form and the Django
|
|
template and Alpine JS code in the template.
|
|
|
|
Having to define some of the the Alpine JS attributes here in the form and some in
|
|
the template feels awkward, and I wish there was a better way. Django Crispy Forms
|
|
is typically a good option, but I really wanted to see if the new Django 5 form
|
|
improvements could beat that (eg. ".as_field_group"). They certainly help, but put
|
|
several levels of abstraction between you and the HTML (eg. renderers) and spread
|
|
your HTML across various template and code files. While I appreciate not having to
|
|
write code to render checked and unchecked boxes, designing attractive interactive
|
|
forms shouldn't be this complicated.
|
|
|
|
Alpine JS has its own trade-offs here. There's nearly no JavaScript as such, but the
|
|
"x-.." attributes are meaningless until you read the Alpine docs.
|
|
"""
|
|
|
|
# To pre-fill the price option buttons in the case of server-side validation errors.
|
|
amount_option = forms.CharField(required=False)
|
|
|
|
template_name = 'supporters/sustainer_form.html'
|
|
|
|
MONTH_OPTIONS = [12, 23, 45, 87]
|
|
YEAR_OPTIONS = [128, 256, 512, 1024]
|
|
MONTH_MINIMUM = 10
|
|
YEAR_MINIMUM = 120
|
|
|
|
class Meta:
|
|
model = SustainerOrder
|
|
fields = [
|
|
'recurring',
|
|
'amount',
|
|
'name',
|
|
'email',
|
|
'acknowledge_publicly',
|
|
'add_to_mailing_list',
|
|
'tshirt_size',
|
|
'street',
|
|
'city',
|
|
'state',
|
|
'zip_code',
|
|
'country',
|
|
]
|
|
widgets = {
|
|
'recurring': ButtonRadioSelect(
|
|
attrs={
|
|
'x-model': 'recurring',
|
|
# Reset the amount field and option when changing monthly/annually.
|
|
'x-on:change': 'amount = ""; amount_option = null',
|
|
}
|
|
),
|
|
'amount': forms.widgets.NumberInput(
|
|
# Retaining default widget, just neater to add many attrs here.
|
|
attrs={
|
|
# So we can update the amount field from the amount_option selected.
|
|
'x-model': 'amount',
|
|
'x-bind:min': 'amount_minimum',
|
|
'onblur': 'this.reportValidity()',
|
|
'style': 'width: 5rem',
|
|
}
|
|
),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.renderer = SustainerFormRenderer()
|
|
|
|
self.fields['recurring'].label = ''
|
|
self.fields['amount'].initial = self.YEAR_OPTIONS[0]
|
|
self.fields['tshirt_size'].widget.attrs['x-model'] = 'tshirt_size'
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
recurring = self.cleaned_data.get('recurring', '')
|
|
amount = self.cleaned_data.get('amount', 0)
|
|
minimum = self.MONTH_MINIMUM if recurring == 'month' else self.YEAR_MINIMUM
|
|
if amount < minimum:
|
|
self.add_error('', f'${minimum:d} is a minimum for Conservancy Sustainers.')
|
|
tshirt_size = self.cleaned_data.get('tshirt_size')
|
|
address_provided = all(
|
|
[
|
|
self.cleaned_data.get('street'),
|
|
self.cleaned_data.get('city'),
|
|
self.cleaned_data.get('country'),
|
|
]
|
|
)
|
|
if tshirt_size and not address_provided:
|
|
self.add_error('street', 'No address provided')
|