4

I have a very simple app which at the moment declares two models: one is called "Content" and simply holds content data and the other is "Page" which includes "Content" as a OneToOneField.

The reason I've done this is so that I can have "Page" as an actual concrete class that I use and when other models in other modules I'm planning need page data, they can simply include "Content" as a OneToOneField. I've done it this way to avoid inheritance and use composition instead.

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 page admin I get this exception:

Exception at /admin/content/page/add/
<class 'content.models.Content'> has no ForeignKey to <class 'content.models.Page'>

What is says of course is correct, but I cannot seem to find a way of doing what I want, which is to include an inline of the non-defining side of a relationship. I don't want to declare the relationship on "Content" as then I'd have to define every single relationship to it inside that class which would introduce dependencies to other modules, which in my opinion it should know nothing about.

Using Django 1.6 on Python 3.3.

Edit: As indicated in the comments, I've decided to use inheritance. My initial concern about this was that I wanted the flexibility to be able to compose classes from multiple other classes. However, since the Django ORM does support multiple inheritance and if I'd realised that method was called "mixins" (new to Python) I would have got somewhere a lot 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.

Gnuffo1
  • 3,478
  • 11
  • 39
  • 53
  • Could be related: http://stackoverflow.com/questions/1744203/django-admin-onetoone-relation-as-an-inline and http://stackoverflow.com/questions/20889806/use-onetoonefield-inlined-in-django-admin – alecxe Apr 26 '14 at 01:57
  • Most definitely related to other SO answers. From this one, it would seem that what you want to achieve is not possible in this manner: http://stackoverflow.com/a/1744445/1345536 – Travis D. Apr 26 '14 at 04:28
  • Subtly different to the satchmo question - he's solved it by making the form's main model the product, which was previously the inline. Since I want Content to be used in multiple places it can't ever be the form's main model. However I've bitten the bullet and gone with the suggestion of using Content as an abstract base class. Don't particularly like it though as it it limits the amount of classes I can "compose" other classes with to one (unless the ORM can handle multiple inheritance). – Gnuffo1 Apr 27 '14 at 04:42

2 Answers2

0

Another solution is moving the OneToOneField from Content to Page

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 cons of that approach is that it will allow the user to create a page without assigning any content to it (in which case page.content will crash)

Its very easy to overcome this issue by creating custom form

class ContentAdminForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        kwargs["empty_permitted"] = False
        super(ContentAdminForm, self).__init__(*args, **kwargs)

Then in the admin page

class ContentInline(admin.TabularInline):

    model = Content
    form = ContentAdminForm
    fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')
Ramast
  • 7,157
  • 3
  • 32
  • 32
0

If you don't want to change your models at all, there's a django module to display the non-defining side inline: django_reverse_admin

You'll need to add django_reverse_admin to your requirements.txt:

-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'
lynx
  • 310
  • 4
  • 7