Django MultiRangeField

Designed to hold a list of pages and page ranges for a book/magazine index.

Originally published 2014-06-01

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

Update 2026-06-26

Modernized for Django 3.2+ / 5.x + Python 3, and added a list-of-ints accessor.

  • Removed the Python-2 / pre-1.10 leftovers: __metaclass__ = SubfieldBase, super(Class, self), and _get_val_from_obj (now value_from_object in value_to_string).
  • Added deconstruct() so the field works with migrations.
  • New to_list(value) plus a generated <field>_list property on the model — the pages expanded to a list of ints, for matching (page in obj.pages_list).
  • first_page() returns 0 on empty input instead of raising IndexError.
  • depack() now tolerates reversed ranges (53-51) and skips malformed parts (12-13-15, letters) instead of raising.
  • Centralized normalization in one helper; the form field rejects stray characters with a clear error while the model stays lenient.
  • Dropped the obsolete from_db_value context arg (removed in Django 2.0); precompiled the validation regex.

The repack consecutive-run grouping (groupby on value − index) is unchanged.