Django admin - OneToOneField inline throws "ForeignKey exception"
I have a very simple application that currently declares two models, one is called "Content" and just contains the content data, and the other is "Page" which includes "Content" as OneToOneField.
The reason I did this is because I can have "Page" as a specific concrete class that I am using, and when other models in other modules I plan to need page data, they can just include "Content" like OneToOneField. I did it this way to avoid inheritance and use composition.
models.py:
from django.db import models
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
class Page(models.Model):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
content = models.OneToOneField(Content)
def __str__(self):
return self.content.title
admin.py:
from django.contrib import admin
from content.models import Page, Content
class ContentInline(admin.TabularInline):
model = Content
fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')
class PageAdmin(admin.ModelAdmin):
fields = ('slug',)
inlines = [ContentInline]
On the admin page I am getting this exception:
Exception at /admin/content/page/add/
<class 'content.models.Content'> has no ForeignKey to <class 'content.models.Page'>
What says is of course correct, but I can't seem to find a way to do what I want, which is to include the inline side of the non-defining side of the relationship. I do not want to declare a relationship in "Content", since then I will need to define each relationship to it within this class, which will inject dependencies to other modules, which, in my opinion, should not know anything.
Using Django 1.6 in Python 3.3.
Edit: As pointed out in the comments, I decided to use inheritance. My first concern with this was that I wanted the flexibility to be able to compose classes from several other classes. However, since the Django ORM supports multiple inheritance, and if I realized this method is called "mixins" (new to Python), I would have gotten somewhere much sooner.
Example mixins with models:
from django.db import models
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
def __str__(self):
return self.title
class Meta:
abstract = True
class Data(models.Model):
data_name = models.CharField(max_length=200)
class Meta:
abstract = True
class Page(Content, Data):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
And then I can just use it as one model in admin.py.
source to share
Another solution is moving OneToOneField
from Content
toPage
class Content(models.Model):
"""Basic page data which can be used by other modules"""
title = models.CharField(max_length=200)
html_title = models.CharField(max_length=200)
meta_desc = models.CharField(max_length=200)
keywords = models.CharField(max_length=200)
content = models.TextField()
page = models.OneToOneField(Page, primary_key=True, related_name="content")
class Page(models.Model):
"""Concrete implementation of a basic page managed by the admin"""
slug = models.SlugField()
def __str__(self):
return self.content.title
You can still do page.content
and the inline form will work out of the box
EDIT:
One of the downsides to this approach is that it will allow the user to create the page without assigning any content to it (in which case it fails page.content
)
It is very easy to solve this problem by creating a custom form
class ContentAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
kwargs["empty_permitted"] = False
super(ContentAdminForm, self).__init__(*args, **kwargs)
Then on the admin page
class ContentInline(admin.TabularInline):
model = Content
form = ContentAdminForm
fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')
source to share
If you don't want to change your models at all, there is a django module for displaying the non-defining inline side: django_reverse_admin
You need to add django_reverse_admin to your requirements.txt file:
-e git+https://github.com/anziem/django_reverse_admin.git#egg=django_reverse_admin
Then import it:
admin.py
from django.contrib import admin
from django_reverse_admin import ReverseModelAdmin
from content.models import Page, Content
# don't need to define an inline anymore for Content
class PageAdmin(ReverseModelAdmin):
fields = ('slug',)
inline_reverse = ['content']
inline_type = 'tabular' # or could be 'stacked'
source to share