如何在 Django 管理站点中增删改查 ContentType?

How to CRUD ContentType in Django admin site?

我正在阅读一本关于 Django 的书,我正在尝试重新解释其中的一些内容。 (我使用的是 Django 2.1 和 Python 3.6) 书中不同类型的内容与一个模块相关联,像这样:

class Module(models.Model):
    course = models.ForeignKey(Course,
                        related_name='modules',
                        on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    order = OrderField(blank=True, for_fields=['course'])

    class Meta:
        ordering = ['order']

    def __str__(self):
        return '{}. {}'.format(self.order, self.title)

class Content(models.Model):
    module = models.ForeignKey(Module,
                        related_name='contents',
                        on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType,
                                on_delete=models.CASCADE,
                                limit_choices_to={'model__in': (
                                    'text',
                                    'video',
                                    'image',
                                    'file')})
    object_id = models.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')
    order = OrderField(blank=True, for_fields=['module'])

    class Meta:
        ordering = ['order']

class ItemBase(models.Model):
    owner = models.ForeignKey(User,
                        related_name='%(class)s_related',
                        on_delete=models.CASCADE)
    title = models.CharField(max_length=250)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    def __str__(self):
        return self.title

    def render(self):
        return render_to_string(
             'courses/content/{}.html'.format(self._meta.model_name),
             {'item': self})

class Text(ItemBase):
    content = models.TextField()

class File(ItemBase):
    file = models.FileField(upload_to='files')

class Image(ItemBase):
    file = models.FileField(upload_to='images')

class Video(ItemBase):
    url = models.URLField()

书中有 CBV 可以为内容类型生成正确的表格:

class ContentCreateUpdateView(TemplateResponseMixin, View):
    module = None
    model = None
    obj = None
    template_name = 'courses/manage/content/form.html'

    def get_model(self, model_name):
        if model_name in ['text', 'video', 'image', 'file']:
            return apps.get_model(app_label='courses',
                                model_name=model_name)
        return None

    def get_form(self, model, *args, **kwargs):
        Form = modelform_factory(model, exclude=['owner',
                                             'order',
                                             'created',
                                             'updated'])
        return Form(*args, **kwargs)

    def dispatch(self, request, module_id, model_name, id=None):
        self.module = get_object_or_404(Module,
                                   id=module_id,
                                   course__owner=request.user)
        self.model = self.get_model(model_name)
        if id:
            self.obj = get_object_or_404(self.model,
                                     id=id,
                                     owner=request.user)
        return super(ContentCreateUpdateView,
           self).dispatch(request, module_id, model_name, id)

    def get(self, request, module_id, model_name, id=None):
        form = self.get_form(self.model, instance=self.obj)
        return self.render_to_response({'form': form,
                                    'object': self.obj})

    def post(self, request, module_id, model_name, id=None):
        form = self.get_form(self.model,
                         instance=self.obj,
                         data=request.POST,
                         files=request.FILES)
        if form.is_valid():
            obj = form.save(commit=False)
            obj.owner = request.user
            obj.save()
            if not id:
                # new content
                Content.objects.create(module=self.module,
                                   item=obj)
            return redirect('module_content_list', self.module.id)
        return self.render_to_response({'form': form,
                                        'object': self.obj})

并且内容由具有特殊权限的用户提供。 好的,现在是问题:我希望内容仅由管理员在管理站点中管理。 我试过这种方式:

class ContentInline(GenericStackedInline):
    model = Content
    extra = 0

@admin.register(Module)
class ModuleAdmin(admin.ModelAdmin):
    list_display = ['title', 'order', 'course']
    list_filter = ['course']
    search_fields = ['title', 'description']
    inlines = [ContentInline]

@admin.register(Content)
class ContentAdmin(admin.ModelAdmin):
    list_display = ['object_id', 'module', 'content_type', 'order']

但我无法在管理站点表单中显示上传正确内容类型的字段。 模块页面如下所示:

module page in admin site

内容页面是这样的:

content page in admin site

我一直在寻找一种解决方案,但始终找不到。这里有一个类似的主题:link 但建议的解决方案暗示 javascript and/or 额外的包,而我只想用 Python 和 Django 这样做。 我还读到在管理站点中显示自定义视图的可能解决方案是编写一个视图,然后将其添加到管理 url,然后将其添加到管理模型。 其他人建议使用书中的 CBV 并在模型管理中使用它。 我曾尝试实施这些建议,但没有成功。 在我的代码的最后一个版本中,我尝试以这种方式将 CBV 与 as_view() 一起使用(在 CBV 之后的 views.py 中):

content_create_update_view = ContentCreateUpdateView.as_view()

然后在 admin.py:

@admin.register(Content)
class ContentAdmin(admin.ModelAdmin):
    list_display = ['object_id', 'module', 'content_type', 'order']

    def get_urls(self):
        urls = super().get_urls()
        my_urls = [
            path('content/<int:object_id>/change/', self.admin_site.admin_view(content_create_update_view)),
        ]
        return my_urls + urls

非常感谢任何帮助或建议。

最后我没能找到解决我问题的方法,至少我没有找到我想要的方法。 所以我试图绕过这个问题,以其他方式获得相同的结果。在寻找不同的方法时,例如不使用 GenericForeignKey 或 ContentTypes 我遇到了这个 link:avoid Django's GenericForeignKey 它看起来清晰简单,虽然可能有点“不太优雅”,所以我实现了以下解决方案。

1) 我修改了内容 class,删除了 GenericForeignKey 并将其替换为每种受支持内容类型的 OneToOne 关系:

text = models.OneToOneField(Text, null=True, blank=True, on_delete=models.CASCADE)
file = models.OneToOneField(File, null=True, blank=True, on_delete=models.CASCADE)
image = models.OneToOneField(Image, null=True, blank=True, on_delete=models.CASCADE)
video = models.OneToOneField(Video, null=True, blank=True, on_delete=models.CASCADE)

并且为了确保每个内容一次只匹配一个附件,我添加了一个覆盖保存功能的检查:

def save(self, **kwargs):
    assert [self.text, self.file, self.image, self.video].count(None) == 3
    return super().save(**kwargs)

最后,我在 class 中添加了一个 属性,这将 return 内容类型:

@property
def target(self):
    if self.text_id is not None:
        return self.text
    if self.file_id is not None:
        return self.file
    if self.image_id is not None:
        return self.image
    if self.video_id is not None:
        return self.video
    raise AssertionError("You have to set content!")

2) 我通过添加所有预期类型的​​内容修改了 admin.py 文件:

@admin.register(Text)
class TextAdmin(admin.ModelAdmin):
    list_display = ['title', 'created', 'updated']

@admin.register(File)
class FileAdmin(admin.ModelAdmin):
    list_display = ['title', 'created', 'updated']

@admin.register(Image)
class ImageAdmin(admin.ModelAdmin):
    list_display = ['title', 'created', 'updated']

@admin.register(Video)
class VideoAdmin(admin.ModelAdmin):
    list_display = ['title', 'created', 'updated']

然后我通过覆盖 get_queryset 函数修改了 ContentAdmin class:

@admin.register(Content)
class ContentAdmin(admin.ModelAdmin):

    def get_queryset(self, request):
        qs = super(ContentAdmin, self).get_queryset(request)
        qs = qs.select_related('text',
                               'file',
                               'image',
                               'video')
        return qs

这样,我就可以在管理界面中加载附件,然后通过从列表中方便地选择它们来将它们与所需的内容组合起来。不幸的是,这是一个分两步的过程,我更愿意以一种形式在管理方面解决所有问题,但是......它有效。