Party System Migration

Developer guide for the Enezaframework party system. Part of the Party System series.

Table of Contents

  1. Overview
  2. New Party-Based Models
  3. Converting an Existing Model
  4. The Safe Migration
  5. Verifying the Migration
  6. Migration Decision Tree
  7. 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.

bash
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, ...):

  1. Add PartyModelMixin to the model's base classes (first).
  2. Remove the manual party = ForeignKey(Party, ...) line.
  3. Add the party_category property and, if needed, override get_party_defaults().
  4. Run makemigrations.
  5. Verify the migration does not drop the column — you want an AlterField, not RemoveField + AddField.
  6. If Django generated a remove/add pair, replace it with the manual AlterField below.
python
# 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.

python
# 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_party is the literal mixin value; Django resolves it per concrete model (for Contact, contact_party). Do not replace it with a concrete name in the migration unless you also override party_related_name on the model.


Verifying the Migration

Before applying, confirm:

  1. makemigrations output mentions only AlterField for party — no RemoveField or AddField for party.
  2. The migration's to/related_name match the mixin (to='enezacommon.party', related_name='%(class)s_party').
  3. No other model references the old reverse accessor.
  4. Existing rows keep their party_id values (an AlterField on related_name does 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]