Admin inline with no ForeignKey relation

Flash picture Flash · Jun 20, 2017 · Viewed 9.2k times · Source

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?

Answer

rm -rf picture rm -rf · Jun 27, 2017

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.