Django GenericForeignKey update

2.8k Views Asked by At

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

There are 2 best solutions below

6
On

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

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