Admin inline with no ForeignKey relation
Asked Answered
G

3

18

Is it possible to manually specify the set of related object to show in an inline, where no foreign key relation exists?

# Parent
class Diary(models.Model):
    day = models.DateField()
    activities = models.TextField()

# Child
class Sleep(models.Model):
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()

class SleepInline(admin.TabularInline):
    model=Sleep
    def get_queryset(self, request):
        # Return all Sleep objects where start_time and end_time are within Diary.day
        return Sleep.objects.filter(XXX) 

class DiaryAdmin(admin.ModelAdmin):
    inlines = (SleepInline, )

I want my Diary model admin to display an inline for Sleep models that have start_time equal to the same day as Diary.day. The problem is that the Sleep model does not have a ForeignKey to Diary (instead, the relation is implicit by the use of dates).

Using the above, Django immediately complains that

<class 'records.admin.SleepInline'>: (admin.E202) 'records.Sleep' has no ForeignKey to 'records.Diary'.

How can I show the relevant Sleep instances as inlines on the Diary admin page?

Gramme answered 20/6, 2017 at 7:33 Comment(5)
have you tried using property?Apostle
There is a relation. You're just denying it. Once you're over your denial, add a ManyToManyField on either side.Intermix
@Melvyn I don't think it's a good idea to add redundant DB columns that I have to manually keep in sync as a workaround for the way Django presents its admin. Maybe there is a way I can fake it with a property or similar without touching the schema?Gramme
What's this manually keep in sync you're talking about? Also, are these your real models? Cause a) you mark Diary as parent of Sleep and your code contradicts it and b) in real world terms, I don't get the relation between diary and sleep.Intermix
@Melvyn This is just an example: Diary logs what the user does in a day, and Sleep tracks when the user sleeps. Logically a "Diary entry" for a single day also contains (a list of) Sleep objects, but I don't want to actually add a ForeignKey Sleep.diary because then I have to keep it in sync with start_time and end_time manually - i.e. override Sleep.save() to point to the correct Diary object based on start_time and end_time entered by the user. I don't want to change my schema for the sole purpose of making that inbuilt admin work.Gramme
I
7

Let me start by showing you the drawbacks of your logic:

  • When adding a foreign key, there are 2 operations, that are uncommon that require adjusting the relation: creating a new sleep object and updating the times on the sleep object.
  • When not using a foreign key, each time a diary is requested the lookup for the corresponding Sleep object(s) needs to be done. I'm assuming reading diaries is much more common then alterations of sleep objects, as it will be in most projects out there.

The additional drawback as you've noticed, is that you cannot use relational features. InlineAdmin is a relational feature, so as much as you say "making the admin work", it is really that you demand a hammer to unscrew a bolt.

But...the admin makes use of ModelForm. So if you construct the form with a formset (which cannot be a an inline formset for the same reason) and handle saving that formset yourself, it should be possible. The whole point of InlineFormset and InlineAdmin is to make generation of formsets from related models easier and for that it needs to know the relation.

And finally, you can add urls and build a custom page, and when extending the admin/base.html template, you will have access to the layout and javascript components.

Intermix answered 24/6, 2017 at 16:52 Comment(0)
B
9

There is no getting around the fact that Django admin inlines are built around ForeignKey fields (or ManyToManyField, OneToOneField). However, if I understand your goal, it's to avoid having to manage "date integrity" between your Diary.day and Sleep.start_time fields, i.e., the redundancy in a foreign key relation when that relation is really defined by Diary.day == Sleep.start_time.date()

A Django ForiegnKey field has a to_field property that allows the FK to index a column besides id. However, as you have a DateTimeField in Sleep and a DateField in Diary, we'll need to split that DateTimeField up. Also, a ForeignKey has to relate to something unique on the "1" side of the relation. Diary.day needs to be set unique=True.

In this approach, your models look like

from django.db import models

# Parent
class Diary(models.Model):
    day = models.DateField(unique=True)
    activities = models.TextField()

