Designed to hold a list of pages and page ranges for a book/magazine index.
A custom model field (and accompanying form field) that saves comma-separated pages and page ranges in human-readable string form. Includes some clean-up code, so that you can add a new page or range at the end of an existing entry, and it will put it in numeric order and combine runs into ranges. So this:
4-33, 43, 45, 60-65, 44, 59
becomes the tidy
4-33, 43-45, 59-65
NOTE: If you comment out the raising of the ValidationError
in the form field's validate() method, it will actually clean up any extraneous characters for you (which could be dangerous, but for me is usually what I want), so even this horrible mess:
;4-33, 46a fads i44 ,p45o
gets cleaned to
4-33, 44-46
*This is the first custom field I've ever written for Django, so may be a little rough but seems to work fine.
__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]) |