How can I undo this (accidental) table inheritance in Django? [duplicate] - django

My current project uses multi-table inheritance models:
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
class Cinema(Place):
sells_tickets = models.BooleanField(default=False)
sells_popcorn = models.BooleanField(default=False)
I want to switch to abstract base classes instead. Since my model is already deployed I need to write some custom migrations to convert the above schema to this one:
from django.db import models
class AbstractPlace(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Meta:
abstract = True
class Restaurant(AbstractPlace):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
class Cinema(AbstractPlace):
sells_tickets = models.BooleanField(default=False)
sells_popcorn = models.BooleanField(default=False)
Does anyone have any advice on the steps to take to achieve this?

I recently tackled this exact problem, which I solved by writing and running the migration in the code block below - loosely translated to fit the models in your case.
I'm pretty sure that it's not possible to alter the tables of the old Restaurant and Cinema models directly, as if you try to add fields to them, they will collide with the existing fields of the base model, and if you try to "decouple" the derived models from the base model by e.g. by manually setting abstract=True in the base model's options, Django reports that it's unable to find the base models of Restaurant and Cinema. (These issues might be caused by a bug, for all I know.) To circumvent this problem, I created new tables for the derived models, copied the data from the old tables to the new ones, deleted the old tables, and renamed the new tables to match the names of the old ones.
I got large parts of the code below from code generated by Django, which can be reproduced by creating a temporary migration (before creating one with the code below) which only deletes Restaurant, Cinema and Place, running makemigrations, and copying the CreateModel()s and AlterField()s (for related fields pointing to Restaurant or Cinema) from the generated migration.
For the record, I'm using Django 3.1.4.
from django.db import migrations, models
def copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp(apps, schema_editor):
Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
Restaurant = apps.get_model('<app name>', 'Restaurant')
Cinema = apps.get_model('<app name>', 'Cinema')
# The `_meta.fields` list includes the PK
copy_objects_from_old_model_to_new_model(Restaurant, Restaurant_Tmp, Restaurant_Tmp._meta.fields)
copy_objects_from_old_model_to_new_model(Cinema, Cinema_Tmp, Cinema_Tmp._meta.fields)
def copy_objects_from_old_model_to_new_model(old_model, new_model, fields_to_copy):
field_names = [field.name for field in fields_to_copy]
for old_obj in old_model.objects.all():
old_obj_field_dict = {
field_name: getattr(old_obj, field_name)
for field_name in field_names
}
new_model.objects.create(**old_obj_field_dict)
def copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema(apps, schema_editor):
Restaurant_Tmp = apps.get_model('<app name>', 'Restaurant_Tmp')
Cinema_Tmp = apps.get_model('<app name>', 'Cinema_Tmp')
Restaurant = apps.get_model('<app name>', 'Restaurant')
Cinema = apps.get_model('<app name>', 'Cinema')
copy_objects_from_old_model_to_new_model(Restaurant_Tmp, Restaurant, Restaurant_Tmp._meta.fields)
copy_objects_from_old_model_to_new_model(Cinema_Tmp, Cinema, Cinema_Tmp._meta.fields)
class Migration(migrations.Migration):
dependencies = [
('<app name>', '<last migration>'),
]
operations = [
migrations.CreateModel(
name='Restaurant_Tmp',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('address', models.CharField(max_length=80)),
('serves_hot_dogs', models.BooleanField(default=False)),
('serves_pizza', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Cinema_Tmp',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('address', models.CharField(max_length=80)),
('sells_tickets', models.BooleanField(default=False)),
('sells_popcorn', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.RunPython(copy_objects_from_restaurant_and_cinema_to_restaurant_tmp_and_cinema_tmp, migrations.RunPython.noop),
# Update foreign keys to reference the non-abstract models directly,
# instead of through the (automatically generated) `place_ptr` field of the old models
<
Run `migrations.AlterField()` here for each related field (like ForeignKey) of other models that point to Restaurant or Cinema,
but change their `to` argument from e.g. `<app name>.restaurant` to `<app name>.restaurant_tmp`
>
migrations.RunPython(migrations.RunPython.noop, copy_objects_from_restaurant_tmp_and_cinema_tmp_to_restaurant_and_cinema),
migrations.DeleteModel(
name='Restaurant',
),
migrations.DeleteModel(
name='Cinema',
),
migrations.DeleteModel(
name='Place',
),
migrations.RenameModel(
old_name='Restaurant_Tmp',
new_name='Restaurant',
),
migrations.RenameModel(
old_name='Cinema_Tmp',
new_name='Cinema',
),
]
Note that the migration I originally wrote was only tested to work using SQLite; other database management systems might not accept such a large variety of migration operations, and you might have to split it into multiple migrations. (I'm somewhat unsure what exactly could cause this problem, but I can recall that I've experienced it with PostgreSQL.)
Please let me know if this solves your problem! 😊

Related

Django incomplete migration of table with multiple foreign keys

Django version: 4.1.2
After heaving the following table defined in the model:
class Tasks(models.Model):
name_text = models.CharField(max_length=200)
duration_int = models.IntegerField(default=1)
...
the next two tables have been defined:
class Metiers(models.Model):
name_text = models.CharField(max_length=50)
...
class TasksProperties(models.Model):
task = models.ForeignKey(Tasks, on_delete=models.CASCADE, related_name='task_relation')
metier = models.ForeignKey(Metiers, on_delete=models.CASCADE, related_name='metier_relation')
...
doing the migration, the metier is not created inside the SQL table, but the rest:
class Migration(migrations.Migration):
dependencies = [
('simo', '0009_alter_specialdays_day_date'),
]
operations = [
migrations.CreateModel(
name='Metiers',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name_text', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TasksProperties',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('workload_mh', models.IntegerField(default=8)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='simo.tasks')),
],
),
]
Is there any reason, why metier is not taken into account?
Update on 27/11/2022:
So, it still not clear why it was not working because I believe the structure is correct; therefore I did some try-on-errors and finally the migration is complete, although the reason is not obvious. See below the resolution:
Step 1) Remove the migrated TaskProperties class from models and do a migration
Step 2) models.py was updated as
class TasksProperties(models.Model):
workload_mh = models.IntegerField(default=8)
metier = models.ForeignKey('Metiers', on_delete=models.CASCADE, related_name='metier_relation')
It resulted that the "metier" as foreignkey was visible in SQL after the migration.
Step 3) Adding the "task" as well, the migration dropped the following question:
class TasksProperties(models.Model):
workload_mh = models.IntegerField(default=8)
metier = models.ForeignKey('Metiers', on_delete=models.CASCADE, related_name='metier_relation')
task = models.ForeignKey(Tasks, on_delete=models.CASCADE, related_name='task_relation')
*It is impossible to add a non-nullable field 'task' to tasksproperties without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
Provide a one-off default now (will be set on all existing rows with a null value for this column)
Quit and manually define a default value in models.py.
Select an option: 2*
Then after updating the model.py adding default value default=0 to "task" the migration was successful:
class TasksProperties(models.Model):
workload_mh = models.IntegerField(default=8)
metier = models.ForeignKey('Metiers', on_delete=models.CASCADE, related_name='metier_relation')
task = models.ForeignKey(Tasks, on_delete=models.CASCADE, related_name='task_relation', default=0)
Why is this "defualt" value is needed?
Try these three commands:
python manage.py makemigrations appname
python manage.py sqlmigrate appname 0001
python manage.py migrate
And see if it solves

