Skip to content

Existing Model Interfaces

ExistingModelInterface (general_manager.interface.ExistingModelInterface) lets a manager wrap an existing Django model without generating new tables. It keeps the GeneralManager API intact—create, update, delete, factories, and history tracking all work the same—while pointing at the tables you already manage elsewhere. When the legacy model exposes an is_active column the interface enables soft delete automatically (delete() toggles the flag).

When to use it

  • You have legacy Django models and want GeneralManager orchestration without migrating data into freshly generated tables.
  • Multiple apps already depend on a model definition and you need the manager layer to stay backward compatible.
  • You want to incrementally adopt managers before refactoring model code.

If you control the schema and prefer GeneralManager to build models for you, stick with DatabaseInterface.

Pointing a manager at a legacy model

Reference the model with either an import, settings.AUTH_USER_MODEL, or an app label string. Annotate the manager attributes you plan to read—GeneralManager derives field accessors from the legacy model instead of from interface field declarations.

from django.conf import settings

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


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

    class Interface(ExistingModelInterface):
        model = settings.AUTH_USER_MODEL

For wrappers that use the same class name as your Django model, keep the Django model in models.py and define the GeneralManager wrapper in managers.py. GeneralManager auto-imports <app>.managers for every installed app during startup before it initializes manager classes, which avoids import cycles and ensures the wrapper class is available for registration and _general_manager_class linking. Import the wrapper from myapp.managers in shells and app code so you do not confuse it with myapp.models.User; if the model is already imported, assign the wrapper class directly to _general_manager_class instead of relying on a string reference.

Auditing and validation

  • create and update assign changed_by_id when the model exposes that column and record history_comment values using django-simple-history.
  • delete toggles is_active (when the column exists) and appends " (deactivated)" to the provided history comment; if your legacy model lacks that field the interface performs a hard delete instead. Use filter(include_inactive=True) when you need to surface soft-deleted rows explicitly.
  • Define Meta.rules on the interface to add GeneralManager validation alongside any rules already declared on the model. The interface merges both sets and replaces full_clean() so the rule set runs consistently everywhere.

Writing data through the manager

ExistingModelInterface reuses the same write helpers as DatabaseInterface:

  • update() refreshes the current manager in place and returns that same manager instance. Reassigning the return value is optional now, so customer.update(name="New") immediately makes customer.name reflect the saved value.
  • delete() invalidates the current manager instance for subsequent field reads. This applies to both hard deletes and soft deletes on models with is_active; the row may still exist for include_inactive=True lookups, but the deleted manager object itself should be treated as spent.

  • Pass many-to-many identifiers with the <relation>_id_list convention or provide GeneralManager instances; the interface normalises and applies them after saving the main record.

  • Foreign key assignments accept manager instances or raw IDs.
  • filter, exclude, and all return manager instances backed by the legacy rows. You can query historical states by passing search_date=... to filter()/exclude() or by calling ManagerInterface(search_date=...) the same way you would with generated models.
customer.update(name="Acme International", history_comment="rename")
assert customer.name == "Acme International"

customer.delete(history_comment="manual block")
# `customer` is now invalid for attribute reads even when the row was soft-deleted.

No schema changes are generated by this interface—keep running your migrations and model definitions in the owning app.

Factory support

An AutoFactory subclass is built automatically for each manager. It reuses any attributes you place on an inner Factory definition and populates missing fields using the legacy model metadata. Calling User.Factory.create() returns manager instances, so your tests keep existing fixtures while benefiting from the richer interface surface. Factory-created managers follow the same lifecycle contract as any other manager instance: updates mutate the current manager object in place, and deletes invalidate that object for later field reads.

Because history tracking is still attached to the underlying legacy row, in-place updates do not change audit behavior. You still get the same changed_by_id and history_comment entries as before; only the in-memory manager identity contract changed.

Looking for sample code? See the Existing model interface cookbook recipes for end-to-end snippets you can drop into your project.