Django GenericForeignKey update - django

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()

Related

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

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! 😊

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',
),
]

When is it required to use `schema_editor.connection.alias` in a django `migrations.RunPython` method?

When is it required to use schema_editor.connection.alias in a migrations.RunPython method and why?
When using RunPython in a data migration, django advises that you should use schema_editor.connection.alias:
from django.db import migrations
def forwards_func(apps, schema_editor):
# We get the model from the versioned app registry;
# if we directly import it, it'll be the wrong version
Country = apps.get_model("myapp", "Country")
db_alias = schema_editor.connection.alias
Country.objects.using(db_alias).bulk_create([
Country(name="USA", code="us"),
Country(name="France", code="fr"),
])
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.RunPython(forwards_func, reverse_func),
]
Warning
RunPython does not magically alter the connection of the models for you; any model methods you call will go to the default database unless you give them the current database alias (available from schema_editor.connection.alias, where schema_editor is the second argument to your function).*
But the docs section that describes data migrations, it doesn't mention the alias at all:
from django.db import migrations
def combine_names(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Person = apps.get_model('yourappname', 'Person')
for person in Person.objects.all():
person.name = '%s %s' % (person.first_name, person.last_name)
person.save()
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(combine_names),
]

Wagtail: Filtering a search on a ManyToMany through model

In a Wagtail-based site, I have an ArticlePage model which is related to an Author snippet model in a many-to-many relationship like this:
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.core.models import Orderable, Page
from wagtail.search import index
from wagtail.snippets.models import register_snippet
class ArticlePage(Page):
search_fields = Page.search_fields + [
index.FilterField('author_id'),
]
#register_snippet
class Author(models.Model):
name = models.CharField(max_length=255, blank=False)
class ArticleAuthorRelationship(Orderable, models.Model):
author = models.ForeignKey('Author', on_delete=models.CASCADE, related_name='articles')
page = ParentalKey('ArticlePage', on_delete=models.CASCADE, related_name='authors')
I want to be able to search ArticlePages and filter them by a particular Author, which I can do like this:
author = Author.objects.get(id=1)
articles = ArticlePage.objects.live() \
.filter(authors__author=author) \
.search('football')
But in the dev server logs I get this warning:
articles.ArticlePage: ArticlePage.search_fields contains non-existent field ‘author_id’
My question is: Am I doing this right? It works, but the warning makes me think there might be a more correct way to achieve the same result.
I have also tried using FilterField('authors'), which stops the warning, but I can't work out how to filter on that.
the correct way was explained in the documentation
here an example that is shared in the official documentation:
from wagtail.search import index
class Book(models.Model, index.Indexed):
...
search_fields = [
index.SearchField('title'),
index.FilterField('published_date'),
index.RelatedFields('author', [
index.SearchField('name'),
index.FilterField('date_of_birth'),
]),
]
so in your case, you should write:
class ArticlePage(Page):
search_fields = Page.search_fields + [
index.RelatedFields(
"author", [index.SearchField("id")]
),
]
let me know if it is working

Django - Is this possible that the add default values when model is creating?

I want to add some default values in my database when the related model is creating with makemigrations command.
For example I have this as model;
class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Created Date')
modified_at = models.DateTimeField(auto_now=True, verbose_name='Update Date')
is_deleted = models.BooleanField(default=False, verbose_name='Deleted')
class Meta:
abstract = True
class ModelType(BaseModel):
description = models.CharField(verbose_name='Name', max_length=225 )
and as I said before I want to add some default values ("value1", "value2", "value3", "value4") for my ModelType table. Is that possible?
If you want to always add the default data when you execute a given migration, the safest way is to use a datamigration (as suggested by #Kos).
To create a data migration, use ./manage.py makemigrations <app_label> --empty and manually add the required code to populate the data.
I normally use a custom operation which executes a get_or_create on the specified model. Add this code to either the migration file itself or somewhere where it can be imported from:
from django.db import migrations
def noop(apps, schema_editor):
pass
class EnsureInstanceCreated(migrations.RunPython):
def __init__(self, app_name, model_name, attrs, defaults=None):
super(EnsureInstanceCreated, self).__init__(self.add_instance, noop)
self.app_name = app_name
self.model_name = model_name
self.attrs = attrs
self.defaults = defaults
def add_instance(self, apps, schema_editor):
Model = apps.get_model(self.app_name, self.model_name)
Model.objects.get_or_create(
defaults=self.defaults,
**self.attrs
)
Then, in the migration itself:
from django.db import migrations
from myproject.utils.migrations import EnsureInstanceCreated
class Migration(migrations.Migration):
dependencies = [
('myproject', '000x_auto_...'),
]
operations = [
EnsureInstanceCreated('myapp', 'ModelType', attrs={
'description': 'value1',
}, defaults={
# ...
}),
EnsureInstanceCreated('myapp', 'ModelType', attrs={'description': 'value2'}),
EnsureInstanceCreated('myapp', 'ModelType', {'description': 'value3'}),
]
Apparently there is a way. You can use Fixtures to initialize models with data.
Refer to this piece of documentation: https://docs.djangoproject.com/en/1.10/howto/initial-data/