preserve data while making changes to fields of model in django

I have two models which are Contact and UpdateInfo. Update model is a foreignkey relationship with Contact like below:
class Contacts(models.Model):
full_name = models.CharField(max_length=100, blank=True)
notes = RichTextField(blank=True)
/............./
class UpdateInfo(models.Model):
contacts = models.ForeignKey(Contacts,on_delete=models.CASCADE, related_name='update_info')
updated_at = models.DateTimeField(auto_now=True)
modified_by = models.CharField(max_length=100, blank=True)
Now what I need is to put the notes field into UpdateInfo model and remove from Contacts model. Because now the businees requirement has changed and what they need is they need to add notes in the existing notes and want to see them who added the notes and when they added it with the notes appended.
But the problem is if I remove the notes field from Contacts and add it to Updateinfo model , there are many data into the production and the notes field data will be lost.
Now how can we run migrate in such a way that I add notes field into the Updatinfo model and also migrating the data at the same time.?? Is this what is done by dev ops or we should do it?? Because usually dev ops are migrating the data to the production server from staging area.
I need expert opinion on this.
You can create two migration files: 0002_add_notes_to_updateinfo.py and 0003_remove_notes_from_contact.py the first one will add the field to UpdateInfo model and then execute a function to migrate old data, after that in the second migration file you can remove the old field.
0002_add_notes_to_updateinfo.py:
from django.db import migrations
def migrate_old_data(apps, schema_editor):
Contacts = apps.get_model('yourappname', 'Contacts')
UpdateInfo = apps.get_model('yourappname', 'UpdateInfo')
for contacts in Contacts.objects.all():
UpdateInfo.objects.filter(contacts=contacts).update(notes=contacts.notes)
class Migration(migrations.Migration):
dependencies = [
#dependencies here,
]
operations = [
migrations.AddField(
model_name='updateinfo',
name='notes',
field=RichTextField(blank=True),
preserve_default=False,
),
migrations.RunPython(migrate_old_data),
]
0003_remove_notes_from_contact.py:
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('yourapp', '0002_add_notes_to_updateinfo'),
]
operations = [
migrations.RemoveField(
model_name='contacts',
name='notes',
),
]

