Login

MultiRangeField and MultiRangeFormField

Author:
bobtiki
Posted:
June 1, 2014
Language:
Python
Version:
1.6
Score:
0 (after 0 ratings)

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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
__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])

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

Please login first before commenting.