Skip to content

Instantly share code, notes, and snippets.

@AnnaDamm
Forked from eerien/forms.py
Last active June 28, 2021 09:26
Comma Separated Values Form Field for Django. There are CommaSeparatedCharField, CommaSeparatedIntegerField.
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
class MinLengthValidator(validators.MinLengthValidator):
message = _("Ensure this value has at least %(limit_value)d elements (it has %(show_value)d).")
class MaxLengthValidator(validators.MaxLengthValidator):
message = _("Ensure this value has at most %(limit_value)d elements (it has %(show_value)d).")
class CommaSeparatedCharField(forms.Field):
def __init__(self, dedup=True, max_length=None, min_length=None, *args, **kwargs):
self.dedup, self.max_length, self.min_length = dedup, max_length, min_length
super(CommaSeparatedCharField, self).__init__(*args, **kwargs)
if min_length is not None:
self.validators.append(MinLengthValidator(min_length))
if max_length is not None:
self.validators.append(MaxLengthValidator(max_length))
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return []
value = [item.strip() for item in value.split(',') if item.strip()]
if self.dedup:
value = list(sorted(set(value)))
return value
def clean(self, value):
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
class CommaSeparatedIntegerField(forms.Field):
default_error_messages = {
'invalid': 'Enter comma separated numbers only.',
}
def __init__(self, dedup=True, max_length=None, min_length=None, *args, **kwargs):
self.dedup, self.max_length, self.min_length = dedup, max_length, min_length
super(CommaSeparatedIntegerField, self).__init__(*args, **kwargs)
if min_length is not None:
self.validators.append(MinLengthValidator(min_length))
if max_length is not None:
self.validators.append(MaxLengthValidator(max_length))
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return []
try:
value = [int(item.strip()) for item in value.split(',') if item.strip()]
if self.dedup:
value = list(sorted(set(value)))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def clean(self, value):
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase
from forms import CommaSeparatedCharField, CommaSeparatedIntegerField
class CommaSeparatedCharFieldTestCase(SimpleTestCase):
test_class = CommaSeparatedCharField
def test_deduplication(self):
field = self.test_class(dedup=True)
self.assertEqual([
'a', 'b', 'c',
], field.clean('a,b,c,a,b'))
field = self.test_class(dedup=False)
self.assertEqual([
'a', 'b', 'c', 'a', 'b',
], field.clean('a,b,c,a,b'))
def test_validators(self):
field = self.test_class(max_length=6, min_length=3)
with self.assertRaises(ValidationError) as cm:
field.clean('')
self.assertEqual(cm.exception.error_list[0].code, 'required')
for test_data in ['a', 'a,b', 'a,a,a,b,b']:
with self.assertRaises(ValidationError) as cm:
field.clean(test_data)
self.assertEqual(cm.exception.error_list[0].code, 'min_length')
for test_data in ['a,b,c', 'a,b,c,d', 'a,b,c,d,e', 'a,b,c,d,e,f', 'a,b,c,d,e,a,b,c,d,e']:
self.assertIsInstance(field.clean(test_data), list)
for test_data in ['a,b,c,d,e,f,g', 'a,b,c,d,e,f,g,h']:
with self.assertRaises(ValidationError) as cm:
field.clean(test_data)
self.assertEqual(cm.exception.error_list[0].code, 'max_length')
class CommaSeparatedIntegerFieldTestCase(SimpleTestCase):
test_class = CommaSeparatedIntegerField
def test_deduplication(self):
field = self.test_class()
self.assertEqual([
1, 2, 3,
], field.clean('1,2,3,1,2'))
field = self.test_class(dedup=False)
self.assertEqual([
1, 2, 3, 1, 2
], field.clean('1,2,3,1,2'))
def test_with_non_integers(self):
field = self.test_class()
for test_data in ['a', '1,2,3,foo,4,5,6', '2.7']:
with self.assertRaises(ValidationError) as cm:
field.clean(test_data)
self.assertEqual(cm.exception.error_list[0].code, 'invalid')
def test_validators(self):
field = self.test_class(max_length=6, min_length=3)
with self.assertRaises(ValidationError) as cm:
field.clean('')
self.assertEqual(cm.exception.error_list[0].code, 'required')
for test_data in ['1', '1,2', '1,1,1,2,2']:
with self.assertRaises(ValidationError) as cm:
field.clean(test_data)
self.assertEqual(cm.exception.error_list[0].code, 'min_length')
for test_data in ['1,2,3', '1,2,3,4', '1,2,3,4,5', '1,2,3,4,5,6', '1,2,3,4,5,6,1,2,3,4,5']:
self.assertIsInstance(field.clean(test_data), list)
for test_data in ['1,2,3,4,5,6,7', '1,2,3,4,5,6,7,8']:
with self.assertRaises(ValidationError) as cm:
field.clean(test_data)
self.assertEqual(cm.exception.error_list[0].code, 'max_length')
@AnnaDamm
Copy link
Author

Changes compared to forked gist:

  • Added unittests
  • Sorting values when deduplication is active (otherwise unit tests sometimes fail)
  • Added error code to Validation error in IntegerField, when non-integers are given

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment