Django:使用会话数据管理多值POST表单字段。表单验证重定向后未传递列表。

huangapple go评论59阅读模式
英文:

Django: Managing multi-value POST form fields with session data. List not passed after form validation redirect

问题

我有一个在我的Django应用程序中的高级搜索表单,我想要使用POST方法。使用GET方法会透露一些应用程序内部工作的细节。使用POST需要一些额外的工作来确保分页功能正常。

我找到了许多相关的答案:
https://stackoverflow.com/questions/2266554/paginating-the-results-of-a-django-forms-post-request?rq=4

我决定将POST查询存储在会话数据中,以确保它在分页时可用。我将整个POST存储为QueryDict,如下所示:
https://www.abidibo.net/blog/2014/09/19/paginating-results-django-form-post-request/

出于某种原因,在成功验证表单并进行重定向后,会话数据会失去任何列表,并仅保留列表中的最后一个值作为单个项。

简化的模型(实际模型更复杂):

class Organisation(models.Model):
    name = CharField()
    location = CharField()
    industry = CharField() # 这是一个包含19个选项的choices字段
...
class OrganisationSearchForm(forms.ModelForm):
    search_org = forms.CharField(label="Organisation")
    location = forms.CharField(label="location")

    search_industry = forms.MultipleChoiceField(label="Industry", choices=[...#19个选项...])

我有两个视图来驱动搜索 - 搜索表单视图和搜索结果视图。

class OrganisationSearchFormView(FormView):

    class Meta:
        model = Organisation

    form_class = OrganisationSearchForm
    template_name = "organisation_search_form.html"
    success_url = reverse_lazy("organisation_search_results")

    def post(self, request):
        form = OrganisationSearchForm(request.POST)
        if form.is_valid():
            request.session['post-query'] = self.request.POST
            return self.form_valid(form)
        else:
            return self.form_invalid(form)    
...
class OrganisationSearchResultListView(ListView):
    model = Organisation
    template_name = "organisation_search_results.html"

    def get(self, request, *args, **kwargs):
        # 如果会话数据中没有post查询,将用户重定向回搜索表单
        if 'post-query' not in request.session:
            return HttpResponseRedirect(reverse_lazy("organisation_search"))
        return super().get(request, *args, **kwargs)

    def get_queryset(self, **kwargs):
        qs = super().get_queryset()
        post_query = QueryDict('').copy()
        post_query.update(self.request.session.get('post-query'))

        search_org = post_query.get('search_org')
        location = post_query.get('location')
        search_industry = post_query.getlist('search_industry') # .getlist用于多值对象。

        qs = Organisation.objects.all()

        # 根据查询参数过滤查询集

这里的工作流程是,表单视图提交(POST方法)进行验证。如果表单有效,将QueryDict对象存储在会话数据中。随后,自动重定向(GET方法)到搜索结果视图。

表单视图 -> 表单视图(验证) -> 结果视图

在重定向期间,QueryDict内的任何列表都被削减为单个值。我在表单视图的POST方法的验证块中以及结果视图的get方法中放置了打印语句,显示了这种变化:

app-web-1  | [2023-07-17 15:31:03 +0000] [7] [DEBUG] POST /advsearch/i/
app-web-1  | <QueryDict: {'csrfmiddlewaretoken': ['************'], 'search_org': [''], 'search_industry': ['11', '21', '22', '23'], 'location': ['']}>
app-web-1  | [2023-07-17 15:31:04 +0000] [7] [DEBUG] GET /sr/i/
app-web-1  | <QueryDict: {'csrfmiddlewaretoken': ['************'], 'search_org': [''], 'search_industry': ['23'], 'location': ['']}>

我认为这个问题与在该链中某处使用字典而不是QueryDict有关:
https://stackoverflow.com/questions/39565023/django-querydict-only-returns-the-last-value-of-a-list

如果我将表单操作直接更改为直接POST到搜索结果视图,我就不会遇到同样的问题,列表会保留并存在于会话数据中。即:
表单视图 -> 结果视图

然而,我认为直接POST到结果视图可能会错过表单验证步骤。这个理解正确吗?

解决此问题的最佳方法是什么?
是否有一种方法可以在以下工作流程中保留列表:表单视图 -> 表单视图(验证) -> 结果视图?

任何建议或指导都将不胜感激。

英文:

I have an advanced search form in my Django app that I want to use the POST method with. Using GET reveals too much info about some inner workings of the app. Using POST requires some extra work to ensure pagination functions correctly.

I've found many great related answers:
https://stackoverflow.com/questions/2266554/paginating-the-results-of-a-django-forms-post-request?rq=4

I decided to store the POST query in session data to ensure it is available for pagination. I'm storing the entire POST as a QueryDict in the session as per:
https://www.abidibo.net/blog/2014/09/19/paginating-results-django-form-post-request/

For some reason after a successful validation of the form and subsequent redirect the session data loses any lists and retains just the last value in the list as a single item.

The simplified model (the actual model is much more complex):

class Organisation(models.Model):
    name = Charfield()
    location = Charfield()
    industry = Charfield() # This is a choices field with 19 options
...
class OrganisationSearchForm(forms.ModelForm):
    search_org = forms.CharField(label=&quot;Organisation&quot;)
    location = forms.Charfield(label=&quot;location&quot;)

    search_industry = forms.MultipleChoiceField(label=&quot;Industry&quot;, choices=[...#19 choices enumerated...])

I have two views with drive the search - a search form view and a search results view.

class OrganisationSearchFormView(FormView):

    class Meta:
        model = Organisation

    form_class = OrganisationSearchForm
    template_name = &quot;organisation_search_form.html&quot;
    success_url = reverse_lazy(&quot;organisation_search_results&quot;)

    def post(self, request):
        form = OrganisationSearchForm(request.POST)
        if form.is_valid():
            request.session[&#39;post-query&#39;] = self.request.POST
            return self.form_valid(form)
        else:
            return self.form_invalid(form)    
...
class OrganisationSearchResultListView(ListView):
    model = Organisation
    template_name = &quot;organisation_search_results.html&quot;

    def get(self, request, *args, **kwargs):
        # if there is not post query in the session data, redirect user back to the search form
        if &#39;post-query&#39; not in request.session:
            return HttpResponseRedirect(reverse_lazy(&quot;organisation_search&quot;))
        return super().get(request, *args, **kwargs)

    def get_queryset(self, **kwargs):
        qs = super().get_queryset()
        post_query = QueryDict(&#39;&#39;).copy()
        post_query.update(self.request.session.get(&#39;post-query&#39;))

        search_org = post_query.get(&#39;search_org&#39;)
        location = post_query.get(&#39;location&#39;)
        search_industry = post_query.getlist(&#39;search_industry&#39;) # .getlist is required for a multivalue object.

        qs = Organisation.objects.all()

        # I filter the queryset based on the query parameters

The workflow here is that the form view submits (POST method) to itself for validation. If the form is valid the QueryDict object is stored in the session data. Subsequently, there is an automatic redirect (GET method) to the Search Results View.

Form View -> Form View (Validation) -> Results View

During that redirect any lists within the QueryDict are stripped down to a single value. I've put a print statement in the form validation block of the form view post method and also in the get method of the results view which shows the change:

app-web-1  | [2023-07-17 15:31:03 +0000] [7] [DEBUG] POST /advsearch/i/
app-web-1  | &lt;QueryDict: {&#39;csrfmiddlewaretoken&#39;: [&#39;************&#39;], &#39;search_org&#39;: [&#39;&#39;], &#39;search_industry&#39;: [&#39;11&#39;, &#39;21&#39;, &#39;22&#39;, &#39;23&#39;], &#39;location&#39;: [&#39;&#39;]}&gt;
app-web-1  | [2023-07-17 15:31:04 +0000] [7] [DEBUG] GET /sr/i/
app-web-1  | &lt;QueryDict: {&#39;csrfmiddlewaretoken&#39;: [&#39;************&#39;], &#39;search_org&#39;: [&#39;&#39;], &#39;search_industry&#39;: [&#39;23&#39;], &#39;location&#39;: [&#39;&#39;]}&gt;

I believe the issue is related to the use of a dict rather than QueryDict being used somewhere in that chain:
https://stackoverflow.com/questions/39565023/django-querydict-only-returns-the-last-value-of-a-list

If I change the form action to POST directly to the search results view, I do not have the same problem, the lists are retained and present in the session data. i.e.
Form View -> Results View

However, I think by posting directly to the results view that the form validation step is being missed. Is this correct?

What is the best approach to resolve this issue?
Is there a way to retain the list using the: Form View -> Form View (Validation) -> Results View workflow?

Any advice or guidance appreciated.

答案1

得分: 1

以下是翻译好的部分:

我遇到了一个类似的问题,并找到了一个解决方案,我认为这个解决方案也可能对你有帮助。

总结一下:

  • 当你将request.POST保存到request.session时,Django会对其进行序列化,这个过程中会导致request.POST中包含多个值的字段被截断为一个值。根据你的示例,'search_industry': ['11', '21', '22', '23']会变成'search_industry': ['23']
  • 为了避免在序列化过程中丢失数据,你可以编写自己的函数,在调用request.session.save()之前对request.POST中的多值字段进行序列化。
  • 在下一个视图中从会话中获取发布的数据时,你需要对多值字段进行反序列化。
  • 序列化和反序列化函数都需要知道request.POST中与多值字段相关联的键。为此,我在表单中添加了一个隐藏输入,列出了所有多选字段。

以下是我用于序列化和反序列化request.POST中多值字段的函数:

def serialize_requestPOST_multivalue_items(requestPOST):  # 传入request.POST
    new_requestPOST = requestPOST.copy()  # 可变的QueryDict

    for key in requestPOST.getlist("multi_value_keys"):
        value = json.dumps(requestPOST.getlist(key))
        new_requestPOST.__setitem__(key, value)
    
    # new_requestPOST["multi_value_keys"]的值也需要序列化。
    new_requestPOST.__setitem__(
        "multi_value_keys", json.dumps(requestPOST.getlist("multi_value_keys"))
    )

    return new_requestPOST


def deserialize_requestPOST_multivalue_items(requestPOST):
    requestPOST["multi_value_keys"] = json.loads(requestPOST["multi_value_keys"])

    for key in requestPOST["multi_value_keys"]:
        requestPOST[key] = json.loads(requestPOST[key])

    return requestPOST

以下是在需要将request.POST保存到request.session选项时添加到表单的混合类:

class SerializerPrereqMixin:
    def add_multi_values_hidden_field(self):
        multi_value_keys = [
            field_key for field_key, field in self.fields.items() 
            if isinstance(
                field.widget, (SelectMultiple, CheckboxSelectMultiple)
            )
        ]
        self.fields["multi_value_keys"] = CharField(
            widget=MultipleHiddenInput(),
            required=False,
            initial=multi_value_keys,
        )

有了这个,在第一个视图中,你可以这样做:

requestPOST = serialize_requestPOST_multivalue_items(request.POST)
request.session["posted_data"] = requestPOST
request.session.save()

在你想要使用发布的数据的视图中:

posted_data = request.session.get("posted_data")
posted_data = deserialize_requestPOST_multivalue_items("posted_data")
英文:

I came across a similar problem and worked out a solution that I think might also help in your case.

In summary:

  • When you save request.POST to request.session, django serializes it and something about
    that process causes any fields in request.POST that contained multiple values to be
    truncated down to one value. Per your example, &#39;search_industry&#39;: [&#39;11&#39;, &#39;21&#39;, &#39;22&#39;, &#39;23&#39;]
    becomes &#39;search_industry&#39;: [&#39;23&#39;].
  • To avoid losing data in that serialization process, you can write your own function to
    serialize any multi-value fields in request.POST before calling request.session.save().
  • When getting the posted data out of the session in the next view, you would have to
    deserialize the values for any multi-value fields.
  • Both the serialization and deserialization functions will need to know which keys in
    request.POST are associated with multi-value fields. For that, I added a hidden input
    to the form that lists all multiselect fields.

Here are my functions for serializing and deserializing multi-value fields in requset.POST:

def serialize_requestPOST_multivalue_items(requestPOST):  # pass in request.POST
    new_requestPOST = requestPOST.copy()  # a mutable QueryDict

    for key in requestPOST.getlist(&quot;multi_value_keys&quot;):
        value = json.dumps(requestPOST.getlist(key))
        new_requestPOST.__setitem__(key, value)
    
    # The value of new_requestPOST[&quot;multi_value_keys&quot;] also needs to be serialized.
    new_requestPOST.__setitem__(
        &quot;multi_value_keys&quot;, json.dumps(requestPOST.getlist(&quot;multi_value_keys&quot;))
    )

    return new_requestPOST


def deserialize_requestPOST_multivalue_items(requestPOST):
    requestPOST[&quot;multi_value_keys&quot;] = json.loads(requestPOST[&quot;multi_value_keys&quot;])

    for key in requestPOST[&quot;multi_value_keys&quot;]:
        requestPOST[key] = json.loads(requestPOST[key])

    return requestPOST

Here's a mixin to add to forms when you'll want the option of saving request.POST to request.session:

class SerializerPrereqMixin:
    def add_multi_values_hidden_field(self):
        multi_value_keys = [
            field_key for field_key, field in self.fields.items() 
            if isinstance(
                field.widget, (SelectMultiple, CheckboxSelectMultiple)
            )
        ]
        self.fields[&quot;multi_value_keys&quot;] = CharField(
            widget=MultipleHiddenInput(),
            required=False,
            initial=multi_value_keys,
        )

With this, in the first view you would so something like:

requestPOST = serialize_requestPOST_multivalue_items(request.POST)
request.session[&quot;posted_data&quot;] = requestPOST
request.session.save()

And in the view where you want to used the posted data:

posted_data = request.session.get(&quot;posted_data&quot;)
posted_data = deserialize_requestPOST_multivalue_items(&quot;posted_data&quot;)

huangapple
  • 本文由 发表于 2023年7月18日 01:05:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76706665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定