tbm
tbm

Reputation: 1201

How to reference model joined across foreign key in django admin

I have a django 1.6 app with the following (trimmed for clarity) classes defined. User is the standard django.contrib.auth User class.

class Event(models.Model):
    user = models.ForeignKey(User, related_name='events')
    name = models.CharField(max_length=64)


class Profile(models.Model):
    user = models.ForeignKey(User, related_name='aprofile')


class MemberProfile(Profile):
    pass

Here are my admin classes:

class ProfileAdmin(ModelAdmin):
    model = Profile
    fields = ('user', )


class MemberProfileAdmin(ModelAdmin):
    model = MemberProfile
    fields = ('user', )
    readonly_fields = ('user', )

What I'd like to do is display a read-only list of all events for a given member, or at least profile. Of course joining across the User foreign key seems like the way to go, but I am drawing a blank as to how to accomplish this. Here's a summary of attempts so far.

Define an inline admin on the Event class directly referencing the user field, and add it to the ProfileAdmin:

class EventInlineAdmin(TabularInline):
    model = Event
    fk_name = 'user'    # Fails - fk_name 'user' is not a ForeignKey to <class 'solo365.solo_profile.models.profile.Profile'>

...well, no, it sure isn't. But our User has an 'aprofile' field, so...

class EventInlineAdmin(TabularInline):
    model = Event
    fk_name = 'user__aprofile'    # Fails - EventInlineAdmin.fk_name' refers to field 'user__aprofile' that is missing from model 'admin_fk_test.Event'.

Ok, those fields look like they should sync up, but perhaps we need to be a little more aggressive:

class EventInlineAdmin(TabularInline):
    model = Event
    fk_name = 'user__aprofile__pk'    # Fails - 'EventInlineAdmin.fk_name' refers to field 'user__aprofile__pk' that is missing from model 'admin_fk_test.Event'.

I've also tried messing with formfield_for_foreignkey() and friends in both the inline and the regular model admins, but without that fk_name having a valid value, those methods never get called.

I then considered trying to access the events field directly from a Profile's user:

class ProfileAdmin(ModelAdmin):
    model = Profile
    fields = ('user', 'user__events')  # Fails - Unknown field(s) (user__events) specified for Profile. Check fields/fieldsets/exclude attributes of class ProfileAdmin.

What about with a custom formfield_for_foreignkey() method? Sadly that never gets called for anything other than the 'user' field. I've also considered a custom get_formsets() method, but frankly I'm not sure how I could use that without a working EventInlineAdmin.

I could of course define a custom field that simply concatenates all of the events and returns that as a string, but ideally I would prefer something like a fully-featured inline (even read-only) than just a chunk o' text. IOW such a custom field would have a method that (ideally) would return an inline form without requiring any sort of custom template, setting of allow_tags, etc.

Am I doomed to have to create a completely custom Form for the Profile admin class? Or is there a simple way to accomplish what I'm trying to do, that I'm just missing?

Update:

Bonus points if a provided solution works for the MemberProfileAdmin class, not just the ProfileAdmin class.

Upvotes: 1

Views: 1977

Answers (1)

Risadinha
Risadinha

Reputation: 16666

The relation between User and Profile should be a 1:1 relation which would allow the referencing via user__aprofile. Otherwise, the reverse relation of a foreing key is a queryset because one foreign key can be assigned to multiple instances. This is might be the reason why your code failed.

Change it to:

class Profile(models.Model):
    user = models.OneToOneKey(User, related_name='aprofile')

This is a bit like using ForeignKey(unique=True).

To know the attributes, it might help to call dir(model_instance) on the model instance in question, or try around in the Django shell (./manage.py shell).

Also, I've experienced that it might be more confusing to assign a custom related_name like in your case where you would expect one profile by looking at the related name but you would actually get back a queryset. The generated name in that case would be profile_set, and you would have to call profile_set.all() or profile_set.values() to get some actual profiles.

Upvotes: 3

Related Questions