Skip to content
Station in the Metro
Menu
  • Voiceover & Voice Acting
  • CV
  • The Optical Podcast
  • Apps and Scripts
    • Crop PDF Shipping Labels
    • Django Internal Links
    • Django MultiRangeField
    • FeedPress Subscribers Status Board Graph Panel
    • Post Atomic Horror Unofficial Episode Guide
    • Markdown Cheat Sheet
    • Proper English Title Caps 2 for iTunes
    • Clean Ripped TV Episodes for iTunes
    • Track Name Clean Parts for iTunes
  • Casio PT-7
    • Photo Galleries
    • Operation Manual
    • Keyboard ribbon cable repair
  • Colophon
Menu

Django MultiRangeField

This post was published more than a few years ago (on 2014-06-01) and may contain broken links, inaccurate information, outmoded thoughts, or cringe takes. Proceed at your own risk.

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.

Mark Boszko

Film & Video Editor, Voiceover Artist, macOS IT Engineer, and Maker

© 2026 Mark Boszko | find Mark elsewhere on the internet