Skip to content

Existing Model Interface Recipes

These examples show how to adopt ExistingModelInterface for legacy tables while keeping the GeneralManager ergonomics.

Wrapping a swappable Django User model

Keep the Django auth model in models.py and expose the GeneralManager wrapper from managers.py. That lets both classes be named User while giving shells and application code a canonical manager import path.

from django.conf import settings

from general_manager.interface import ExistingModelInterface
from general_manager.manager import GeneralManager, graph_ql_property


class User(GeneralManager):
    id: int
    username: str
    email: str | None

    class Interface(ExistingModelInterface):
        model = settings.AUTH_USER_MODEL

    class Factory:
        username = "legacy-user"
        email = "legacy@example.com"
        password = "not-hashed"

    @graph_ql_property
    def display_name(self) -> str:
        if self.email:
            return f"{self.username} ({self.email})"
        return self.username

Import the wrapper with from myapp.managers import User; keep myapp.models.User for direct ORM usage only. GeneralManager auto-imports myapp.managers during startup so foreign keys pointing at settings.AUTH_USER_MODEL resolve to the wrapper once the app is loaded. In Django shell sessions, GeneralManager's shell command prefers wrapper import paths over raw model import paths when a model is linked through _general_manager_class, then appends any registered manager classes Django did not already auto-import.

User.create and User.update call through to the configured auth table, User.filter(is_active=True) returns manager instances, and User.Factory.create() seeds users for tests without duplicating schema.

Adding validation to a legacy model

Combine existing business rules with new ones by declaring Meta.rules. The interface merges them and rewires full_clean() so the complete rule set runs everywhere.

from django.db import models

from general_manager.interface import ExistingModelInterface
from general_manager.manager import GeneralManager
from general_manager.rule import Rule


class LegacyProject(models.Model):
    name = models.CharField(max_length=100, unique=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        app_label = "projects"


class UniqueNameRule(Rule):
    """Reject placeholder project names."""

    def evaluate(self, obj: models.Model) -> bool:
        return obj.name != "TBD"

    def get_error_message(self) -> dict[str, list[str]]:
        return {"name": ["tbd_not_allowed"]}


class Project(GeneralManager):
    name: str
    is_active: bool

    class Interface(ExistingModelInterface):
        model = LegacyProject

        class Meta:
            rules = [UniqueNameRule()]

Project.create(name="TBD") now raises a validation error, while historical tracking and activation semantics continue to flow through to LegacyProject. If Django's field validation also reports name, GeneralManager keeps both the Django message and the rule message in the final ValidationError.

Route atomic writes and history to another database

For a legacy model that already uses simple-history, select the same database alias for the interface and a database-aware history tracker:

# models.py
from django.conf import settings
from django.db import models

from general_manager.interface.utils.history import DatabaseAwareHistoricalRecords


class LegacyContract(models.Model):
    title = models.CharField(max_length=120)
    reviewers = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
    history = DatabaseAwareHistoricalRecords(m2m_fields=["reviewers"])


# managers.py
from general_manager.interface import ExistingModelInterface
from general_manager.manager import GeneralManager
from contracts.models import LegacyContract


class Contract(GeneralManager):
    class Interface(ExistingModelInterface):
        model = LegacyContract
        database = "archive"

With an archive entry in DATABASES, this directly usable write commits the row, audit reason, and relation table together on that alias:

from django.contrib.auth import get_user_model

users = get_user_model().objects.using("archive")
reviewer = users.get(pk=41)  # Pre-existing user on the archive database.
lead_reviewer = users.get(pk=42)  # Pre-existing user on the archive database.

contract = Contract.create(
    title="Supply agreement",
    reviewers_id_list=[reviewer.id],
    history_comment="imported",
    ignore_permission=True,
)
contract.update(
    title="Supply agreement v2",
    reviewers_id_list=[lead_reviewer.id],
    history_comment="review reassigned",
    ignore_permission=True,
)

Any validation, save, history-reason, or many-to-many exception rolls the whole ordinary mutation back. Models without pre-existing history are registered automatically. DatabaseAwareHistoricalRecords is the required 0.63.1 helper for a pre-tracked model on a non-default alias, but its implementation-module import is not part of the stable general_manager.interface export registry; pin and test the dependency when using it. See the task guide for rollback handling.