Как лучше всего отправить несколько форм Django formset_factory одновременно?

#python #html #django

Вопрос:

Это, вероятно, слишком широкий/сложный вопрос для стека, но мне пришлось пройти через процесс, чтобы задать этот вопрос, чтобы прояснить свои мысли. Я очень открыт для предложений о том, как его разбить или сузить. Банкомат, я не уверен, как это сделать без всего этого контекста…

Я довольно новыми для Django, и я только что закончил доказательство концепции странице «расширенный поиск», которая заняла 2 недели для меня, чтобы работать, как я хочу — и это работает (очень хорошо, на самом деле), но это очень kludgey, и теперь у меня есть доказательство концепции работает, я заинтересован в том, чтобы научиться делать это правильно, и я ищу предложения о том, как реструктурировать это, чтобы сделать его более понятным. Для обобщения функциональности интерфейс позволяет создавать сложные поисковые запросы с использованием динамической иерархической формы. Это выполняется (в настоящее время) для 1 из 2 выходных форматов (что фактически приводит к отображению соединения 5 или 6 таблиц в модели, в зависимости от выбранного выходного формата), но он отправляет все иерархии и выполняет поиск по выбранному. Вот gif-демонстрация интерфейса, чтобы сделать его, возможно, более понятным. Обратите внимание, что в gif поиск выполняется только по иерархии поисковых терминов «PeakData», которая отображается при отправке. Условия поиска «Группы пик» (скрытые при отправке) игнорируются (но сохраняются и по-прежнему доступны на странице результатов).:

рабочая демонстрация кода kludgey

То, как я это сделал, хотя и кажется действительно странным. Я в порядке с компонентом javascript, так как он кажется мне довольно простым. Вкратце, он генерирует иерархическое отображение, список выбора выходного формата и списки выбора «Сопоставить все/любые». Он скрывает/отображает иерархии на основе выбранного формата вывода и преобразует иерархические данные в значение, хранящееся в скрытом поле набора форм (именованное pos — присутствует во всех классах форм). Поскольку это может сбивать с толку из gif, я должен отметить, что на самом деле существует 2 полных набора иерархических структур наборов форм, которые по сути выглядят одинаково (но содержат разные типы наборов форм). (Если это будет полезно для ясности, я мог бы сделать еще один gif со всеми иерархиями, видимыми одновременно.)

It’s the python/Django code that seems like a hack. Here are the highlights:

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
This code was originally obtained elsewhere and I modified it to process different formsets inside the same HTML form — because I want to save all entered search terms in every filled out hierarchy behind the scenes, so that if they change the output format select list value, all their previously entered search terms are still there. The javascript keeps track of it all in an object and upon submit, it saves the hierarchy amp; formset entries in the hidden pos field above. This multiforms.py is the kludgiest part. I’ll try to only include the important bits…

 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
This is just the form-related content (minus the javascript). The kludgiest part here is the fact that I arbitrarily use a single management form from one of the form classes to handle all the (currently 2) classes: forms.pgtemplate.errors.val , forms.pgtemplate.management_form … The downside of this is that the hidden form fields (if not filled out) show a validation error if after performing a search, they change output formats for the next search. It doesn’t affect the functionality — just the display, since I submit the form via 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>
 

Here are my specific questions:

  1. What’s the best way to submit all the entered search form data regardless of how many different formsets populate the form, and which formset class is being used to perform the actual search? Should I somehow be using multiple HTML forms — I imagine doing that would cause me to lose all the other entries of the other formset types.
  2. How do I submit all the search form data, but only validate one of the formset types/hierarchies (so I don’t see validation errors for the unselected hierarchies)? I currently skip validation by checking the formset type saved in the pos field, set by the javascript.
  3. Как я могу предоставить единую форму управления для всех типов наборов форм? В настоящее время я устанавливаю TOTAL_FORMS в javascript для включения всех полей набора форм всех типов наборов форм, чтобы отправить все введенные данные.
  4. Забегая вперед, я хочу иметь возможность динамически изменять/заполнять поля val и ncmp в зависимости от типа модели, выбранного в fld поле (например, измените ввод текста val на список выбора, если поле является перечислением).