Skip to content

angee.base.mixins

Reusable abstract model mixins for Angee source models.

ARCHIVE_FLAG_FIELD

The one archive-flag column name — the single archive vocabulary word.

Every model that composes :class:ArchiveMixin carries this exact column, and the resource-metadata field classifier recognises the archive flag by this name (angee.data.field_classification.is_archive_field). Keeping the name identical everywhere is the contract that lets pickers default-filter archived rows and lists expose an archived facet without per-model wiring.

TimestampMixin

python
class TimestampMixin(models.Model)

Add conventional creation and update timestamps to a model.

created_at

The timestamp when the row was first created.

updated_at

The timestamp when the row was most recently saved.

Meta

python
class Meta()

Django model options for timestamp-only abstract inheritance.

update_fields_with_auto_now

python
def update_fields_with_auto_now(instance: models.Model,
                                update_fields: Any) -> set[str]

Return non-empty update_fields plus this model's auto_now fields.

SqidMixin

python
class SqidMixin(models.Model)

Add an opaque public identifier backed by the model primary key.

A model sets only the varying fact — its prefix — as sqid_prefix (e.g. sqid_prefix = "nte_"); the shared sqid column reads it (see SqidField.contribute_to_class), so no model re-declares the field.

sqid_prefix

Public-id prefix for sqid (e.g. "nte_"); empty means no prefix.

sqid

Opaque public identifier encoded from the integer primary key.

Meta

python
class Meta()

Django model options for sqid-only abstract inheritance.

public_id_value

python
def public_id_value() -> Any

Return the raw public identifier value for this instance.

public_id_lookup

python
@classmethod
def public_id_lookup(cls, value: str) -> dict[str, Any]

Return the Django lookup for this model's public identifier.

public_id_from_pk

python
@classmethod
def public_id_from_pk(cls, value: Any) -> str

Return the public id encoded from this model's primary-key value.

AuditMixin

python
class AuditMixin(models.Model)

Add conventional user-owned audit foreign keys to a model.

created_by

The user that created the row, when known.

updated_by

The user that most recently updated the row, when known.

Meta

python
class Meta()

Django model options for audit-only abstract inheritance.

save

python
def save(*args: Any, **kwargs: Any) -> None

Persist the row after stamping user audit fields.

ArchiveMixin

python
class ArchiveMixin(models.Model)

Add a soft-archive flag to a model.

