Core - Models Documentation
Location: api/nextango/apps/core/models/base.py (base classes), api/nextango/apps/core/models.py (mixins)
Last Updated: 2026-07-09
Overview
Core defines the model inheritance spine every domain app builds on: a UUID primary key, automatic timestamps, and bidirectional Sanity CMS sync with deterministic, revision-based sync decisions. Two mixins add audit tracking and soft deletion to any model that needs them.
BaseModel
Source: models/base.py:63-70
Abstract base providing a UUID primary key instead of an auto-incrementing integer.
| Field | Type | Description |
|---|---|---|
id | UUIDField | Primary key, auto-generated (uuid.uuid4), not editable |
from nextango.apps.core.models import BaseModel
class MyModel(BaseModel):
name = models.CharField(max_length=255)
TimestampedModel
Source: models/base.py:73-81
Extends BaseModel. Adds automatic creation and update timestamps.
| Field | Type | Description |
|---|---|---|
created_at | DateTimeField | Set once, on creation (auto_now_add) |
updated_at | DateTimeField | Refreshed on every save (auto_now) |
SanityIntegratedModel
Source: models/base.py:84-577
Extends TimestampedModel. Base class for every model that mirrors a Sanity CMS document. Provides sync bookkeeping fields, a deterministic revision-based sync decision, and a P13 deletion-semantics contract that governs how instances may be deleted depending on who is authoring the record.
Fields
| Field | Type | Description |
|---|---|---|
sanity_id | CharField(255) | Sanity document ID, unique, indexed, nullable |
sanity_rev | CharField(255) | Sanity document revision, used for conflict resolution |
last_synced_at | DateTimeField | Timestamp of the last successful sync |
sync_enabled | BooleanField | Whether this record participates in sync (default True) |
sync_status | CharField(20) | synced, out_of_sync, or deleted_from_sanity |
is_draft | BooleanField | Whether this record mirrors a Sanity draft document |
django_revision | PositiveIntegerField | Auto-incremented on meaningful field changes |
last_synced_django_revision | PositiveIntegerField | Revision value at the last successful sync |
A composite index (sync_enabled, django_revision, last_synced_django_revision) supports efficient out-of-sync queries.
Class attributes subclasses override
_outbound_sync_enabled(defaultTrue): set toFalseon models with no Django-to-Sanity outbound sync. Excludes the model from the revision health check._is_sanity_sot(defaultTrue): the P13 deletion-semantics classifier.Truemeans Sanity is the source of truth for this model's lifecycle (deletion must originate from a Sanity webhook; Django admin hard-delete is denied).Falsemeans the model is Django-owned and admin deletion is permitted, subject to dependency checks.
Revision-based sync decision
Sync is needed when django_revision > last_synced_django_revision. save() bumps django_revision automatically whenever a meaningful (non-sync-metadata) field changes, except during inbound webhook processing (SyncContext.is_webhook_processing()) or when settings.SKIP_REVISION_TRACKING is set. New records always bump to revision 1. Existing records use an atomic F('django_revision') + 1 expression, then refresh the field from the database.
product.name = 'Updated Name'
product.save() # django_revision increments; sync_status becomes 'out_of_sync'
if product.needs_sync_to_sanity():
sync_to_sanity(product)
product.mark_synced_to_sanity(sanity_rev='abc123')
# Sync metadata updates alone never bump the revision
product.last_synced_at = timezone.now()
product.save(update_fields=['last_synced_at'])
Sync lifecycle methods
needs_sync_to_sanity():Trueifsync_enabled, notdeleted_from_sanity, anddjango_revision > last_synced_django_revision.get_sync_lag():django_revision - last_synced_django_revision.mark_synced_to_sanity(sanity_rev=None): writes sync bookkeeping via a queryset.update()(bypassespost_save, so it never re-triggers outbound sync signals) and mirrors the change onto the in-memory instance.mark_out_of_sync(): setssync_status='out_of_sync'and saves.mark_deleted_from_sanity(): soft-delete, settingsync_status='deleted_from_sanity'andsync_enabled=False.is_deleted_from_sanity():Trueifsync_status == 'deleted_from_sanity'.published()(classmethod): queryset excluding draft documents (is_draft=False).
should_sync() and mark_synced() remain for backward compatibility; new code uses needs_sync_to_sanity() and mark_synced_to_sanity().
Deletion semantics
SanityIntegratedModel implements the P13 deletion-semantics standard: hard-deleting a record that other rows depend on, or that Sanity still considers live, is a data-integrity hazard. Three pieces cooperate.
has_dependencies() walks the model's reverse relations and returns a DeletionDependencyCheckResult. It reports a blocker for every reverse FK or OneToOne relation using PROTECT, RESTRICT, or DO_NOTHING that has related rows, and for every M2M reverse relation with related rows. CASCADE / SET_NULL / SET_DEFAULT relations are not blockers (Django's delete collector handles those) unless a subclass elevates them via get_custom_deletion_blockers(). Exceptions during discovery propagate rather than being swallowed; an unknown dependency state must never silently allow a hard delete.
get_custom_deletion_blockers() is a subclass override hook (default: empty) for blockers beyond auto-discovered FK/M2M relations, such as active reservations or audit-history rows whose on_delete doesn't naturally block.
safe_delete(*, origin, hard_delete=False) is the origin-aware dispatcher. origin is one of sanity_webhook, django_admin, django_internal, cleanup_worker.
- Sanity-SoT model (
_is_sanity_sot=True) receivingorigin='sanity_webhook': soft-deletes unconditionally (mark_deleted_from_sanity()), skipping the dependency check (the hot webhook path). - Sanity-SoT model receiving
origin='django_admin'or'django_internal': blocked (reason='sanity_gate_required'). Deletion must come from Sanity. - Sanity-SoT model receiving
origin='cleanup_worker'withhard_delete=True: hard-deletes only if there are no dependency blockers and the record is already soft-deleted (is_deleted_from_sanity()isTrue). - Django-owned model (
_is_sanity_sot=False): admin, internal, or cleanup-worker origins may hard-delete whenhard_delete=Trueand there are no dependency blockers.origin='sanity_webhook'is a no-op; a Django-owned model never takes deletion orders from Sanity.
safe_delete() returns a SafeDeleteResult (action: blocked | soft_deleted | hard_deleted | noop, plus reason and the dependency_check). The admin-layer enforcement of this contract is documented in Admin Infrastructure.
AuditableMixin
Source: core/models/__init__.py:13-30
Mixin adding audit-trail actor fields. The live class carries the two FKs only; it does not provide timestamps, so models composing it pair it with TimestampedModel for created_at/updated_at. (A fuller definition exists in the module file core/models.py, but that file is shadowed by the core/models/ package and never imports; the shipped class is the package's fallback definition.)
| Field | Type | Description |
|---|---|---|
created_by | ForeignKey(users.Employee) | Nullable, SET_NULL on delete |
updated_by | ForeignKey(users.Employee) | Nullable, SET_NULL on delete |
SoftDeleteMixin
Source: core/models/__init__.py:31-43
Adds soft-deletion fields. SoftDeleteMixin declares the fields only; it does not override delete(), restore(), or provide manager overrides (objects/all_objects/deleted); a model composing this mixin implements that behavior itself.
| Field | Type | Description |
|---|---|---|
is_deleted | BooleanField | Default False |
deleted_at | DateTimeField | Nullable |
deleted_by | ForeignKey(users.Employee) | Nullable, SET_NULL on delete |
Model Inheritance Pattern
from nextango.apps.core.models import BaseModel, TimestampedModel, SanityIntegratedModel
class Product(SanityIntegratedModel):
"""Inherits UUID PK, timestamps, and Sanity sync/deletion semantics."""
name = models.CharField(max_length=255)
Most Sanity-synced domain models inherit SanityIntegratedModel directly, since it already includes TimestampedModel and BaseModel in its MRO. Pure Django-owned models (no Sanity mirror) inherit TimestampedModel or BaseModel alone and add AuditableMixin / SoftDeleteMixin as needed.
Related Documentation
- Admin Infrastructure for the deletion-guard admin mixin, sync-health admin mixin, and editorial widgets built on these base classes
- Validators
- Fields
- Exceptions