could not create unique index "credits_credit_pkey"

Initially, I had a model of Credit. But after adding models of Hypothec and AutoCredit with similar functionality, I realized that I needed to make a base model and inherit from it.
When I tried makemigrations, I received a question:
You are trying to add a non-nullable field 'abstractbaseproduct_ptr' to `credit` without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
*I entered 1; 1. Then I got this question:
It is not clear what the 'abstractbaseproduct_ptr' field is?
Then i got
You are trying to add a non-nullable field abstractbaseaction_ptr to creditaction without a default; we can't do that (the database needs something to populate existing rows).
Again introduced 1; 1.
When I try to migrate, I get
django.db.utils.IntegrityError: could not create unique index "credits_credit_pkey"
DETAIL: Key (abstractbaseproduct_ptr_id) = (1) is duplicated.
Such questions arose only with the Credit model. Apparently, because there is already data in this table...
How should I fix this?
class AbstractBaseProduct(models.Model):
bank = models.ForeignKey('banks.Bank', verbose_name=_('bank'))
#other fields
class AbstractBaseAction(models.Model):
name = models.CharField(_('name'), max_length=255)
short_description = models.CharField(_('short description'), max_length=255)
full_description = models.TextField(_('full description'), blank=True, null=True)
class Credit(AbstractBaseProduct):
class Meta:
verbose_name = _('credit')
verbose_name_plural = _('Credits')
class CreditAction(AbstractBaseAction):
credit = models.ForeignKey(Credit, verbose_name=_('credit'))
migration
migrations.AddField(
model_name='credit',
name='abstractbaseproduct_ptr',
field=models.OneToOneField(auto_created=True, default=1, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='credits.AbstractBaseProduct'),
preserve_default=False,
),
migrations.AddField(
model_name='creditaction',
name='abstractbaseaction_ptr',
field=models.OneToOneField(auto_created=True, default=1, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='credits.AbstractBaseAction'),
preserve_default=False,
),
You have named your model AbstractBaseProduct, but you haven't added abstract = True to the model's Meta class, so Django thinks you want multi-table inheritance instead of abstract base classes.
See the docs on model inheritance for more info.

Django GenericForeignKey update

