Skip to content

v2.2.0

Choose a tag to compare

@rroblf01 rroblf01 released this 28 Apr 12:41
· 94 commits to main since this release

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

  • makemigrations now serialises every public field type.
    _FIELD_IMPORTS and _serialize_field were stuck at the 2.0
    set, so a model with an EnumField produced EnumField()
    with no enum class (silently broken migration), and
    DurationField / CITextField / ArrayField /
    GeneratedField / the range family / PositiveSmallIntegerField
    / FileField were missing their import lines (NameError at
    load). The writer now covers every type, recurses into
    ArrayField.base_field / GeneratedField.output_field, and
    emits the user-side from <module> import <Enum> line for
    EnumField references. Round-trip tests in
    tests/test_migration_writer_new_fields.py execute every
    generated file to catch regressions.

Added — Operational tooling

  • dorm doctor audits STORAGES. Warns when the default
    alias is missing, when FileSystemStorage.location is not a
    writable directory, when S3Storage is missing bucket_name
    or has hardcoded access_key / secret_key (a near-universal
    prod red flag), or when endpoint_url uses plain HTTP for a
    non-local host. Adds a note when FileField is in use but
    STORAGES is unset (dorm falls back to ./media).
  • dorm inspectdb recognises 2.2's PG types. INTERVAL
    DurationField, CITEXTCITextField, int4range /
    int8range / numrange / daterange / tstzrange
    the matching RangeField subclass. Projects adopting dorm
    against a pre-existing schema get the right field classes
    instead of the TextField fallback.
  • dorm init settings template includes commented STORAGES
    blocks
    for FileSystemStorage, AWS S3, and S3-compatible
    services (MinIO / R2 / B2) — see cmd_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 a VARCHAR(max_length) holding the
    storage-side name; the Python value is a FieldFile wrapper that
    delegates .url / .size / .open() / .delete() to the
    configured backend. upload_to accepts a static string, a
    strftime template, or a callable
    f(instance, filename) -> str.

  • dorm.storage module: Storage abstract base, default
    FileSystemStorage (local disk, default backend), File /
    ContentFile wrappers, FieldFile (descriptor result),
    get_storage(alias) registry and a default_storage proxy
    that re-resolves on every call. Storage methods come in sync + async
    pairs; async defaults wrap sync via asyncio.to_thread so backends
    with no native async client still work without thread blocking the
    event loop.

  • STORAGES setting — 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 next get_storage() re-reads.

  • dorm.contrib.storage.s3.S3Storage — AWS S3 backend gated
    behind the new s3 extra (pip install 'djanorm[s3]'). Lazy
    boto3 client init, presigned-URL support
    (querystring_auth), CDN / vanity-domain custom_domain,
    configurable default_acl, location prefix, opt-in
    file_overwrite for 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 / delete resolves name against an
    absolute location and refuses any path that escapes the root,
    even if the basename slipped through get_valid_name.

Added — Field types

  • DurationField stores datetime.timedelta. Native INTERVAL
    on PostgreSQL; on SQLite a process-wide sqlite3.register_adapter
    encodes durations as integer microseconds in a BIGINT column so
    the same Python value round-trips on both backends.
  • EnumField(enum_cls) stores an enum.Enum member. Column type
    is derived from the value type (string → VARCHAR, int →
    INTEGER); choices is auto-populated from the enum so admin /
    form layers see every member without restating them in Meta.
  • CITextField — case-insensitive text. Maps to PostgreSQL's
    CITEXT (extension required) and falls back to
    TEXT COLLATE NOCASE on SQLite.
  • Range fieldsIntegerRangeField, BigIntegerRangeField,
    DecimalRangeField, DateRangeField, DateTimeRangeField.
    The Python value is dorm.Range(lower, upper, bounds="[)");
    SQLite raises NotImplementedError from db_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.adelete now route through asend so an
    async def post_save receiver fires from the async path.
  • Sync Signal.send skips coroutine receivers with a WARNING on
    the dorm.signals logger instead of silently dropping them or
    deadlocking on asyncio.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
    single atomic() block; M2M relations restore in a second phase
    after all parent rows land. save() and signals are bypassed for
    performance — use Model.save() when you do want pre-save hooks
    to fire.
  • dorm.serialize module exposes the same operations
    programmatically: serialize, dumps, deserialize and
    load.

Changed

  • SQLQuery._compile_leaf now routes the bound value through the
    resolved field's get_db_prep_value
    before reaching the cursor.
    Custom field types (EnumField, DurationField,
    RangeField …) bind in their wire form rather than as opaque
    Python objects. __in lookups coerce element-by-element; lookups
    whose value is structural (isnull, range, regex) bypass
    the coercion as before.
  • dorm sql --all skips models whose fields have no SQL on the
    active backend
    (typical case: a RangeField while introspecting
    against SQLite). The skip is reported on stderr; previously the
    whole dump aborted on the first incompatible model.