angee.messaging.tracking
Odoo-style field-change tracking for record chatter.
ThreadedModelMixin logs configured field changes into a record's chatter on save. This module owns the generic field-diff mechanism it composes: :class:FieldTracker reads the configured tracked fields, snapshots their pre-save values, diffs them against the post-save values, and renders human displays, emitting :class:TrackingChange rows. Keeping the ~130-line mechanism here (instead of on the mixin) keeps it out of every consumer model's MRO and lets the mixin stay the thin, permission-gated verb owner.
:class:TrackingChange is the tracked old→new row shape authored once, so the mixin's tracker builds it and the message write path persists it from the same shape — the row shape is not declared in the mixin and re-validated again in the manager.
TrackingChange
@dataclass(frozen=True)
class TrackingChange()One tracked field's old→new values, in TrackingValue's row shape.
Emitted by :class:FieldTracker and consumed by the message write path (MessageManager.post_to_thread via _normalise_tracking_value), so the tracked field set is declared in exactly one place.
FieldTracker
class FieldTracker()The field-diff mechanism ThreadedModelMixin composes to track record changes.
Bound to one record instance and its configured thread_tracking_fields; the mixin delegates snapshotting/diffing/rendering here and keeps only the chatter verbs.
snapshot
def snapshot(
update_fields: Iterable[str] | None = None
) -> tuple[dict[str, Any], ...]Return the pre-save old values for the tracked fields, before save.
changes
def changes(
snapshot: tuple[dict[str, Any], ...]) -> tuple[TrackingChange, ...]Return the tracked fields whose value changed since snapshot.
create_changes
def create_changes() -> tuple[TrackingChange, ...]Return the tracked initial (non-default) values for the record's first save.