4

Here are my simplified models :

from django.contrib.contenttypes.fields import (
    GenericForeignKey, GenericRelation)
from django.db import models
from django.utils.translation import ugettext_lazy as _


class Thing(models.Model):
    '''
    Our 'Thing' class
    with a link (generic relationship) to an abstract config
    '''

    name = models.CharField(
        max_length=128, blank=True,
        verbose_name=_(u'Name of my thing'))

    # Link to our configs
    config_content_type = models.ForeignKey(
        ContentType,
        null=True,
        blank=True)
    config_object_id = models.PositiveIntegerField(
        null=True,
        blank=True)
    config_object = GenericForeignKey(
        'config_content_type',
        'config_object_id')


class Config(models.Model):
    '''
    Base class for custom Configs
    '''
    class Meta:
        abstract = True

    name = models.CharField(
        max_length=128, blank=True,
        verbose_name=_(u'Config Name'))

    thing = GenericRelation(
        Thing,
        related_query_name='config')


class FirstConfig(Config):
    pass


class SecondConfig(Config):
    pass

And Here's the admin:

from django.contrib import admin
from .models import FirstConfig, SecondConfig, Thing


class FirstConfigInline(admin.StackedInline):
    model = FirstConfig


class SecondConfigInline(admin.StackedInline):
    model = SecondConfig


class ThingAdmin(admin.ModelAdmin):
    model = Thing

    def get_inline_instances(self, request, obj=None):
    '''
    Returns our Thing Config inline
    '''
        if obj is not None:
            m_name = obj.config_object._meta.model_name
            if m_name == "firstconfig":
                return [FirstConfigInline(self.model, self.admin_site), ]
            elif m_name == "secondconfig":
                return [SecondConfigInline(self.model, self.admin_site), ]
        return []


admin.site.register(Thing, ThingAdmin)

So far, I've a Thing object with a FirstConfig object linked together. The code is simplified: in an unrelevant part I manage to create my abstract Config at a Thing creation and set the right content_type / object_id.

Now I'd like to see this FirstConfig instance as an inline (FirstConfigInline) in my ThingAdmin.

I tried with the django.contrib.contenttypes.admin.GenericStackedInline, though it does not work with my current models setup.
I tried to play around with the fk_name parameter of my FirstConfigInline.
Also, as you can see, I tried to play around with the 'thing' GenericRelation attribute on my Config Model, without success..

Any idea on how to proceed to correctly setup the admin?

djvg
  • 11,722
  • 5
  • 72
  • 103
ppython
  • 485
  • 6
  • 19

2 Answers2

2

According to the Django Docs you have to define the ct_fk_field and the ct_field if they were changed from the default values. So it may be enough to set ct_field to config_content_type.

Hope it works!

edit: Those values have to be declared in the Inline:

class SecondConfigInline(admin.StackedInline):
    model = SecondConfig
    ct_fk_field = "config_object_id"
    ct_field = "config_content_type"

edit2:

I just realized an error in my assumption. Usually you should declare the Foreignkey on the Inline-model. Depending on the rest of your code you could just remove the generic Foreignkey on Thing+the genericRelation on Config and declare a normal Foreignkey on the Config-Basemodel.

lilaLeon
  • 476
  • 3
  • 5
  • I've already tried to add the `fk_name` (to `FirstConfig.thing.config_content_type.through`), but `AttributeError: 'ReverseGenericManyToOneDescriptor' object has no attribute 'through'`. Also tried to to define a `ct_field`/`ct_fk_field`, but I think this would apply if i'd do a ConfigAdmin, to get my Things Inlines! – ppython Nov 06 '17 at 16:57
  • Did you try adding them to the Inline? – lilaLeon Nov 07 '17 at 21:22
  • Yes, I also tried as you suggested and I get the `ValueError: 'myapp.Config' has no ForeignKey to 'myapp.Thing'.` because the "FK" is on a CT on the Thing and not in my Config. I tried different combination, I feel the answer lies in the 'Reverse GenericRelation' with it's 'related_query_name' but no luck so far for the inline. – ppython Nov 08 '17 at 15:21
1

This question is old, but I'll give it a try anyway.

I think the solution depends on what kind of relation you intend to create between Thing and your Config subclasses.

many-to-one/one-to-many

The way it is currently set up, it looks like a many-to-one relation: each Thing points to a single Config subclass, and many Things can point to the same Config subclass. Due to the generic relation, each Thing can point to a different model (not necessarily a Config subclass, unless you do some extra work).

In this case I guess it would make more sense to put the inline on the admin for the Config. That is, create a GenericStackedInline for Thing (which has the GenericForeignkey), and add the inline to a ConfigAdmin, which you can then use for all Config subclasses. Also see the example below. The generic inline will then automatically set the correct content_type and object_id.

many-to-many

On the other hand, if you are looking for a many-to-many relation between Thing and each Config subclass, then I would move the GenericForeignkey into a separate many-to-many table (lets call it ThingConfigRelation).

A bit of code says more than a thousand words, so let's split up your Thing class as follows:

class Thing(models.Model):
    name = models.CharField(max_length=128)


class ThingConfigRelation(models.Model):
    thing = models.ForeignKey(to=Thing, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, null=True, blank=True,
                                     on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField(null=True, blank=True)
    config_object = GenericForeignKey(ct_field='content_type',
                                      fk_field='object_id')

Now it does make sense to add an inline to the ThingAdmin. The following is a bare-bones example of an admin that works for both sides of the relation:

from django.contrib import admin
from django.contrib.contenttypes.admin import GenericStackedInline
from .models import Thing, FirstConfig, SecondConfig, ThingConfigRelation


class ConventionalTCRInline(admin.StackedInline):
    model = ThingConfigRelation
    extra = 0


class GenericTCRInline(GenericStackedInline):
    model = ThingConfigRelation
    extra = 0


class ThingAdmin(admin.ModelAdmin):
    inlines = [ConventionalTCRInline]


class ConfigAdmin(admin.ModelAdmin):
    inlines = [GenericTCRInline]


admin.site.register(Thing, ThingAdmin)
admin.site.register(FirstConfig, ConfigAdmin)
admin.site.register(SecondConfig, ConfigAdmin)

Note that we use the conventional inline for the ForeignKey-side of the relation (i.e. in ThingAdmin), and we use the generic inline for the GenericForeignKey-side (in ConfigAdmin).

A tricky bit would be filtering the content_type and object_id fields on the ThingAdmin.

... something completely different:

Another option might be to get rid of the GenericForeignKey altogether and use some kind of single-table inheritance implementation with plain old ForeignKeys instead, a bit like this.

djvg
  • 11,722
  • 5
  • 72
  • 103