from __future__ import unicode_literals
from copy import deepcopy
from django.core.exceptions import ValidationError
from django.forms.fields import CharField
from django.forms.fields import ChoiceField
from django.forms.fields import DecimalField
from django.forms.fields import MultiValueField
from django.utils.translation import ugettext as _
from django_core.forms.widgets import ChoiceAndCharInputWidget
from .widgets import MultipleDecimalInputWidget
[docs]class CharFieldStripped(CharField):
"""Wrapper around CharField that strips whitespace from the CharField when
validating so .strip() doesn't have to be called every time you validate
the field's data.
"""
def clean(self, value):
if value:
value = value.strip()
return super(CharFieldStripped, self).clean(value)
[docs]class CommaSeparatedListField(CharFieldStripped):
"""Form field that takes a string and converts into a list of strings."""
def __init__(self, max_list_length=None, max_list_length_error_msg=None,
*args, **kwargs):
"""
:params max_list_length: the max number of items in the list. If None,
there is no limit.
:params max_list_length_error_msg: if the max limit is reached, this is
the error message that will display. This can also be a formatted
string that contains one or more of the following values:
- max_list_length: the max list length for the field
- actual_list_length: the current number of items
"""
super(CommaSeparatedListField, self).__init__(*args, **kwargs)
self.max_list_length = max_list_length
self.max_list_length_error_msg = max_list_length_error_msg
if max_list_length:
# add the data attribute to the widget so the max length is
# accessible in the dom
self.widget.attrs['data-max_list_length'] = max_list_length
def validate(self, value):
super(CommaSeparatedListField, self).validate(value)
if (self.max_list_length is not None and
len(value) > self.max_list_length):
if self.max_list_length_error_msg:
error_msg_kwargs = {}
if '{max_list_length}' in self.max_list_length_error_msg:
error_msg_kwargs['max_list_length'] = self.max_list_length
if '{actual_list_length}' in self.max_list_length_error_msg:
error_msg_kwargs['actual_list_length'] = len(value)
if error_msg_kwargs:
raise ValidationError(self.max_list_length_error_msg.format(
**error_msg_kwargs
))
raise ValidationError(self.max_list_length_error_msg)
raise ValidationError(_(
'Maximum number of items is {0}. There are currently {1} items '
'listed.').format(self.max_list_length, len(value)))
def to_python(self, value):
value = super(CharFieldStripped, self).to_python(value)
return [item.strip() for item in value.split(',') if item.strip()]
[docs]class CommaSeparatedIntegerListField(CommaSeparatedListField):
"""Comma Separated Integer list field."""
# TODO: this should honor:
# max_value (each item in the list can't be > this value)
# min_value (each item in the list can't be < this value)
# max_length (could be the number of items in the list)
def to_python(self, value):
value = super(CommaSeparatedIntegerListField, self).to_python(value)
if isinstance(value, (list, tuple)):
# Ensure all values are integers
try:
value = [int(item) for item in value]
except:
raise ValidationError('All values in list must be whole '
'numbers.')
return value
[docs]class MultipleDecimalField(MultiValueField):
"""A field with multiple decimal fields that should be converted to single
line.
Example response values:
- "5px 0 5px 4px"
"""
widget = MultipleDecimalInputWidget
def __init__(self, num_inputs=2, value_suffix='', *args, **kwargs):
"""
:param value_suffix: the suffix to append to the end of the decimal
field. Default is nothing.
"""
self.num_inputs = num_inputs
self.value_suffix = value_suffix
fields = [DecimalField(required=False)
for i in range(num_inputs)]
widget = self.widget(num_inputs=num_inputs)
super(MultipleDecimalField, self).__init__(fields=fields,
widget=widget,
*args, **kwargs)
[docs] def clean(self, value):
"""Validates that the input can be converted to a list of decimals."""
if not value:
return None
# if any value exists, then add "0" as a placeholder to the remaining
# values.
if isinstance(value, list) and any(value):
for i, item in enumerate(value):
if not item:
value[i] = '0'
return super(MultipleDecimalField, self).clean(value)
def compress(self, data_list):
# This should be formatted to a string 5px 5px 2px 2px
if not data_list:
return None
values = deepcopy(data_list)
for index, value in enumerate(values):
if value and float(value).is_integer():
value = int(value)
if value:
values[index] = '{0}{1}'.format(value, self.value_suffix)
else:
values[index] = '0'
return ' '.join(values)
[docs] def to_python(self, value):
"""Validates that the input can be converted to a list of decimals."""
if not value:
return None
if isinstance(value, list):
for index, position_val in enumerate(value):
val = super(MultipleDecimalField, self).to_python(position_val)
value[index] = val
return value
class ChoiceAndCharField(MultiValueField):
widget = ChoiceAndCharInputWidget
def __init__(self, choices=None, widget_css_class='', *args, **kwargs):
if 'widget' in kwargs:
self.widget = kwargs.pop('widget')
fields = (
ChoiceField(choices=choices, required=False),
CharField(required=False)
)
widget_kwargs = {
'choices': choices
}
if widget_css_class:
css_class = 'choice-and-char-widget {0}'.format(widget_css_class)
widget_kwargs['widget_css_class'] = css_class
widget = self.widget(**widget_kwargs)
super(ChoiceAndCharField, self).__init__(fields=fields,
widget=widget,
*args,
**kwargs)
def compress(self, data_list):
return data_list