Django - Two forms on one page, how can I maintain URL parameters when either form is submitted?

I'm building an application that contains a list of meals, where each meal has various filters, a price, and a rating.

The filters are like tags; the user can select multiple, and the page only shows the meals that have the selected filters.

The price and ratings are integers, and the user can sort by either price or rating, which sorts the meals (cheapest -> most expensive for price, highest -> lowest for rating).

I have built two forms in Django, one for filters and one for sorting, and they both work on their own. However, let's say I submit the sorting form to sort by price; when I do this, it does sort by price, but it removes all of the prior filters I had submitted.

Below are the important pieces of code relevant to this problem:

views.py

def meals(request):
    meal_list = Meal.objects.all()
    tags = Tag.objects.all()
    reviews = Review.objects.all()
    filter_form = FilterForm(request.GET or None)
    sorting_form = SortingForm(request.GET or None)
    sort = ""
    active_filters = []

    if filter_form.is_valid():
        tags = filter_form.cleaned_data.get('tags')
        for tag in tags:
            meal_list = meal_list.filter(tags__name=tag)
            active_filters.append(tag)

    if sorting_form.is_valid():
        sort = sorting_form.cleaned_data.get('sort')
        if sort == "price":
            meal_list = meal_list.order_by('price')
        else:
            meal_list = meal_list.order_by('-rating')

    paginator = Paginator(meal_list, 8)
    page_number = request.GET.get('page')
    meals_on_page = paginator.get_page(page_number)

    context = {"meal_list": meal_list,
               "distances": distances,
               "tags": tags,
               "reviews": reviews,
               "active_filters": active_filters,
               "meals_on_page": meals_on_page,
               "filter_form": filter_form,
               "sorting_form": sorting_form,
               }
    return render(request, 'meals/meals.html', context)

forms.py

from django import forms

# Tag is the model for the filters, it is just a ManyToManyField that contains a name attribute
from .models import Tag


class FilterForm(forms.Form):
    tags = forms.ModelMultipleChoiceField(
        queryset=Tag.objects.all(), widget=forms.CheckboxSelectMultiple)


class SortingForm(forms.Form):
    SORT_CHOICES = [
        ('price', 'Price'),
        ('rating', 'Rating'),
    ]
    sort = forms.ChoiceField(choices=SORT_CHOICES, widget=forms.Select)

meals.html

  <form method="get">
    {% for field in filter_form %}
        {{ field.as_widget }} {{ field.label_tag }}
    {% endfor %}
    <input type="submit" value="Filter">
  </form>


  <form method="get">
    {% for field in sorting_form %}
        {{ field.as_widget }}
    {% endfor %}
    <input type="submit" value="Sort">
  </form>

I have sadly way too long trying to fix this, and the closest I got was using get_copy = request.GET.copy() and then trying to manually add the URL parameters back onto the end of a URL after a form was submitted. However, none of my approaches using this seemed to work.

Thanks in advance for the help!

In your Django view, you can access the current URL parameters using the request object's GET attribute. To maintain these parameters when either form is submitted, you can include them in the form action attribute in your template.

For example, in your template, you can update the form action attribute to include the current URL parameters like this:

<form method="get" action="{% url 'meals' %}?{{ request.GET.urlencode }}">

This will append the current URL parameters to the form action, so when the form is submitted, the current parameters will be included in the request.

In your view, you can then access these parameters using the request.GET dictionary. You can use these parameters to filter and sort your queryset accordingly before rendering the template.

Note: you should also check if the forms are valid before processing the form data to avoid unexpected behavior.

Also, you can use Django's forms.HiddenInput() to include the current parameters on your forms as hidden fields, that way you don't need to update form's action attribute.

I ended up solving this after soaking (way too many) more hours into it.

The way I did this was to build my own dictionary of parameters, and then to pass these parameters into the forms as hidden inputs.

Below is the added code:

views.py

get_copy = request.GET.copy()
parameters = get_copy.urlencode()
get_copy.pop('page', None)
param_list = parameters.split("&")
param_dict = defaultdict(list)
for param in param_list:
    try:
        key, value = param.split("=")
    except ValueError:
        # Handle the case where there's no "=" in the parameter string
        key = param
        value = ""
    if key == "tags":
        param_dict[key].append(int(value))
    else:
        param_dict[key].append(value)
# Django requires me to turn dictionary to items here, rather than in the template
param_dict_items = param_dict.items()

meals.html

<form method="get" action="{% url 'meals' %}?{{ request.GET.urlencode }}">
{% for field in filter_form %}
    {{ field.as_widget }} {{ field.label_tag }}
{% endfor %}

{% for key, value_list in param_dict_items %}
  {% if key != 'tags' %}
    {% for value in value_list %}
      <input type="hidden" name="{{ key }}" value="{{ value }}">
    {% endfor %}
  {% endif %}
{% endfor %}
<input type="submit" value="Filter">
<form method="get" action="{% url 'meals' %}">
{% for field in sorting_form %}
    {{ field.as_widget }}
{% endfor %}

{% for key, value_list in param_dict_items %}
  {% if key != 'sort' %}
    {% for value in value_list %}
      <input type="hidden" name="{{ key }}" value="{{ value }}">
    {% endfor %}
  {% endif %}
{% endfor %}
<input type="submit" value="Sort">
Back to Top