I am trying to convert a ForeignKey to GenericForeignKey in django. I plan to do this in three migrations, mig1, mig2, mig3.
Migration 1 (mig1) has the following code
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('post_service', '0008_auto_20180802_1112'),
]
operations = [
migrations.AddField(
model_name='comment',
name='content_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AddField(
model_name='comment',
name='object_id',
field=models.PositiveIntegerField(null=True),
),
]
Migration 2 (mig2) has the following code
def change_posts_to_generic_key_comment(apps, schema_editor):
Comment = apps.get_model('post_service', 'Comment')
db_alias = schema_editor.connection.alias
comments = Comment.objects.using(db_alias).all()
for comment in comments:
Comment.objects.filter(id=comment.id).update(content_object=comment.post)
def reverse_change_posts_to_generic_key_comment(apps, schema_editor):
Comment = apps.get_model('post_service', 'Comment')
db_alias = schema_editor.connection.alias
comments = Comment.objects.using(db_alias).all()
for comment in comments:
Comment.objects.filter(id=comment.id).update(content_object=)
class Migration(migrations.Migration):
dependencies = [
('post_service', '0009_auto_20180802_1623'),
]
operations = [
migrations.RunPython(change_posts_to_generic_key_comment, reverse_change_posts_to_generic_key_comment),
]
I tried to use both update and direct assignment of object
comment.content_object = content.post followed by comment.save()
none of them seems to work. How do i update generic foreign key field.
One method is to manually set content_type and object_id. Is there any better way of doing this?
EDIT: Comment Model
class Comment(models.Model):
post = models.ForeignKey(Post,on_delete=models.CASCADE)
# Fields for generic relation
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey()
I had problems updating generic foreign keys in the migration. If I tried setting the key directly to the object, it did not set anything on save (because it did not recognise the GenericForeignKey), and if I tried setting content_type and object_id I got errors about content_type must be set to ContentType (which was what I was doing).
In the end I created a management command which was run from the migration as follows:
from django.db import migrations, models
import django.db.models.deletion
from django.core.management import call_command
def populate_discounts(apps, schema_editor):
"""
I could not get special to update as a generic foriegn key in the
migration. Do it as a one off management command
"""
call_command('initialise_credit_specials')
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('money', '0068_auto_20190123_0147'),
]
operations = [
migrations.AddField(
model_name='invoicecredit',
name='special_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='invoicecredit',
name='special_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType'),
),
migrations.RunPython(populate_discounts)
]
and my management command was pretty simple:
from django.core.management.base import CommandError, BaseCommand
from money.models import InvoiceCredit, PromotionCode, Sale
class Command(BaseCommand):
"""
Report how much we need to refund at the end of the financial year
"""
def handle(self, *args, **options):
print('updating discounts')
# first promotions
for pc in PromotionCode.objects.all():
ics = InvoiceCredit.objects.filter(
desc__contains=pc.code
)
for ic in ics.all():
ic.special = pc
ic.save()
print('invoice credit %d updated with %s' % (ic.id, ic.special.code))
# Then sales
for sale in Sale.objects.all():
ics = InvoiceCredit.objects.filter(
desc__startswith=Sale.desc,
invoice__booking__tour_date__tour=sale.tour_combinations.first().base_tour
)
for ic in ics.all():
ic.special = sale
ic.save()
print('invoice credit %d updated with sale %d' % (ic.id, sale.id))
There's no reason to use filter and update here. You have the object already.
for comment in comments:
comment.content_object = comment.post
comment.save()

Django: Default value in Database, not just ORM

I would like to have a default value for a column in the database, not just the django orm.
Related ticket is in state "wontfix": https://code.djangoproject.com/ticket/470
What is the preferred way to create a default value in the relational database?
In my case it is a BooleanField which should default to FALSE.
I use PostgreSQL, but AFAIK this should not matter in this context.
I solved it like this. The file was created with manage.py makemigrations. I added only the line at the bottom.
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='State',
fields=[
('id', models.CharField(max_length=256, serialize=False, primary_key=True)),
('state', models.BooleanField(default=True)),
],
bases=(models.Model,),
),
### The following line was added
migrations.RunSQL('alter table box_state alter column state set default false')
]
You can do like this:
class MYModel(models.Model):
is_teacher = models.BooleanField(default=False)