Skip to content

Instantly share code, notes, and snippets.

@swizzlevixen
Created June 1, 2014 17:24
Show Gist options
  • Save swizzlevixen/c6c905c38f1f28a97885 to your computer and use it in GitHub Desktop.
Save swizzlevixen/c6c905c38f1f28a97885 to your computer and use it in GitHub Desktop.
Django MultiRangeField and MultiRangeFormField
__author__ = 'Mark Boszko'
import re
from operator import itemgetter
from itertools import groupby
from django.core.exceptions import ValidationError
from django.db import models
from django.forms import CharField
class MultiRangeField(models.CharField):
# default_validators = [validators.validate_comma_separated_integer_list]
description = "A multi-range of integers (e.g. page numbers 30, 41, 51-57, 68)"
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 1000
kwargs['help_text'] = "Comma-separated pages and page ranges."
super(MultiRangeField, self).__init__(*args, **kwargs)
def get_internal_type(self):
return 'CharField'
def to_python(self, value):
"""
:type value: str
"""
if not value:
return ''
# Validate
if re.match("^[0-9, -]*$", value):
return repack(depack(value))
# else something's wrong.
return value
def get_prep_value(self, value):
if not value:
return ''
return repack(depack(value))
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return repack(depack(value))
def formfield(self, **kwargs):
defaults = {'form_class': MultiRangeFormField}
defaults.update(kwargs)
return super(MultiRangeField, self).formfield(**defaults)
def clean(self, value, model_instance):
if re.match("^[0-9, -]*$", value):
return repack(depack(value))
else:
# Turn all other characters into commas, because it's probably a typo
value = re.sub("[^0-9, -]", ",", value)
return repack(depack(value))
class MultiRangeFormField(CharField):
def validate(self, value):
"""
Check if the value consts of valid page ranges,
with only numbers, hyphens, commas, and spaces
:param value:str
:return:
"""
if re.match("^[0-9, -]*$", value):
return repack(depack(value))
# Comment this out if you'd rather just have it auto-clean your entry
else:
raise ValidationError('Can only contain numbers, hyphens, commas, and spaces.')
def depack(value):
"""
Unpacks a string representation of integers and ranges into a list of ints
:type value: str
"""
page_list = []
# Strip out the spaces first, before we depack
value = re.sub("[\s]", '', value)
for part in value.split(','):
if '-' in part:
# It's a range
a, b = part.split('-')
a, b = int(a), int(b)
page_list.extend(range(a, b + 1))
else:
# Make sure that it contains a number before we add it.
if re.match("[0-9]+", part):
a = int(part)
page_list.append(a)
return page_list
def repack(page_list):
"""
Returns a string representation from integers in a list
:type page_list: list
:return: str
"""
# Need to sort the list first, so that we can combine runs into ranges
sorted_values = sorted(page_list, key=int)
ranges = []
for key, group in groupby(enumerate(sorted_values), lambda (index, item): index - item):
group = map(itemgetter(1), group)
if len(group) > 1:
ranges.append(xrange(group[0], group[-1])) # under Python 3.x, switch to "range"
else:
ranges.append(group[0])
ranges_strings = []
for item in ranges:
if isinstance(item, xrange): # This only works under Python 2.x - under 3.x, switch to "range"
# 1-2
range = "%d-%d" % (item[0], item[-1]+1)
ranges_strings.append(range)
else:
ranges_strings.append(str(item))
return ', '.join([unicode(s) for s in ranges_strings])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment