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

问题描述 投票:0回答:1

作为我一直在开发的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__()方法基于模型的内容生成的字段is的完整列表,但从未传递给模型本身。

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

感谢您的协助。

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

django-forms wagtail
1个回答
0
投票

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

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

我的修改后的基本代码如下:

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 :(已删除-不需要编辑处理程序)

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)

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()]),
            })
© www.soinside.com 2019 - 2024. All rights reserved.