首页 > 解决方案 > 如何最好地一次提交多个 Django formset_factory 表单?

问题描述

对于堆栈来说,这可能是一个过于宽泛/复杂的问题,但我必须通过提出这个问题来澄清我的想法。我非常愿意接受有关如何分解或缩小范围的建议。ATM,如果没有所有这些上下文,我不确定该怎么做...

我对 Django 还很陌生,我刚刚完成了一个概念验证高级搜索页面,我花了 2 周时间才开始按照我的意愿工作 - 它工作正常(实际上非​​常好),但它真的很笨拙,现在我有概念验证工作,我有兴趣学习如何正确地做到这一点,并且正在寻找有关如何重构它以使其更直接的建议。为了总结功能,该界面允许使用动态分层形式创建复杂的搜索查询。它(当前)对 2 种输出格式中的 1 种执行此操作(这实际上会导致模型中 5 或 6 个表的连接呈现,具体取决于所选的输出格式),但它会提交所有层次结构并执行搜索在选定的一个。这是界面的 gif 演示,以使其更清晰。请注意,在 gif 中,仅在提交时显示的“PeakData”搜索词层次结构上执行搜索。“PeakGroups”搜索词(提交时隐藏)被忽略(但已保存,并且仍可在结果页面上访问):

kludgey 代码的工作演示

我这样做的方式虽然看起来很笨拙。我对 javascript 组件很好,因为它对我来说似乎很简单。简而言之,它生成分层显示、输出格式选择列表和“匹配所有/任意”选择列表。它根据选定的输出格式隐藏/显示层次结构,并将层次结构数据转换为存储在隐藏表单集字段中的值(命名pos- 存在于所有表单类中)。由于它可能从 gif 中混淆,我应该注意实际上有 2 套完整的分层表单集结构,它们看起来基本相同(但包含不同的表单集类型)。(如果有助于清晰,我可以制作另一个同时显示所有层次结构的 gif。)

看起来像是 hack 的 python/Django 代码。以下是重点:

forms.py

class AdvSearchPeakGroupsForm(forms.Form):
    # posprefix is a static value used to skip validation of forms coming from other classes
    posprefix = 'pgtemplate'
    # `pos` (a hidden field) saves the hierarchical path of the form position that was generated by javascript
    pos = forms.CharField(widget=forms.HiddenInput())
    fld = forms.ChoiceField(choices=(
            ("peak_group__msrun__sample__tissue__name", "Tissue"),
            ("peak_group__msrun__sample__animal__feeding_status", "Feeding Status"),
            ...
    )
    ncmp = forms.ChoiceField(choices=(...))
    val = forms.CharField(widget=forms.TextInput())

    def clean(self):
        self.saved_data = self.cleaned_data
        return self.cleaned_data

    def is_valid(self):
        data = self.cleaned_data
        fields = self.base_fields.keys()
        # Only validate if the pos field contains the posprefix - otherwise, it belongs to a different form class
        if 'pos' in data and self.posprefix in data["pos"]:
            self.selected = True
            for field in fields:
                if field not in data:
                    return False
        else:
            print("pos not in data or does not have prefix:",self.posprefix)
        return True

class AdvSearchPeakDataForm(forms.Form):
    # This is essentially the same as the class above, but with different values

views.py

from DataRepo.multiforms import MultiFormsView

class AdvancedSearchView(MultiFormsView):
    template_name = "DataRepo/search_advanced.html"
    form_classes = {
        'pgtemplate': formset_factory(AdvSearchPeakGroupsForm),
        'pdtemplate': formset_factory(AdvSearchPeakDataForm)
    }
    success_url = ""
    mixedform_prefixes = {'pgtemplate': "pgtemplate", 'pdtemplate': "pdtemplate"}
    mixedform_selected_formtype = "fmt"
    mixedform_prefix_field = "pos"
    prefix = "form" # I'll explain this if asked

    # I override get_context_data, but only to grade URL params for the browse link - so it's not relevant to my question (skipping)

    # Skipping form_invalid...

    def form_valid(self, formset):
        # This turns the form data into an object I use to construct the query
        qry = formsetsToDict(formset, self.prefix, {
            'pgtemplate': AdvSearchPeakGroupsForm.base_fields.keys(),
            'pdtemplate': AdvSearchPeakDataForm.base_fields.keys()
        })
        res = {}
        q_exp = constructAdvancedQuery(qry)

        # This executes the query on different joins based on the selected output format (the top select list)
        if qry['selectedtemplate'] == "pgtemplate":
            res = PeakData.objects.filter(q_exp).prefetch_related(
                "peak_group__msrun__sample__animal__studies"
            )
        elif qry['selectedtemplate'] == "pdtemplate":
            res = PeakData.objects.filter(q_exp).prefetch_related(
                "peak_group__msrun__sample__animal"
            )

        # Results are shown below the form...
        return self.render_to_response(
            self.get_context_data(res=res, forms=self.form_classes, qry=qry, debug=settings.DEBUG)
        )

multiforms.py 这段代码最初是在别处获得的,我对其进行了修改以处理同一个 HTML 表单中的不同表单集——因为我想在幕后的每个填写的层次结构中保存所有输入的搜索词,以便如果它们更改输出格式选择列表值,他们之前输入的所有搜索词仍然存在。javascript 在一个对象中跟踪所有内容,并在提交时将层次结构和表单集条目保存在上面的隐藏pos字段中。这个 multiforms.py 是最复杂的部分。我会尽量只包括重要的部分......

class MultiFormMixin(ContextMixin):

    form_classes = {}
    prefixes = {}
    success_urls = {}
    grouped_forms = {}

    # I created the concept of a "mixed form" in order to save all entries, but only validate the selected formset type
    # A mixed form is a form submission containing any number of forms (i.e. formsets) and any number of formset types (created by formset_factory using a different form class)
    # Only 1 form type (based on a selected form field) will be validated

    # mixedform_prefixes is a dict keyed on the same keys as form_classes
    mixedform_prefixes = {}

    # mixedform_selected_formtype is a form field superficially added (e.g. via javascript) that contains a prefix of the form classes the user has selected from the mixed forms.  This is the top select list named `fmt`
    mixedform_selected_formtype = ""

    # This is a form field included in each of the form_classes whose value will start with one of the mixedform_prefixes (e.g. the `pos` field)
    mixedform_prefix_field = ""
    
    initial = {}
    prefix = None
    success_url = None

    def get_forms(self, form_classes, form_names=None, bind_all=False):
        return dict([(key, self._create_form(key, klass, (form_names and key in form_names) or bind_all)) \
            for key, klass in form_classes.items()])
    
    def get_form_kwargs(self, form_name, bind_form=False):
        kwargs = {}
        kwargs.update({'initial':self.get_initial(form_name)})
        kwargs.update({'prefix':self.get_prefix(form_name)})
        
        if bind_form:
            kwargs.update(self._bind_form_data())

        return kwargs

    def forms_valid(self, forms):
        num_valid_calls = 0
        for form_name in forms.keys():
            form_valid_method = '%s_form_valid' % form_name
            if hasattr(self, form_valid_method):
                getattr(self, form_valid_method)(forms[form_name])
                num_valid_calls += 1
        if num_valid_calls == 0:
            return self.form_valid(forms)
        else:
            return HttpResponseRedirect(self.get_success_url(form_name))

    def _create_form(self, form_name, klass, bind_form):
        form_kwargs = self.get_form_kwargs(form_name, bind_form)
        form_create_method = 'create_%s_form' % form_name
        if hasattr(self, form_create_method):
            form = getattr(self, form_create_method)(**form_kwargs)
        else:
            form = klass(**form_kwargs)
        return form

    ...

class ProcessMultipleFormsView(ProcessFormView):
    def post(self, request, *args, **kwargs):
        form_classes = self.get_form_classes()
        form_name = request.POST.get('action')
        return self._process_mixed_forms(form_classes)

    def _process_mixed_forms(self, form_classes):
        # Get the selected form type using the mixedform_selected_formtype
        form_kwargs = self.get_form_kwargs("", True)
        selected_formtype = form_kwargs['data'][self.mixedform_selected_formtype]

        # THIS IS MY LIKELY FLAWED UNDERSTANDING OF THE FOLLOWING CODE...
        # I only want to get the forms in the context of the selected formtype.  That is managed by the content of the dict passed to get_forms.  And I want that form data to be bound to kwargs.  That is accomplished by supplying the desired key in the second argument to get_forms.
        # These 2 together should result in a call to forms_valid with all the form data (including the not-selected form data - which is what we want, so that the user's entered searches are retained.
        selected_form_classes = {}
        selected_form_classes[selected_formtype] = form_classes[selected_formtype]
        formsets = self.get_forms(selected_form_classes, [selected_formtype])

        # Only validate the selected form type
        if all([form.is_valid() for form in formsets.values()]):
            return self.forms_valid(formsets)
        else:
            return self.forms_invalid(formsets)

class BaseMultipleFormsView(MultiFormMixin, ProcessMultipleFormsView):
    """
    This class is empty.
    """
 
class MultiFormsView(TemplateResponseMixin, BaseMultipleFormsView):
    """
    This class is empty.
    """

search_advanced.html 这只是与表单相关的内容(减去 javascript)。这里最麻烦的部分是我任意使用表单类之一中的单个管理表单来处理所有(当前为 2 个)类:forms.pgtemplate.errors.val, forms.pgtemplate.management_form... 这样做的缺点是隐藏的表单字段(如果未填写) 如果在执行搜索后更改了下一次搜索的输出格式,则显示验证错误。它不会影响功能 - 只是显示,因为我通过 javascript 提交表单。

    <div>
        <h3>Advanced Search</h3>

        <!-- Form template for the PeakGroups output format from django's forms.py -->
        {% with forms.pgtemplate.empty_form as f %}
            <div id="pgtemplate" style="display:none;">
                {{ f.pos }}
                {{ f.fld }}
                {{ f.ncmp }}
                {{ f.val }}
                <label class="text-danger"> {{ f.val.errors }} </label>
            </div>
        {% endwith %}

        <!-- Form template for the PeakData output format from django's forms.py -->
        {% with forms.pdtemplate.empty_form as f %}
            <div id="pdtemplate" style="display:none;">
                {{ f.pos }}
                {{ f.fld }}
                {{ f.ncmp }}
                {{ f.val }}
                <label class="text-danger"> {{ f.val.errors }} </label>
            </div>
        {% endwith %}

        <form action="" id="hierarchical-search-form" method="POST">
            {% csrf_token %}
            <div class="hierarchical-search"></div>
            <button type="submit" class="btn btn-primary" id="advanced-search-submit">Search</button>
            <!-- There are multiple form types, but we only need one set of form management inputs. We can get away with this because all the fields are the same. -->
            {{ forms.pgtemplate.errors.val }}
            {{ forms.pgtemplate.management_form }}
            <label id="formerror" class="text-danger temporal-text"></label>
        </form>
        <a id="browselink" href="{% url 'search_advanced' %}?mode=browse" class="tiny">Browse All</a>
    </div>

以下是我的具体问题:

  1. 无论有多少不同的表单集填充表单,以及使用哪个表单集类来执行实际搜索,提交所有输入的搜索表单数据的最佳方式是什么?我是否应该以某种方式使用多个 HTML 表单 - 我想这样做会导致我丢失其他表单集类型的所有其他条目。
  2. 如何提交所有搜索表单数据,但仅验证表单集类型/层次结构之一(因此我看不到未选择层次结构的验证错误)?我目前通过检查保存在 pos 字段中的表单集类型来跳过验证,由 javascript 设置。
  3. 如何为所有表单集类型提供一个管理表单?我目前在 javascript 中设置 TOTAL_FORMS 以包含所有表单集类型的所有表单集字段,以便提交所有输入的数据。
  4. 提前考虑,我希望能够根据在字段中选择的模型类型动态更改/填充valand字段(例如,如果字段是枚举,则将 val TextInput 更改为选择列表)。ncmpfld

标签: pythonhtmldjango

解决方案


推荐阅读