# Child
class Sleep(models.Model):
    diary = models.ForeignKey(Diary, to_field='day', on_delete=models.CASCADE)
    start_time = models.TimeField()
    end_time = models.DateTimeField()

and then your admin.py is just

from django.contrib import admin
from .models import Sleep, Diary

class SleepInline(admin.TabularInline):
    model=Sleep

@admin.register(Diary)
class DiaryAdmin(admin.ModelAdmin):
    inlines = (SleepInline, )

Even though Sleep.start_time no longer has a date, the Django Admin is quite what you'd expect, and avoids "date redundancy":

Django Admin: Diary


Thinking ahead to a more real (and problematic) use case, say every user can have 1 Diary per day:

class Diary(models.Model):
    user = models.ForeignKey(User)
    day = models.DateField()
    activities = models.TextField()

    class Meta:
        unique_together = ('user', 'day')

One would like to write something like

class Sleep(models.Model):
    diary = models.ForeignKey(Diary, to_fields=['user', 'day'], on_delete=models.CASCADE)

However, there's no such feature in Django 1.11, nor can I find any serious discussion of adding that. Certainly composite foreign keys are allowed in Postgres and other SQL DBMS's. I get the impression from the Django source they're keeping their options open: https://github.com/django/django/blob/stable/1.11.x/django/db/models/fields/related.py#L621 hints at a future implementation.

Finally, https://pypi.python.org/pypi/django-composite-foreignkey looks interesting at first, but doesn't create "real" composite foreign keys, nor does it work with Django's admin.

Bonsai answered 27/6, 2017 at 3:49 Comment(2)
If I understand correctly, this will work in the date case but won't solve the more general problem of using a custom query instead of a ForeignKey relation - is that right?Gramme
Yes, that's correct. This is the most "Django-thonic" way. I did a brief experiment, trying to "fake" the inline foreign key field using various tricks, some undocumented. That approach led to increasingly ugly Django problems to workaround. I still believe there is a way to achieve what you're looking for (which makes a lot of sense, fwiw). However, I'm pretty sure it would require a custom ORM field inherited from RelatedField, and not be a simple queryset in your admin, inline classes.Bonsai
I
7

Let me start by showing you the drawbacks of your logic:

  • When adding a foreign key, there are 2 operations, that are uncommon that require adjusting the relation: creating a new sleep object and updating the times on the sleep object.
  • When not using a foreign key, each time a diary is requested the lookup for the corresponding Sleep object(s) needs to be done. I'm assuming reading diaries is much more common then alterations of sleep objects, as it will be in most projects out there.

The additional drawback as you've noticed, is that you cannot use relational features. InlineAdmin is a relational feature, so as much as you say "making the admin work", it is really that you demand a hammer to unscrew a bolt.

But...the admin makes use of ModelForm. So if you construct the form with a formset (which cannot be a an inline formset for the same reason) and handle saving that formset yourself, it should be possible. The whole point of InlineFormset and InlineAdmin is to make generation of formsets from related models easier and for that it needs to know the relation.

And finally, you can add urls and build a custom page, and when extending the admin/base.html template, you will have access to the layout and javascript components.

Intermix answered 24/6, 2017 at 16:52 Comment(0)
L
0

You can achieve this, using nested_admin library.

So in myapp/admin.py:

from nested_admin.nested import NestedStackedInline, NestedModelAdmin

class SleepInline(NestedStackedInline):

    model = Sleep

class DiaryAdmin(NestedModelAdmin):

   inlines = [SleepInline]
Lohman answered 31/3, 2020 at 18:42 Comment(1)
I have tried it in a similar case, with the only difference that there is a foreign key relation, but the other way round. Contact has a Foreign key to CaseStudy, but I would like to have the Contact form inside the case study instead of having a choice field and a plus sign for adding a Contact instance. Since I have django-nested-admin and django-nested-inline installed anyway, I wanted to give it a try, but I got an error complaining about Contact not having a ForeignKey to CaseStudy.Hermetic

© 2022 - 2024 — McMap. All rights reserved.