django-versioned-models
Drop-in versioning for Django models. Every model that inherits from VersionedModel gets full release management, a data status workflow, and CI integration — automatically.
🚀 Features
- Release management — every row in every table is tagged to a release. Branch, lock, patch, and deprecate with simple commands.
- Data status workflow —
DRAFT → FUTURE → APPROVED. CI only sees approved rows. Live architect edits never break tests. - Lock enforcement — locked releases are fully immutable at the model level. No edits, no deletes — from Admin, API, or shell.
- Soft-delete — rows can be deactivated without deletion. Deactivating an approved row resets it to
DRAFTso it must be re-approved before CI sees it again. - Auto-discovery — inherit
VersionedModeland your model is versioned. No registration needed. - Topological FK sort — models are duplicated in the correct dependency order automatically when branching a release.
- Soft deprecation — old releases are hidden by default but data is always preserved and reversible.
- CI-ready commands —
create_release,approve_release,lock_release, and more, ready to plug into any pipeline.
📦 Installation
pip install django-versioned-models
⚡ Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
...
"django_versioned_models",
]
2. Run migrations
python manage.py migrate
3. Define your versioned models
from django.db import models
from django_versioned_models.mixins import VersionedModel
class Product(VersionedModel):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta(VersionedModel.Meta):
unique_together = [("release", "name")] # unique per release, not globally
Important: always inherit
MetafromVersionedModel.Metato preserve the composite index on(release, status, active).
4. Run migrations for your models
python manage.py makemigrations
python manage.py migrate
5. Create the first release
python manage.py create_release --release-version v1.0.0
6. Add data, then lock
# Add data via Admin, API, or shell — all rows start as DRAFT
python manage.py lock_release --release-version v1.0.0
🔄 Ongoing Workflow
create_release → architects edit (DRAFT) → lock_release → CI approves → lock_release → deploy_release → ship
# Branch from the previous locked release
python manage.py create_release --release-version v1.1.0 --based-on v1.0.0
# Architects add and edit rows (status=DRAFT by default)
# Need a correction? Unlock is allowed before deployment
python manage.py unlock_release --release-version v1.1.0 # interactive
python manage.py unlock_release --release-version v1.1.0 --force # CI / automation
# Lock before CI runs — no more edits after this point
python manage.py lock_release --release-version v1.1.0
# CI approves all stable DRAFT rows (FUTURE rows are left untouched)
python manage.py approve_release --release-version v1.1.0
# Run tests against approved data only
pytest
# Lock again if unlocked for corrections, then deploy
python manage.py lock_release --release-version v1.1.0 # skip if already locked
python manage.py deploy_release --release-version v1.1.0 # requires locked — permanently blocks unlock
# Bug found after deployment? Create a patch — never unlock a deployed release
python manage.py create_release --release-version v1.1.1 --based-on v1.1.0
🔍 How It Works
Every model that inherits from VersionedModel automatically gets:
| Field | Type | Description |
|---|---|---|
release |
FK → Release |
Which release this row belongs to |
status |
CharField | Data readiness: draft / future / approved |
active |
BooleanField | Soft-delete flag. Inactive rows are invisible to for_release() and approved() |
Release state fields:
| Field | Type | Description |
|---|---|---|
is_locked |
BooleanField | Immutable — no edits or deletes allowed |
deployed |
BooleanField | Deployed to production — unlock is permanently blocked |
is_deprecated |
BooleanField | Hidden by default, data preserved |
Status Flow
DRAFT <-> FUTURE -> APPROVED (APPROVED is one-way — CI only)
| Status | Set by | Meaning |
|---|---|---|
DRAFT |
Architects | Being worked on |
FUTURE |
Architects | Planned but not ready |
APPROVED |
CI only | Stable — what tests run against |
Active / Inactive (Soft-Delete)
row.deactivate() # sets active=False, resets APPROVED → DRAFT
row.reactivate() # sets active=True, status stays DRAFT — must re-approve
Deactivating an approved row resets its status to DRAFT, so it must go through the approval flow again before CI can see it.
Lock Enforcement
Locked releases are immutable. Any save() or delete() on a non-approved row raises ValidationError — from Admin, API, or shell:
row.save() # raises ValidationError if release is locked and status != APPROVED
row.delete() # raises ValidationError if release is locked
The only operation permitted on a locked release is approve() — this is how CI stamps rows after locking.
📋 Management Commands
| Command | Description |
|---|---|
create_release --release-version v1.0.0 |
Create a standalone release (unlocked) |
create_release --release-version v1.1.0 --based-on v1.0.0 |
Branch from a locked release — copies all rows |
lock_release --release-version v1.1.0 |
Lock a release (immutable) |
approve_release --release-version v1.1.0 |
Approve all active DRAFT rows (CI only — FUTURE and inactive untouched) |
deploy_release --release-version v1.1.0 |
Mark as deployed — permanently blocks unlock |
unlock_release --release-version v1.1.0 |
Unlock with interactive confirmation (pre-deployment only) |
unlock_release --release-version v1.1.0 --force |
Unlock without prompt — safe for CI/automation (pre-deployment only) |
deprecate_release --release-version v1.0.0 |
Soft-deprecate (data preserved, hidden by default) |
deprecate_release --release-version v1.0.0 --undo |
Restore a deprecated release |
🗄 Querying
from django_versioned_models.models import Release
release = Release.objects.get(version="v1.1.0")
# Active rows only, all statuses — use in architect-facing GUI views
Product.objects.for_release(release)
# Approved + active rows only — use in CI and production logic
Product.objects.approved(release)
# All rows including inactive — use for copy/audit only, not business logic
Product.objects.all_rows(release)
# Filter by status directly
Product.objects.for_release(release).filter(status="future")
📥 Imports Reference
from django_versioned_models.mixins import VersionedModel, DataStatus
from django_versioned_models.models import Release
from django_versioned_models.services import (
create_release,
lock_release,
get_versioned_models,
get_versioned_models_ordered,
)
🧪 Running Tests
uv run pytest
🤝 Contributing
Contributions are welcome! Please fork the repo, create a branch, and submit a pull request.
📄 License
MIT License — see LICENSE for details.
Metadata
Release files for django-versioned-models 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_versioned_models-1.2.0.tar.gz | 12.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_versioned_models-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.0 kB
Release files / django_versioned_models-1.2.0.tar.gz
| Download URL | django_versioned_models-1.2.0.tar.gz |
|---|---|
| Size | 12.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5f40b9fdcbffba73c4f183255e47200f5a4717014ac4acd46bfecad90afafb4
|
|
BLAKE2b-256 checksum How to use checksums |
d489d48abdf7bd9e27878591cdf73fc32fa8005e6062766a06ef6f5a95c765c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / django_versioned_models-1.2.0-py3-none-any.whl
| Download URL | django_versioned_models-1.2.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
44f0895e2c1ce17ec50817c59d55149c7926e72ab55d7b4835eb9556d1b74c4c
|
|
BLAKE2b-256 checksum How to use checksums |
722b5b5d5df9207ae027252783f5e0ac1fdc35a64600a161809fd5b8450b9656
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|