As long as you aren't adding extra fields to these two models, you can make them into proxy
ones.
class EntertainmentTicket(Ticket):
class Meta:
proxy = True
# some functions and common logic extracted
class MovieTicket(EntertainmentTicket):
class Meta:
proxy = True
# logic unique to MovieTicket
This has no effect on the database and it's just how you interact with them in Django itself
Edit
I completely missed that PolymorphicModel and wasn't aware of it actually making database tables..
Proxy model's wouldn't really fit your existing scheme, so this is what I would do:
1. Rename MovieTicket
to EntertainmentTicket
2. Create a new Model MovieTicket
This could be done in two migrations, but I did makemigrations
after each step and then copied them into 1 & deleted #2
Example End Migration:
# Generated by Django 3.2.4 on 2023-03-27 15:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('child', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='MovieTicket',
new_name='EntertainmentTicket',
),
migrations.CreateModel(
name='MovieTicket',
fields=[
('entertainmentticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='child.entertainmentticket')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('child.entertainmentticket',),
),
]
3. Handle old MovieTicket
objects (now EntertainmentTicket
)
# Create Movie Ticket objects pointing to the new model
for i in EntertainmentTicket.objects.all():
MovieTicket.objects.create(entertainmentticket_ptr_id=i.pk)
I did all this in a test project and it all worked, plus it keeps your PolymorphicModel
thing
Edit 2
ya just had to make it more complicated, didn't you?!- this was horrible! but i did learn alot.
I recommend making a temp project and playing around with it before trying anything in Production.
Starting Models:
from django.db import models
from polymorphic.models import PolymorphicModel
class Ticket(PolymorphicModel):
name = models.CharField(max_length=50)
company = models.CharField(max_length=80)
price = models.CharField(max_length=10)
class MovieTicket(Ticket):
pass
class MarvelTicket(MovieTicket):
pass
class TheatreTicket(Ticket):
pass
Starting Data:
# Note my app was named child
from child.models import *
MoveTicket.objects.create(name='test movie', company='test company', price='$5')
MarvelTicket.objects.create(name='Ant Man', company='Marvel', price='$250')
TheatreTicket.objects.create(name='Singing in the Rain', company='Gene Kelly ', price='$2')
Updated Models:
from django.db import models
from polymorphic.models import PolymorphicModel
class Ticket(PolymorphicModel):
name = models.CharField(max_length=50)
company = models.CharField(max_length=80)
price = models.CharField(max_length=10)
class EntertainmentTicket(Ticket):
pass
class MovieTicket(EntertainmentTicket):
pass
class MarvelTicket(MovieTicket):
pass
class TheatreTicket(EntertainmentTicket):
pass
Migrations
See: Moving a Django Foreign Key to another model while preserving data? for the blueprint of how
You'll need to change the child
in all of the apps.get_model('child', 'Ticket')
calls to match your app
Migration #1. Temp Model, Deleting.
Steps:
- Create Temp Model
- Pack Temp Model
- Remove all downstream affected Models
- One-to-One Primary Keys screw everything up.
- We must delete
marvel
because we must delete movie
- Create new 'top' / Entertainment Model
- Create Entertainment objects
# Generated by Django 3.2.4 on 2023-03-27 23:17
from django.db import migrations, models
import django.db.models.deletion
def create_transfer_objects(apps, schema_editor):
"""
Pack Movie, Theatre and Marvel into the temp model
Structure of temp:
pk = auto-generated number
ticket_type = two char string
ticket_pk = PK for parent Ticket
Explications:
pk, needs a pk
ticket_type, so it knows what model to create with later
ticket_pk, what parent ticket it's associated with
Note Marvel still stores parent ticket and fetch Movie from ticket
I just found this easier instead of adding more temp fields
"""
transfer_model = apps.get_model('child', 'TransferModel')
# create direct -> ticket items
entertainment_models = [
['00', apps.get_model('child', 'MovieTicket')],
['01', apps.get_model('child', 'TheatreTicket')],
]
for t, m in entertainment_models:
for m_obj in m.objects.all():
transfer_model.objects.create(
ticket_type=t,
ticket_pk=m_obj.ticket_ptr.pk,
)
# create passthrough ticket items
# pass through item's pk is still a ticket, just another name
entertainment_models = [
['02', apps.get_model('child', 'MarvelTicket')],
]
for t, m in entertainment_models:
for m_obj in m.objects.all():
if '02':
# use movie ticket
ticket_pk = m_obj.movieticket_ptr.pk
# elif ...
transfer_model.objects.create(
ticket_type=t,
ticket_pk=ticket_pk,
)
def reverse_transfer_objects(apps, schema_editor):
"""
Reverse the process of creating the transfer object,
This will actually create the Movie, Theatre and Marvel objects
"""
transfer_model = apps.get_model('child', 'TransferModel')
ticket_model = apps.get_model('child', 'Ticket')
# reverse direct -> ticket items
target_dict = {
'00': apps.get_model('child', 'MovieTicket'),
'01': apps.get_model('child', 'TheatreTicket'),
}
for obj in transfer_model.objects.filter(ticket_type__in=target_dict.keys()):
target_dict[obj.ticket_type].objects.create(
ticket_ptr=ticket.objects.get(pk=obj.ticket_pk),
)
# reverse passthrough ticket items
# Note: This only does "1 level" below
target_dict = {
'02': {
'obj': apps.get_model('child', 'MarvelTicket'),
'target': apps.get_model('child', 'MovieTicket'),
'field': 'movieticket_ptr',
},
}
for obj in transfer_model.objects.filter(ticket_type__in=target_dict.keys()):
target_dict[obj.ticket_type]['obj'].objects.create(**{
target_dict[obj.ticket_type]['field']: target_dict[obj.ticket_type]['target'].objects.get(pk=obj.ticket_pk),
})
def migrate_to_entertainment(apps, schema_editor):
"""
Create Entertainment objects from tickets
"""
ticket_model = apps.get_model('child', 'Ticket')
entertainmentticket_model = apps.get_model('child', 'EntertainmentTicket')
for ticket_obj in ticket_model.objects.all():
entertainmentticket_model.objects.create(
ticket_ptr=ticket_obj
)
def reverse_entertainment(apps, schema_editor):
# Removing Model should do the trick
pass
class Migration(migrations.Migration):
dependencies = [
('child', '0001_initial'),
]
operations = [
# create a temp model to old values
migrations.CreateModel(
name='TransferModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_type', models.CharField(choices=[('00', 'Movie'), ('01', 'Theatre'), ('02', 'Marvel')], default='00', max_length=2)),
('ticket_pk', models.PositiveIntegerField(default=0)),
],
),
# Create & Pack TransferModels
migrations.RunPython(
# This is called in the forward migration
create_transfer_objects,
# This is called in the backward migration
reverse_code=reverse_transfer_objects
),
# Delete
migrations.DeleteModel(
name='MovieTicket',
),
migrations.DeleteModel(
name='TheatreTicket',
),
migrations.DeleteModel(
name='MarvelTicket',
),
# Create New Model
migrations.CreateModel(
name='EntertainmentTicket',
fields=[
('ticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='child.ticket')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('child.ticket',),
),
# Connect Entertainment to Ticket
migrations.RunPython(
# This is called in the forward migration
migrate_to_entertainment,
# This is called in the backward migration
reverse_code=reverse_entertainment
),
]
Migration #2. Reforming
Steps:
- Recreate deleted Models
- Generate Objects using Temp Model, pointing at new upstream
- Delete Temp Model
# Generated by Django 3.2.4 on 2023-03-28 01:01
from django.db import migrations, models
import django.db.models.deletion
def connect_to_entertainment(apps, schema_editor):
"""
Recreate all the lost models using Temp Model,
Connect through Entertainment instead of Ticket directly.
"""
transfer_model = apps.get_model('child', 'TransferModel')
entertainmentticket_model = apps.get_model('child', 'EntertainmentTicket')
target_dict = {
'00': apps.get_model('child', 'MovieTicket'),
'01': apps.get_model('child', 'TheatreTicket'),
}
for obj in transfer_model.objects.filter(ticket_type__in=target_dict.keys()):
target = entertainmentticket_model.objects.get(
ticket_ptr__pk=obj.ticket_pk,
)
target_dict[obj.ticket_type].objects.create(
entertainmentticket_ptr=target,
)
# Create passthrough ticket items
target_dict = {
'02': {
'obj': apps.get_model('child', 'MarvelTicket'),
'target': apps.get_model('child', 'MovieTicket'),
'field': 'movieticket_ptr',
},
}
for obj in transfer_model.objects.filter(ticket_type__in=target_dict.keys()):
target_dict[obj.ticket_type]['obj'].objects.create(**{
target_dict[obj.ticket_type]['field']: target_dict[obj.ticket_type]['target'].objects.get(
pk=obj.ticket_pk
),
})
def reverse_entertainment_connect(apps, schema_editor):
"""
Recreate Temp Model from Movie, Theatre and Marvel.
"""
transfer_model = apps.get_model('child', 'TransferModel')
entertainment_models = [
['00', apps.get_model('child', 'MovieTicket')],
['01', apps.get_model('child', 'TheatreTicket')],
]
for t, m in entertainment_models:
for m_obj in m.objects.all():
transfer_model.objects.create(
ticket_type=t,
ticket_obj=m_obj.entertainmentticket_ptr.ticket_ptr,
)
# Create passthrough ticket items
entertainment_models = [
['02', apps.get_model('child', 'MarvelTicket')],
]
for t, m in entertainment_models:
for m_obj in m.objects.all():
if '02':
# use movie ticket
ticket_pk = m_obj.movieticket_ptr.pk
# elif ...
transfer_model.objects.create(
ticket_type=t,
ticket_pk=ticket_pk,
)
class Migration(migrations.Migration):
dependencies = [
('child', '0002_transfer'),
]
operations = [
# Create the Previously Removed Models
migrations.CreateModel(
name='MovieTicket',
fields=[
('entertainmentticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='child.entertainmentticket')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('child.entertainmentticket',),
),
migrations.CreateModel(
name='TheatreTicket',
fields=[
('entertainmentticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='child.entertainmentticket')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('child.entertainmentticket',),
),
migrations.CreateModel(
name='MarvelTicket',
fields=[
('movieticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='child.movieticket')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('child.movieticket',),
),
# Recreate
migrations.RunPython(
# This is called in the forward migration
connect_to_entertainment,
# This is called in the backward migration
reverse_code=reverse_entertainment_connect
),
# Delete Temp Model
migrations.DeleteModel(
name='TransferModel',
),
]
Run python manage migrate
Side Notes:
I know this is a drastic change and a lot of stuff, but holy it gets complicated quick!
I tried for the longest time to keep the original models and just repoint or rename, but because they were One-to-One
and Primary Keys
it was doomed from the start.
- You can't really redo primary keys, or at least I couldn't find a way.
- As long as that One-to-One existed, I couldn't create the
EntertainmentTicket
connection.
I also tried doing it in a single migration, but Django didn't like the immediate recreation of exact models. it was almost as if they weren't forgotten yet.
If you had another middle
like model like:
class Ticket(PolymorphicModel):
pass
class SpeakerTicket(Ticket): # <- like this
pass
class MovieTicket(Ticket):
pass
# etc..
All you'd have to do is filter out those items when creating the EntertainmentTicket
objects in the migrations
# Migrations #1
def migrate_to_entertainment(apps, schema_editor):
"""
Create Entertainment objects from tickets
"""
ticket_model = apps.get_model('child', 'Ticket')
entertainmentticket_model = apps.get_model('child', 'EntertainmentTicket')
ticket_object_list = ticket_model.objects.all()
speaker_model = apps.get_model('child', 'SpeakerTicket')
ticket_object_list = ticket_object_list.exclude(
pk__in=speaker_model.objects.all().values_list('ticket_ptr')
)
# .. exclude another, etc
for ticket_obj in ticket_object_list:
entertainmentticket_model.objects.create(
ticket_ptr=ticket_obj
)