Django 管理后台 - OneToOneField 内联抛出“没有 ForeignKey”异常

4 投票
2 回答
3478 浏览
提问于 2025-04-18 04:18

我有一个非常简单的应用程序,目前定义了两个模型:一个叫“Content”,它只是用来存储内容数据;另一个叫“Page”,它包含了“Content”,并且是一个一对一的关系。

我这样做的原因是希望“Page”能作为一个具体的类来使用。当我计划在其他模块中需要页面数据时,可以简单地将“Content”作为一对一的关系来包含进来。我选择这种方式是为了避免使用继承,而是采用组合的方式。

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]

在页面的管理界面上,我遇到了这个异常:

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

它说的确实没错,但我似乎找不到实现我想要的方式,也就是在关系的非定义方包含一个内联。我不想在“Content”上声明这个关系,因为那样我就得在这个类里定义每一个与之相关的关系,这样会引入对其他模块的依赖,而我认为它不应该知道这些。

我使用的是Django 1.6和Python 3.3。

编辑:正如评论中提到的,我决定使用继承。我最初担心的是我想要能够灵活地从多个其他类组合类。然而,由于Django的ORM支持多重继承,如果我早知道这种方法叫做“mixins”(这是我刚接触Python时的事),我会更早找到解决方案。

模型的示例mixins:

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()

然后我可以在admin.py中将其作为一个模型使用。

2 个回答

0

如果你不想对你的模型做任何修改,可以使用一个叫做django_reverse_admin的Django模块,它可以让你在页面上直接显示那些不主要的部分。你可以在这里找到它:django_reverse_admin

你需要把django_reverse_admin添加到你的requirements.txt文件里:

-e git+https://github.com/anziem/django_reverse_admin.git#egg=django_reverse_admin

然后导入它:

在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'
0

另一种解决方案是把 OneToOneFieldContent 移到 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

这样你仍然可以使用 page.content,并且内联表单会直接正常工作。

补充说明:

这种方法的一个缺点是,用户可以创建一个页面而不为其分配任何内容(这样的话 page.content 会出错)。

不过,解决这个问题非常简单,只需要创建一个自定义表单。

class ContentAdminForm(forms.ModelForm):

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

然后在管理页面中。

class ContentInline(admin.TabularInline):

    model = Content
    form = ContentAdminForm
    fields = ('title', 'html_title', 'meta_desc', 'keywords', 'content')

撰写回答