Skip to content

Commit

Permalink
Merge pull request #406 from alphagov/ldeb-date-field
Browse files Browse the repository at this point in the history
Add a DateField for WTForms
  • Loading branch information
lfdebrux authored Jun 8, 2018
2 parents 02968dd + eba4e27 commit f29878b
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 3 deletions.
2 changes: 1 addition & 1 deletion dmutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
import flask_featureflags # noqa


__version__ = '38.2.0'
__version__ = '38.3.0'
103 changes: 102 additions & 1 deletion dmutils/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
'''
Fields for WTForms that are used within Digital Marketplace apps
'''

import datetime
from itertools import chain

from wtforms import StringField
from wtforms import Field, Form, FormField
from wtforms.fields import IntegerField, StringField
from wtforms.utils import unset_value
from wtforms.validators import Length

from .filters import strip_whitespace
Expand Down Expand Up @@ -28,3 +35,97 @@ def __init__(self, label=None, **kwargs):
),
))
super(EmailField, self).__init__(label, **kwargs)


class DateField(Field):
'''
A date field(set) that uses a day, month and year field.
The data is converted to a `datetime.date`. The year is
required to be four digits.
It behaves like a WTForms.FieldForm, but it can be used
with validators like a normal WTForms.Field.
>>> from wtforms import Form
>>> from wtforms.validators import DataRequired
>>> from werkzeug.datastructures import MultiDict
>>> formdata = MultiDict({
... 'date-day': '31',
... 'date-month': '12',
... 'date-year': '1999'})
>>> class DateForm(Form):
... date = DateField(validators=[DataRequired()])
>>> form = DateForm(formdata)
>>> form.date.data
datetime.date(1999, 12, 31)
'''

# An internal class that defines the fields that make up the DateField.
#
# Inheriting from wtforms.FormField has limitations on using validators.
#
# Instead, the DateField is composed of a wtforms.FormField that is used
# to turn the form data into integer values, and we then grab the data.
#
# The FormField instance is based on this class.
class _DateForm(Form):
day = IntegerField("Day")
month = IntegerField("Month")
year = IntegerField("Year")

def __init__(self, label=None, validators=None, separator='-', **kwargs):
super().__init__(label=label, validators=validators, **kwargs)
self.form_field = FormField(self._DateForm, separator=separator, **kwargs)

def _value(self):
'''
Return the values that are used to display the form
Overrides wtforms.Field._value().
'''
if self.raw_data:
return self.raw_data[0]
else:
return {}

def process(self, formdata, data=unset_value):
'''
Process incoming data.
Overrides wtforms.Field.process().
Filters, process_data and process_formdata are not supported.
'''
self.process_errors = []

# use the FormField to process `formdata` and `data`
self.form_field.process(formdata, data)

# make a "fake" raw_data property from the FormField values
# we need the raw_data property for various validators
raw_data = {field.name: field.raw_data[0] for field in self.form_field
if field.raw_data}
if not any(raw_data.values()):
# if all fields were empty we want raw_data to be None-ish
raw_data = {}

# the WTForms.Field api expects .raw_data to be a list
self.raw_data = [raw_data]

try:
self.data = datetime.date(**self.form_field.data)
except (TypeError, ValueError):
self.data = None
self.process_errors.append(self.gettext('Not a valid date value'))

# if the year does not have four digits call the data invalid
if self.data:
try:
digits = len(self.form_field.year.raw_data[0])
except IndexError:
digits = 0

if not digits == 4:
self.data = None
self.process_errors.append(self.gettext('Not a valid date value'))
64 changes: 63 additions & 1 deletion dmutils/forms/validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'''
Validators for WTForms used in the Digital Marketplace frontend
EmailValidator -- validate that a string is a valid email address
GreaterThan -- compare the values of two fields
FourDigitYear -- validate that a four digit year value is provided
'''

import re

from wtforms.validators import Regexp
from wtforms.validators import Regexp, ValidationError


class EmailValidator(Regexp):
Expand All @@ -10,3 +17,58 @@ class EmailValidator(Regexp):
def __init__(self, **kwargs):
kwargs.setdefault("message", "Please enter a valid email address.")
super(EmailValidator, self).__init__(self._email_re, **kwargs)


class GreaterThan:
"""
Compares the values of two fields.
:param fieldname:
The name of the other field to compare to.
:param message:
Error message to raise in case of a validation error.
"""
def __init__(self, fieldname, message=None):
self.fieldname = fieldname
self.message = message

def __call__(self, form, field):
try:
other = form[self.fieldname]
except KeyError:
raise ValidationError(field.gettext("Invalid field name '%s'." % self.fieldname))
if other.data and not field.data > other.data:
d = {
'other_label': hasattr(other, 'label') and other.label.text or self.fieldname,
'other_name': self.fieldname
}
message = self.message
if message is None:
message = field.gettext('Field must be greater than %(other_name)s.')

raise ValidationError(message % d)


class FourDigitYear:
"""
Validates that a `DateField`'s year field has a four digit year value.
:param message:
Error message to raise in case of a validation error.
"""
def __init__(self, message=None):
self.message = message

def __call__(self, form, field):
try:
digits = len(field.form_field.year.raw_data[0])
except IndexError:
digits = 0

if not digits == 4:
message = self.message
if message is None:
message = field.gettext("Year must be YYYY.")

raise ValidationError(message)
Loading

0 comments on commit f29878b

Please sign in to comment.