One vocabulary, everywhere: the column is is_archived (see :data:ARCHIVE_FLAG_FIELD) and the read scopes are .archived() / .unarchived() (compose :class:ArchiveQuerySet into the model's queryset). Archived rows are soft-hidden from default surfaces but kept for an explicit archived facet — a metadata fact the field classifier carries as archivable, not per-page logic. This is archival, distinct from a soft-delete/trash flag or an enablement flag, which own different contracts.

is_archived

Whether the row is archived — soft-hidden from default pickers and lists.

Meta

python
class Meta()

Django model options for archive-only abstract inheritance.

ArchiveQuerySet

python
class ArchiveQuerySet(models.QuerySet[_ArchiveModelT])

Composable read scopes for the :class:ArchiveMixin archive flag.

Mix into a model's queryset alongside its base queryset (e.g. class DriveQuerySet(ArchiveQuerySet[Drive], AngeeQuerySet[Drive])) so the archive vocabulary — .archived() / .unarchived() — reads as chainable predicates over the one is_archived column rather than repeated inline filters.

archived

python
def archived() -> Self

Return rows flagged archived.

unarchived

python
def unarchived() -> Self

Return rows not flagged archived — the default picker/list scope.

HistoryMixin

python
class HistoryMixin(models.Model)

Mark a model as tracked by django-simple-history.

angee_model_attributes

python
@classmethod
def angee_model_attributes(
    cls, *, app_label: str, model_class: type[models.Model],
    extension_bases: tuple[type[models.Model], ...]
) -> tuple[ModelClassAttribute, ...]

Return the simple-history class attribute for a concrete model.

angee_history_excluded_fields

python
@staticmethod
def angee_history_excluded_fields(
        model_bases: tuple[type[models.Model], ...]) -> list[str]

Return source fields simple-history cannot mirror.

Meta

python
class Meta()

Django model options for history-only abstract inheritance.

RevisionMixin

python
class RevisionMixin(models.Model)

Mark a model as tracked by django-reversion snapshots.

angee_model_decorators

Composer decorators applied to emitted concrete revision models.

revisioned_fields

Model field names registered with django-reversion.

Meta

python
class Meta()

Django model options for revision-only abstract inheritance.

revisions

python
@property
def revisions() -> Any

Return this row's django-reversion versions newest-first.

revert_to

python
def revert_to(version: Any) -> None

Restore declared revisioned fields from version and save.

Saves with update_fields so unrelated in-memory columns are not flushed. The method records its own revert revision so integrity does not depend on the caller's transport opening a reversion block.

HierarchyQuerySet

python
class HierarchyQuerySet(models.QuerySet[_HierarchyModelT])

Subtree read scopes for models composing :class:HierarchyMixin.

Compose alongside the model's base queryset (e.g. class LocationQuerySet(HierarchyQuerySet[Location], AngeeQuerySet[Location])) so the subtree vocabulary — :meth:subtree_of / :meth:ancestors_of — reads as chainable predicates over the maintained path column, served by the prefix index rather than a client-side parent walk.

subtree_of

python
def subtree_of(node: HierarchyMixin) -> Self

Return node and every descendant (INCLUSIVE), by path prefix.

A node's own path is the prefix of every descendant's path and of itself, so a single LIKE 'path%' covers the whole subtree. An unmaterialized node (empty path) matches nothing rather than the whole table.

ancestors_of

python
def ancestors_of(node: HierarchyMixin) -> Self

Return every proper ancestor of node (EXCLUSIVE of node).

HierarchyMixin

python
class HierarchyMixin(models.Model)

Materialized-path tree membership for a self-parented model.

Adds a parent self-FK and a maintained path column of zero-padded, delimiter-terminated primary-key segments (/0000000012/0000000045/), so subtree membership is a prefix test the database serves from an index rather than a fact each addon re-derives by walking parent in the client. The terminal delimiter is the correctness guarantee — a path is a string prefix of another exactly when the first node is an ancestor-or-self of the second — and the zero-padding (see :attr:path_segment_width) keeps segments lexically ordered.

Compose it on a self-parented model, pair it with :class:HierarchyQuerySet for the subtree_of / ancestors_of read scopes, and inherit its Meta so the concrete table carries the prefix index::

class Location(HierarchyMixin, AngeeDataModel):
    ...

    class Meta(HierarchyMixin.Meta):
        abstract = False
        app_label = "inventory"
        rebac_resource_type = "inventory/location"

(Django propagates Meta.indexes only through Meta-class inheritance, not across sibling abstract bases, so a consumer that needs other indexes lists *HierarchyMixin.Meta.indexes alongside its own.)

:meth:save maintains the path: derived from the parent on create, and on reparent it rejects a cycle (a new parent inside the node's own subtree) and a parent in a different scope (any field the model names in :attr:hierarchy_scope_fields), then rewrites the whole subtree's paths in one bulk UPDATE. It owns no REBAC of its own; path maintenance runs unscoped so a reparent reaches descendants the acting user cannot read.

hierarchy_scope_fields

Field names a child must share with its parent (e.g. ("scope",)).

A reparent (and a create under a parent) rejects a parent that differs on any of these fields, so a subtree never straddles a scope boundary. It is a declared contract — generic and iam-free — owned by the consuming model rather than probed by column name: a scoped tree declares hierarchy_scope_fields = ("scope",), an unscoped tree leaves it empty. An FK is compared by its stored id (the field's attname); a parent must agree on every listed field.

parent

The parent node, or NULL for a root; PROTECT keeps a subtree whole.

path

Maintained root-to-self path of padded pk segments; server-owned.

editable=False keeps it out of forms and the auto-CRUD write surface — the mixin is its only writer. The column width bounds tree depth: at :attr:path_segment_width = 12 each segment costs 13 characters, so max_length=255 holds ~19 levels — deeper than any ERP location/category tree, but a consumer expecting deeper nesting must widen the column. Maintenance writes go through queryset update() (the one-UPDATE cascade), so path changes bypass post_save — a HistoryMixin consumer's historical rows do not track path, a derivable server-owned value.

path_segment_width

Zero-pad width for one pk segment.

Governs lexical ordering only; correctness rests on the terminal delimiter, so a primary key wider than this stays correct (it just sorts by raw digits within its level). Twelve digits order rows up to a trillion per table.

PATH_DELIMITER

Segment delimiter; safe because a padded pk segment is digits only.

Meta

python
class Meta()

Abstract options carrying the prefix-serving path index.

from_db

python
@classmethod
def from_db(cls, db: Any, field_names: Sequence[str],
            values: Sequence[Any]) -> Self

Record the loaded parent so :meth:save can detect a reparent.

Only when parent was actually loaded: seeding the baseline off a deferred field (.only(...)/.defer(...) excluding parent) would trigger one extra query per row. :meth:_hierarchy_needs_repath falls back to the live parent_id when the baseline is absent, so a deferred load simply stays lazy.

refresh_from_db

python
def refresh_from_db(using: str | None = None,
                    fields: Sequence[str] | None = None,
                    from_queryset: models.QuerySet[Any] | None = None) -> None

Reload the row, re-syncing the reparent baseline to the loaded parent.

Without the re-sync a refresh after an external parent change leaves the baseline stale, and the next unrelated save() would be misclassified as a reparent.

ancestor_paths

python
def ancestor_paths() -> list[str]

Return the paths of this node's proper ancestors, root-first.

Decomposes this node's own path into the cumulative prefixes at each delimiter boundary, dropping the last (the node itself) — so a root node yields an empty list.

is_within

python
def is_within(other: HierarchyMixin) -> bool

Return whether this node is other or a descendant of other.

The test is intentionally inclusive and query-free: the maintained, delimiter-terminated path column is a prefix of exactly its own subtree. Empty/unmaterialized paths match nothing so they cannot become an accidental whole-tree prefix.

save

python
def save(*args: Any, **kwargs: Any) -> None

Persist the row, maintaining path on create and reparent.

Released under the AGPL-3.0 License.