首页 > 解决方案 > Django ModelForm 没有正确摄取派生的表单数据

问题描述

作为我一直在开发的 Wagtail CMS 应用程序的一部分,我无法让文件上传/处理过程正常工作。在过去的六周里,我只在站点上上下班工作,但到目前为止,我在设置多级页面模型结构、流域中的新块类型以及从外部云中摄取图像方面取得了相当大的成功环境进入 Wagtail Image 商店。

我的目标是建立一个模型,列出一系列路径(即纬度/经度序列),其信息是从通过 Wagtail 管理界面上传的文件中提取的。到目前为止,我采用的方法是围绕 Wagtail ModelAdmin 构建它,以便我可以维护路径列表(列表、编辑、删除)并覆盖添加/创建功能,以便可以通过拖放上传路径文件 -降低; 代码来源于 Wagtail 的图像和文档管理应用程序。

具体来说,我发现使用模型表单时未设置模型中字段的值。这些值来自上传文件的内容,而不是直接来自 POSTed 表单。因为没有设置模型值,所以保存模型和后续编辑都会受到影响。

以下是简化的代码片段:

模型.py:

from django.db import models

class AbstractPath(models.Model):
    name = models.CharField(max_length=100, blank=False, null=False)
    start_location = models.CharField(max_length=120, blank=False, null=False)
    start_timestamp = models.DateTimeField(blank=False, null=False)
    stop_location = models.CharField(max_length=120, blank=False, null=False)
    stop_timestamp = models.DateTimeField(blank=False, null=False)

    class Meta:
        abstract = True

    def __str__(self):
        return self.name

class RealPath(AbstractPath):
    average_speed = models.FloatField(blank=False, null=False)
    max_speed = models.FloatField(blank=False, null=False)

表格.py:

from wagtail.admin.forms import WagtailAdminModelForm
from wagtail.admin.edit_handlers import BaseFormEditHandler

class PathForm(WagtailAdminModelForm):
    permission_policy = paths_permission_policy

    class Meta:
        model = RealPath
        fields = '__all__'

class PathFormEditHandler(BaseFormEditHandler):
    base_form_class = PathForm

管理员.py:

from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.contrib.modeladmin.views import CreateView, EditView

from django.template.loader import render_to_string
from django.http import HttpResponseBadRequest, JsonResponse
from django.utils.encoding import force_str

class RealPathCreateView(CreateView):
    def get_template_names(self):
        return ['RealPath_create.html']

    def post(self, request, *args, **kwargs):
        if not request.is_ajax():
            return HttpResponseBadRequest("Cannot POST to this view without AJAX")

        if not request.FILES:
            return HttpResponseBadRequest("Must upload a file")

        # Get dict with data derived from the file - keys align with model fields
        path_form_data = pathfile_process(request.FILES['files[]'])

        # Build a form for validation
        RealPathForm = self.get_form_class()
        form = RealPathForm(path_form_data, { 'file': request.FILES['files[]'] })

        if form.is_valid():
            path = form.save(commit=False)

            # Temporary workaround to load path model with derived data - shouldn't be necessary?
            for f, v in path_form_data.items():
                setattr(path, f, v)

            path.save()

            return JsonResponse({
                'success': True,
                'path_id': int(path.id),
                'form': render_to_string('RealPath_edit.html', {
                    'path': path,
                    'form': RealPathForm(
                        instance=path, prefix='path-%d' % path.id
                    ),
                }, request=request),
            })
        else:
            # Validation error
            return JsonResponse({
                'success': False,
                'error_message': '\n'.join(['\n'.join([force_str(i) for i in v]) for k, v in form.errors.items()]),
            })

    def get_edit_handler(self):
        edit_handler = self.model_admin.get_edit_handler(
            instance=self.get_instance(), request=self.request
        )
        return edit_handler.bind_to(model=self.model_admin.model)


class RealPathAdmin(ModelAdmin):
    model = RealPath
    menu_label = "Real Path"
    menu_icon = "arrow-right"
    menu_order = 320
    add_to_settings_menu = False
    exclude_from_explorer = False
    list_display = ("name", "start_timestamp", "start_location", "stop_timestamp", "stop_location")
    form_fields_exclude = ["start_timestamp", "stop_timestamp", "average_speed"]

    create_view_class = RealPathCreateView

    def get_edit_handler(self, instance, request):
        return PathFormEditHandler(())


modeladmin_register(RealPathAdmin)

路径文件上传正常,可在 RealPathCreateView.post() 方法中使用。该文件被处理以提取相关数据,并放入path_form_data. 我的期望是,当使用创建表单时RealPathForm,它将创建RealPath模型的一个实例并用该数据填充其中的字段。我发现没有填充数据值;如果此时我尝试使用 保存form.save(commit=True),则会引发异常django.db.utils.IntegrityError: NOT NULL constraint failed: paths_realpath.start_timestamp

