Created
June 1, 2014 17:24
-
-
Save swizzlevixen/c6c905c38f1f28a97885 to your computer and use it in GitHub Desktop.
Django MultiRangeField and MultiRangeFormField
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__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