v2.2.0
The 2.2 release adds file storage as a first-class concern of the
ORM (FieldField + pluggable backends), four field types that 2.1
left on the roadmap, async signal receivers, and JSON fixture
loading. Every feature ships against both SQLite and PostgreSQL; the
S3 backend is exercised end-to-end against MinIO in CI.
Fixed — Migration writer
makemigrationsnow serialises every public field type.
_FIELD_IMPORTSand_serialize_fieldwere stuck at the 2.0
set, so a model with anEnumFieldproducedEnumField()
with no enum class (silently broken migration), and
DurationField/CITextField/ArrayField/
GeneratedField/ the range family /PositiveSmallIntegerField
/FileFieldwere missing their import lines (NameErrorat
load). The writer now covers every type, recurses into
ArrayField.base_field/GeneratedField.output_field, and
emits the user-sidefrom <module> import <Enum>line for
EnumFieldreferences. Round-trip tests in
tests/test_migration_writer_new_fields.pyexecute every
generated file to catch regressions.
Added — Operational tooling
dorm doctorauditsSTORAGES. Warns when thedefault
alias is missing, whenFileSystemStorage.locationis not a
writable directory, whenS3Storageis missingbucket_name
or has hardcodedaccess_key/secret_key(a near-universal
prod red flag), or whenendpoint_urluses plain HTTP for a
non-local host. Adds a note whenFileFieldis in use but
STORAGESis unset (dorm falls back to./media).dorm inspectdbrecognises 2.2's PG types.INTERVAL→
DurationField,CITEXT→CITextField,int4range/
int8range/numrange/daterange/tstzrange→
the matchingRangeFieldsubclass. Projects adopting dorm
against a pre-existing schema get the right field classes
instead of theTextFieldfallback.dorm initsettings template includes commentedSTORAGES
blocks forFileSystemStorage, AWS S3, and S3-compatible
services (MinIO / R2 / B2) — seecmd_init.
Changed — Public API
Field.uses_class_descriptor(renamed from
_uses_class_descriptor) is now a documented opt-in for custom
field subclasses that install themselves as class-level
descriptors and need__set__to fire on assignment. The leading
underscore signalled "private", but third-party fields with the
same need (encryption, audit, lazy resolution) want the same hook —
the rename promotes it to a stable extension point.FileField
is the canonical built-in user.
Added — File storage
-
FileField(upload_to=, storage=, max_length=)— pluggable file
storage. The column is aVARCHAR(max_length)holding the
storage-side name; the Python value is aFieldFilewrapper that
delegates.url/.size/.open()/.delete()to the
configured backend.upload_toaccepts a static string, a
strftimetemplate, or a callable
f(instance, filename) -> str. -
dorm.storagemodule:Storageabstract base, default
FileSystemStorage(local disk, default backend),File/
ContentFilewrappers,FieldFile(descriptor result),
get_storage(alias)registry and adefault_storageproxy
that re-resolves on every call. Storage methods come in sync + async
pairs; async defaults wrap sync viaasyncio.to_threadso backends
with no native async client still work without thread blocking the
event loop. -
STORAGESsetting — multi-alias config that mirrors
DATABASES::STORAGES = { "default": { "BACKEND": "dorm.storage.FileSystemStorage", "OPTIONS": {"location": "/var/app/media", "base_url": "/media/"}, }, }dorm.configure(STORAGES=...)invalidates the storage cache so
the nextget_storage()re-reads. -
dorm.contrib.storage.s3.S3Storage— AWS S3 backend gated
behind the news3extra (pip install 'djanorm[s3]'). Lazy
boto3client init, presigned-URL support
(querystring_auth), CDN / vanity-domaincustom_domain,
configurabledefault_acl,locationprefix, opt-in
file_overwritefor content-addressed layouts. Works with any
S3-compatible service (MinIO, Cloudflare R2, Backblaze B2) via
endpoint_url=. -
Path-traversal hardening in
FileSystemStorage: every
save/open/deleteresolvesnameagainst an
absolutelocationand refuses any path that escapes the root,
even if the basename slipped throughget_valid_name.
Added — Field types
DurationFieldstoresdatetime.timedelta. NativeINTERVAL
on PostgreSQL; on SQLite a process-widesqlite3.register_adapter
encodes durations as integer microseconds in aBIGINTcolumn so
the same Python value round-trips on both backends.EnumField(enum_cls)stores anenum.Enummember. Column type
is derived from the value type (string →VARCHAR, int →
INTEGER);choicesis auto-populated from the enum so admin /
form layers see every member without restating them inMeta.CITextField— case-insensitive text. Maps to PostgreSQL's
CITEXT(extension required) and falls back to
TEXT COLLATE NOCASEon SQLite.- Range fields —
IntegerRangeField,BigIntegerRangeField,
DecimalRangeField,DateRangeField,DateTimeRangeField.
The Python value isdorm.Range(lower, upper, bounds="[)");
SQLite raisesNotImplementedErrorfromdb_type()so the
limitation surfaces at migrate time, not at first query.
Added — Async signals
Signal.asend(...)— async dispatch entry point. Awaits
coroutine receivers sequentially (in the order they were connected)
and calls sync receivers directly.Model.asave/
Model.adeletenow route throughasendso an
async def post_savereceiver fires from the async path.- Sync
Signal.sendskips coroutine receivers with aWARNINGon
thedorm.signalslogger instead of silently dropping them or
deadlocking onasyncio.run. Connect the same receiver and dorm
picks the right dispatch automatically based on whether the caller
used the sync or async ORM path.
Added — Fixtures CLI
dorm dumpdata— serialise model rows to JSON. Format mirrors
Django's ({model, pk, fields}records); FKs as the target's PK,
M2M as a list of related PKs. Custom envelopes preserve types JSON
can't represent natively (decimals, UUIDs, datetimes, durations,
ranges, bytes).dorm loaddata— load JSON fixtures back. Each file runs in a
singleatomic()block; M2M relations restore in a second phase
after all parent rows land.save()and signals are bypassed for
performance — useModel.save()when you do want pre-save hooks
to fire.dorm.serializemodule exposes the same operations
programmatically:serialize,dumps,deserializeand
load.
Changed
SQLQuery._compile_leafnow routes the bound value through the
resolved field'sget_db_prep_valuebefore reaching the cursor.
Custom field types (EnumField,DurationField,
RangeField…) bind in their wire form rather than as opaque
Python objects.__inlookups coerce element-by-element; lookups
whose value is structural (isnull,range,regex) bypass
the coercion as before.dorm sql --allskips models whose fields have no SQL on the
active backend (typical case: aRangeFieldwhile introspecting
against SQLite). The skip is reported on stderr; previously the
whole dump aborted on the first incompatible model.