"""
Common form fields
These can also be accessed from ``fusionbox.forms``.
"""
import calendar
import datetime
import re
import six
from functools import partial
import phonenumbers
from django import forms
from django.utils import timezone
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
from django.utils.safestring import mark_safe
from django.forms.util import flatatt
from fusionbox.forms.widgets import MultiFileWidget
[docs]class MonthField(forms.TypedChoiceField):
"""
:class:`MonthField` is a :class:`TypedChoiceField` that selects a month.
Its python value is a 1-indexed month number.
"""
def __init__(self, *args, **kwargs):
super(MonthField, self).__init__(*args, **kwargs)
self.choices = [('', ' -- ')] + \
[(i, "%s - %s" % (i, calendar.month_name[i]))
for i in range(1, 13)]
self.coerce = int
[docs]class FutureYearField(forms.TypedChoiceField):
"""
:class:`FutureYearField` is a :class:`TypedChoiceField` that selects a
year a defined period in the future. Useful for credit card expiration
dates.
"""
def __init__(self, *args, **kwargs):
number_of_years = kwargs.pop('number_of_years', 6)
super(FutureYearField, self).__init__(*args, **kwargs)
self.choices = [('', ' -- ')] + \
[(i % 100, str(i))
for i in range(datetime.datetime.now().year, datetime.datetime.now().year + number_of_years)]
self.coerce = int
[docs]class MultiFileField(forms.FileField):
"""
Implements a multifile field for multiple file uploads.
This class' `clean` method is implented by currying `super.clean`
and running map over `data` which is a list of file upload objects received
from the :class:`MultiFileWidget`.
Using this field requires a little work on the programmer's part in order
to use correctly. Like other Forms with fields that inherit from
:class:`FileField`, the programmer must pass in the kwarg `files` when creating
the form instance. For example:
```
form = MyFormWithFileField(data=request.POST, files=request.FILES)
```
After validation, the cleaned data will be a list of files. You might
want to iterate over in a manner similar to this:
```
if form.is_valid():
for media_file in form.cleaned_data['field_name']:
MyMedia.objects.create(
name=media_file.name,
file=media_file
)
```
"""
default_error_messages = {
'required': u'This field is required.',
'invalid': u'Enter a valid value.',
}
widget = MultiFileWidget
def clean(self, data, initial=None):
try:
curry_super = partial(super(MultiFileField, self).clean,
initial=initial)
return map(curry_super, data)
except TypeError:
return []
class UncaptchaWidget(forms.HiddenInput):
"""
Renders as an empty string. To render this field use the uncaptcha
template tag.
"""
def render(self, name, value, attrs=None):
return ''
class UncaptchaField(forms.CharField):
"""
Extension of Charfield with ``UncaptchaWidget`` as its default widget.
"""
widget = UncaptchaWidget
[docs]class NoAutocompleteCharField(forms.CharField):
"""
:class:`NoAutocompleteCharField` is a subclass of ``CharField`` that sets
the ``autocomplete`` attribute to ``off``. This is suitable for credit
card numbers and other such sensitive information.
This should be used in conjunction with the `sensitive_post_parameters
<https://docs.djangoproject.com/en/dev/howto/error-reporting/#sensitive_post_parameters>`
decorator.
"""
def widget_attrs(self, *args, **kwargs):
ret = super(NoAutocompleteCharField, self).widget_attrs(*args, **kwargs) or {}
ret['autocomplete'] = 'off'
return ret
class USDCurrencyField(forms.DecimalField):
"""
Form field for entering dollar amounts. Allows an optional leading dollar
sign, which gets stripped.
"""
def clean(self, value):
return super(USDCurrencyField, self).clean(value.lstrip('$'))
class BetterReadOnlyPasswordHashWidget(ReadOnlyPasswordHashWidget):
"""
A more user friendly password hash field for use with the displaying
passwords in the django admin, or elsewhere.
"""
def render(self, name, value, attrs):
final_attrs = flatatt(self.build_attrs(attrs))
hidden = u'<div{attrs}><strong>*************</strong></div>'.format(
attrs=final_attrs
)
return mark_safe(hidden)
class PhoneNumberField(forms.CharField):
"""
A USA or international phone number field. Normalizes its value to
a common US format '(303) 555-5555' for US phone numbers, and an
international format for others -- '+86 10 6944 5464'. Also supports
extensions -- '3035555555ex12' -> '(303) 555-5555 ext. 12.'
If you do not wish to allow phone numbers with extensions, use
`allow_extension=False`.
"""
default_error_messages = {
'invalid': 'Enter a valid phone number.',
'extension': 'A phone number with an extension is not supported.',
}
def __init__(self, *args, **kwargs):
self.allow_extension = kwargs.pop('allow_extension', True)
super(PhoneNumberField, self).__init__(*args, **kwargs)
def clean(self, value):
value = super(PhoneNumberField, self).clean(value)
if not value:
return value
try:
number = phonenumbers.parse(value, 'US')
except phonenumbers.NumberParseException:
raise forms.ValidationError(self.error_messages['invalid'])
if not phonenumbers.is_valid_number(number):
raise forms.ValidationError(self.error_messages['invalid'])
if not self.allow_extension and number.extension is not None:
raise forms.ValidationError(self.error_messages['extension'])
if number.country_code == 1:
return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL)
else:
return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
class CCExpirationDateField(forms.CharField):
"""
Credit Card expiration date.
"""
default_error_messages = {
'invalid': 'Enter a valid month and year',
'expired': 'This card is expired',
}
class MonthYear(object):
def __init__(self, month, year):
self.month = month
self.year = year
def __init__(self, *args, **kwargs):
kwargs.setdefault('help_text', "MM / YY or MM / YYYY")
super(CCExpirationDateField, self).__init__(*args, **kwargs)
def clean(self, value):
value = super(CCExpirationDateField, self).clean(value)
match = re.match(r'^\s*(?P<month>\d{1,2})\s*\/\s*(?P<year>\d{2}|\d{4})\s*$', value)
if match:
month = int(match.group('month'), 10)
year = int(match.group('year'), 10)
if year < 100: # If they entered only two number for the year
year += 2000 # Use 20YY
if not 0 < month < 13:
raise forms.ValidationError(self.error_messages['invalid'])
now = timezone.now()
if year < now.year:
raise forms.ValidationError(self.error_messages['expired'])
if year == now.year and month < now.month:
raise forms.ValidationError(self.error_messages['expired'])
return CCExpirationDateField.MonthYear(month, year)
elif not self.required and value == '':
return None
else:
raise forms.ValidationError(self.error_messages['invalid'])
class CCNumberField(forms.CharField):
"""
Credit Card Number Field
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('widget', forms.TextInput)
super(CCNumberField, self).__init__(*args, **kwargs)
def clean(self, value):
if isinstance(value, six.string_types):
value = re.sub('\D', '', value)
number = super(CCNumberField, self).clean(value)
return number