__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])