Party System Migration
Developer guide for the Enezaframework party system. Part of the Party System series.
Table of Contents
- Overview
- New Party-Based Models
- Converting an Existing Model
- The Safe Migration
- Verifying the Migration
- Migration Decision Tree
- Related Guides
Overview
Adding PartyModelMixin to a new model is a normal migration. Converting an existing model that already has a party column needs care: Django may generate a destructive-looking RemoveField + AddField pair when all you actually changed is the field's related_name. This guide shows how to keep the column and apply a metadata-only change.
New Party-Based Models
When creating a new model with PartyModelMixin, run migrations normally. Django creates the party_id column from the abstract model's field.
python manage.py makemigrations myapp
python manage.py migrate
No manual migration editing is required.
Converting an Existing Model
Follow these steps for a model that already has a manually declared party = models.ForeignKey(Party, ...):
- Add
PartyModelMixinto the model's base classes (first). - Remove the manual
party = ForeignKey(Party, ...)line. - Add the
party_categoryproperty and, if needed, overrideget_party_defaults(). - Run
makemigrations. - Verify the migration does not drop the column — you want an
AlterField, notRemoveField+AddField. - If Django generated a remove/add pair, replace it with the manual
AlterFieldbelow.
# Before
class Contact(EnezaMaster):
party = models.ForeignKey(
'enezacommon.Party',
null=True,
blank=True,
on_delete=models.PROTECT,
related_name='master_contacts',
verbose_name="Party",
)
# After
class Contact(PartyModelMixin, EnezaMaster):
@property
def party_category(self):
return self.contact_type
The mixin supplies the field with related_name='%(class)s_party'. The old reverse accessor master_contacts changes to contact_party. If code references the old name, update it in the same change.
The Safe Migration
Replace any generated remove/add pair with a single AlterField. This is metadata-only — no SQL is executed for the column.
# myapp/migrations/00XX_contact_party_related_name.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('myapp', '00XX_previous'),
]
operations = [
migrations.AlterField(
model_name='contact',
name='party',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='%(class)s_party',
to='enezacommon.party',
verbose_name='Party',
),
),
]
%(class)s_partyis the literal mixin value; Django resolves it per concrete model (forContact,contact_party). Do not replace it with a concrete name in the migration unless you also overrideparty_related_nameon the model.
Verifying the Migration
Before applying, confirm:
makemigrationsoutput mentions onlyAlterFieldforparty— noRemoveFieldorAddFieldforparty.- The migration's
to/related_namematch the mixin (to='enezacommon.party',related_name='%(class)s_party'). - No other model references the old reverse accessor.
- Existing rows keep their
party_idvalues (anAlterFieldonrelated_namedoes not touch data).
Run migrate only after those checks. Per project rules, the developer/agent does not run or claim test results; the user validates the migrated data.
Migration Decision Tree
flowchart TD
START[Adding party to a model] --> NEW{New or existing?}
NEW -->|New model| AUTO["makemigrations<br/>creates party_id"]
NEW -->|Existing model| ADD["Add mixin, remove manual party FK"]
ADD --> MM["makemigrations"]
MM --> CHECK{"Generated RemoveField<br/>+ AddField?"}
CHECK -->|No, AlterField| OK[Apply as generated]
CHECK -->|Yes| MANUAL["Replace with manual AlterField<br/>related_name='%(class)s_party'"]
MANUAL --> VERIFY["Verify no column drop, no data loss"]
AUTO --> MIGRATE[migrate]
OK --> MIGRATE
VERIFY --> MIGRATE
MIGRATE --> DONE[Done]