Django ModelForm无法正确提取派生的表单数据

时间:2019-12-22 08:57:36

标签: django-forms wagtail

作为我一直在开发的Wagtail CMS应用程序的一部分,我无法使文件上载/处理过程正常工作。在过去的六周内,我只是一直在网站上或不在网站上工作,但到目前为止,我在建立多级页面模型结构,流字段中的新块类型以及从外部云提取图像方面取得了相当大的成功环境进入Wagtail图片存储。

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

具体来说,我发现使用模型表单时未设置模型中字段的值。这些值是从上载文件的内容派生的,而不是直接从POSTed表单派生的。由于未设置模型值,因此会影响模型的保存和后续编辑。

以下是简化的代码段:

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:

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

admin.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模型字段,然后对表单进行“验证”,并且保存失败,因为传递给数据库层的大多数数据是None0.0。字段 的完整列表是在django的ModelFormMetaclass.__new__()方法中根据模型的内容生成的,但从未传递给模型本身。

我最终实现了手动设置模型字段的解决方法(如上面的代码),但是随后我发现随后的表单呈现也被破坏,因为需要遍历表单字段-两者均未填充。显然,我应该解决第一个问题,因为它将解决第二个问题(可能还有其他问题),但是我看不到在django代码中此传输发生在哪里,因此我需要对代码进行哪些更改。

感谢您的帮助。

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

1 个答案:

答案 0 :(得分:0)

我最终得出的结论是,如果要从派生的文件数据中设置模型字段,并且如果我想让ModelAdmin和django表单函数起作用,那么我必须在{中删除form_fields_exclude属性{1}}-本质上与RealPathAdmin模型中的NOT NULL约束不兼容。随后进行编辑时,这将导致形成一个包含所有数据的表单,其中包括我不想编辑的字段。然后,我可以通过使用RealPath属性,指定ModelAdmin.panels小部件并使用一些CSS来将这些不可编辑字段隐藏起来。

>

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

我修改后的代码如下:

models.py :(不变)

HiddenInput

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

admin.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)

views.py :(视图类已从admin.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)