对该问题的深入调查显示该form对象具有一个空fields属性,这意味着RealPath从未设置模型字段,然后表单被“验证”并且保存失败,因为传递给数据库层的大部分数据都是Noneor 0.0。完整的字段列表在 django 的方法中生成ModelFormMetaclass.__new__()基于模型的内容,但从未传递给模型本身。

我最终实现了手动设置模型字段的解决方法(如上面的代码中所示),但后来我发现后续的表单渲染也被破坏了,因为需要迭代表单字段 - 也没有填充。显然,我应该解决第一个问题,因为它将解决第二个问题(可能还有其他问题),但是我看不到 django 代码中这种转移发生的位置,因此我需要对我的代码进行哪些更改。

感谢你的协助。

注意:您会注意到RealPath模型是基于AbstractPath模型的——原因是我将有几种类型的实数Path,其数据来自不同的来源。我把那个结构留在那里,以防它是我遇到问题的原因。

标签: django-formswagtail

解决方案


我最终得出的结论是,如果要从派生文件数据中设置模型字段,并且如果我希望 ModelAdmin 和 django 表单功能正常工作,我将不得不删除其中的form_fields_exclude属性RealPathAdmin- 它本质上与 NOT NULL 不兼容模型中的约束RealPath。随后进行编辑时,这会生成一个包含所有数据的表单,包括我不想编辑的字段。然后,我可以通过使用属性、指定小部件并使用一些 CSS将这些不可编辑的字段完全隐藏在用户面前。ModelAdmin.panelsHiddenInput

这也意味着接受所有数据将流入和流出浏览器,从而提供一个技术机会来更改应该只来自原始文件的数据。在我的部署方案中,这是低风险的,但可以在编辑视图中使用更多逻辑关闭打开。

我修改后的要点代码如下:

models.py:(没有变化)

from django.db import models

class AbstractPath(models.Model):
    name = models.CharField(max_length=100, blank=False, null=False)
    start_location = models.CharField(max_length=120, blank=False, null=False)
    start_timestamp = models.DateTimeField(blank=False, null=False)
    stop_location = models.CharField(max_length=120, blank=False, null=False)
    stop_timestamp = models.DateTimeField(blank=False, null=False)

    class Meta:
        abstract = True

    def __str__(self):
        return self.name

class RealPath(AbstractPath):
    average_speed = models.FloatField(blank=False, null=False)
    max_speed = models.FloatField(blank=False, null=False)

forms.py:(已删除 - 不需要编辑处理程序)

管理员.py:

from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .models import RealPath
from .views import RealPathCreateView

class RealPathAdmin(ModelAdmin):
    model = RealPath
    menu_label = "Real Path"
    menu_icon = "arrow-right"
    menu_order = 320
    add_to_settings_menu = False
    exclude_from_explorer = False
    list_display = ("name", "start_timestamp", "start_location", "stop_timestamp", "stop_location")

    create_view_class = RealPathCreateView

    panels = [
        MultiFieldPanel([
            FieldPanel('name'),
            FieldPanel('start_location'),
            FieldPanel('stop_location'),
        ], heading="Real Path Name"),
        MultiFieldPanel([
            FieldPanel('start_timestamp', classname="realpath_admin_hidden", widget=HiddenInput),
            FieldPanel('stop_timestamp', classname="realpath_admin_hidden", widget=HiddenInput),
            FieldPanel('average_speed', classname="realpath_admin_hidden", widget=HiddenInput),
            FieldPanel('max_speed', classname="realpath_admin_hidden", widget=HiddenInput),
        ])
    ]


modeladmin_register(RealPathAdmin)

views.py:(视图类从 admin.py 移动)

from wagtail.contrib.modeladmin.views import CreateView

from django.template.loader import render_to_string
from django.http import HttpResponseBadRequest, JsonResponse
from django.utils.encoding import force_str

class RealPathCreateView(CreateView):
    def get_template_names(self):
        return ['RealPath_create.html']

    def post(self, request, *args, **kwargs):
        if not request.is_ajax():
            return HttpResponseBadRequest("Cannot POST to this view without AJAX")

        if not request.FILES:
            return HttpResponseBadRequest("Must upload a file")

        # Get dict with data derived from the file - keys align with model fields
        path_form_data = pathfile_process(request.FILES['files[]'])

        # Build a form for validation
        RealPathForm = self.get_form_class()
        form = RealPathForm(path_form_data, { })

        if form.is_valid():
            path = form.save()

            return JsonResponse({
                'success': True,
                'path_id': int(path.id),
                'form': render_to_string('RealPath_edit.html', {
                    'path': path,
                    'form': RealPathForm(
                        instance=path, prefix='path-%d' % path.id
                    ),
                }, request=request),
            })
        else:
            # Validation error
            return JsonResponse({
                'success': False,
                'error_message': '\n'.join(['\n'.join([force_str(i) for i in v]) for k, v in form.errors.items()]),
            })

推荐阅读