Django Form Pagination

Hi everyone, i’m new here.
So, i’m working with Django since last year, and i’m still learning working with it. It’s a great framework, but there’s a lot to figure out.

What i’am trying to do, is a pagined form.
Right now, my form is working, the pagination is working, even the filter form to reduce overall choices works.
But i struggle to have a consistent overall form.
If i check options whithin the first page, i loose those options when changing page.
Wich means, i’m not abble to navigate efficiently throught all pages, and i will not able to validate the whole options trought all pages too.

I’m working on a pretty big project, so my basecode is splitted in many classes and files.
I share, what i suppose to be the more obvious to work with:

First, the form code:

class OrderProductForm(forms.Form):
    ORDER = []
    PRODUCT_CHOICES = []
    order = forms.ChoiceField(
        widget=forms.Select(attrs={"class": "form_select"}),
        label="Commande",
        required=True,
    )
    product = forms.MultipleChoiceField(
        widget=forms.CheckboxSelectMultiple(attrs={"class": "form_checkbox"}),
        label="Produits disponibles",
        required=True,
    )

The view code:

class AvailableProducts(Base, CreationView):
    search_form_class = ProductSearchForm
    product_collection = None
    order = None
    ITEMS_PER_PAGE = 23

    def __init__(self):
        super().__init__()
        self.product_data = ProductData()

    def _get_template_path(self):
        template_path = self.template_path_creator.create_available_products_path()
        return template_path

    def add_order_uuid_to_context_dictionary(self, order):
        self._update_context_dictionary({"order_uuid": order.id})

    def get_order(self, order_id):
        order = Order.objects.filter(id=order_id)
        return order

    def get_available_products(self):
        self.product_collection = self.product_data.get_product_queryset()

    def add_product_collection_to_context_dictionary(self, product_collection):
        self._update_context_dictionary({"product_collection": product_collection})

    def make_product_dictionary_from_product_collection(self, product_collection):
        product_dictionary = {}
        for product in product_collection:
            product_dictionary[product.id] = {                
                "chip_number": product.chip_number,                
            }
        return product_dictionary

    def add_product_dictionary_from_product_collection_to_context_dictionary(
        self, product_collection
    ):
        product_dictionary = self.make_product_dictionary_from_product_collection(
            product_collection
        )
        self._update_context_dictionary({"product_dictionary": product_dictionary})

    def apply_product_choices(self, form, product_collection):
        product_choices = [(product.id, str(product.birth_number)) for product in product_collection]
        form.fields["product"].choices = product_choices
        return form

    def apply_order(self, form, order):
        order_choices = []
        if order:
            for ord in order:
                order_choices.append((ord.id, str(ord)))
                self.add_order_uuid_to_context_dictionary(ord)
        form.fields["order"].choices = order_choices
        form.fields["order"].initial = order
        return form

    def set_configuration_form(self, request_post=None):
        form = OrderProductForm(request_post)
        page_obj = self.handle_pagination(
            collection=self.product_collection, collection_name="product_collection"
        )
        form = self.apply_product_choices(form, product_collection=page_obj)
        form = self.apply_order(form, self.order)
        self.add_product_collection_to_context_dictionary(product_collection=page_obj)
        self.add_product_dictionary_from_product_collection_to_context_dictionary(
            product_collection=page_obj
        )
        return form

    def set_configuration_form_GET(self, *args, **kwargs):
        return self.set_configuration_form()

    def set_configuration_form_POST(self, request_post, *args, **kwargs):
        return self.set_configuration_form(request_post=request_post)

    def handle_request_parameters(self, request_parameters):
        result = None
        parameters = self._decode_request_parameters(request_parameters)
        self.order = self.get_order(parameters["order_id"])
        if self.request.method == "GET":
            result = self.handle_get()
        else:
            result = self.handle_post()
        return result

    def apply_filters(self, search_form):
        if search_form.is_valid():
            self.product_data.apply_chip_number_filter(
                search_form.cleaned_data["chip_number"]
            )            
            self.product_data.apply_status_filter(search_form.cleaned_data["status"])
            self.product_data.apply_exit_date_none_filter()
            self.product_data.apply_order_none_filter()

    def get(self, request, order_parameters=None, *args, **kwargs):
        self.search_form = self.search_form_class(request.GET)
        self._update_context_dictionary({"search_form": self.search_form})
        self.apply_filters(self.search_form)
        self.get_available_products()
        request_parameters = order_parameters
        return super().get(request, request_parameters, *args, **kwargs)

    def _insert_model_instance_form_to_database(self):
        product_instance = None
        self.form = self.set_configuration_form_POST(self.request.POST)
        if self.form.is_valid():
            selected_order = self.form.cleaned_data["order"]
            order_instance = Order.objects.get(pk=selected_order)
            selected_product_ids = self.form.cleaned_data["product"]
            selected_products = Product.objects.filter(id__in=selected_product_ids)
            for product_instance in selected_products:
                product_instance.order = order_instance
                product_instance.save(
                    user=self.request.user, comment="Ajouté à la commande"
                )
        return product_instance

    def post(self, request, order_parameters=None, *args, **kwargs):
        self.search_form = self.search_form_class(request.GET)
        self._update_context_dictionary({"search_form": self.search_form})
        self.apply_filters(self.search_form)
        self.get_available_products()
        request_parameters = order_parameters
        return super().post(request, request_parameters, *args, **kwargs)

Template code:

<div class="container"> {% create_order_parameters order_uuid as order_parameters %} {% include "product_production/order/sidebar.html" %} <div class="main-content"> <div class="subsection"> {% include "base/pagination.html" with collection=product_collection %}

<form method="post">
            {% csrf_token %}
            <div class="subsection">  
                {{ model_form.order }}                                        
            </div>

            <div class="subsection">  
                <table class="product_table">
                    <thead class="product_thead">
                        <tr>
                            <th class="col_select">Sélection</th>                            
                            <th class="col_chip_number">n° Puce / Nom</th>                            
                        </tr>
                    </thead>
                    <tbody>
                        {% for product in model_form.product %}
                            <tr>
                                {% get_product_details product.data.value product_dictionary as product_details %}
                                <td class="col_select">{{ product.tag }}</td>                                
                                <td class="col_chip_number">{% get_product_detail "chip_number" product_details %}</td>                                
                            </tr>
                        {% endfor %}
                        
                    </tbody>        
                </table>
                {% if model_form.product.errors %}
                    <div class="error">
                        {{ model_form.product.errors }}
                    </div>
                {% endif %}
            </div>
            {% include "base/creation_submit.html" %}
        </form>

        {% include "base/pagination.html" with collection=product_collection %}
    </div>
</div>

Thank you!

Welcome @KMBCL !

Side Note: When posting code here, enclose the code between lines of three
backtick - ` characters. This means you’ll have a line of ```, then your code,
then another line of ```. This forces the forum software to keep your code
properly formatted. (I have taken the liberty of correcting your original posts.
Please remember to do this in the future.)

Can you clarify what you mean by a “paginated form”? (The form you’re showing doesn’t appear to be big enough to require multiple pages.) Perhaps also provide a more detailed explanation of what it is you’re trying to do here?

Hi @KenWhitesell !
Well thank your for the wellcoming. I will remember the quotes trick to display the code correctly. Roger Roger!

Yes, this is because, for confidentiality purpose, i have replaced many names, to more conventional ones. So it is not very explicit.
In fact, the choices are dynamic each time the page is reloaded. It can be 10, or 5000. So i wanted to paginate displayed choice, for UI consistency, and template render performance (5000 is up to 6-7 secondes to display, wich is too long). By the way, SQL performance are fine (less than 0.7 secs).
To the user be efficient, first he has to filter wathever he wants, to reduce the choices to the most obviouses, and check everyone he wants, trought multiple pages, if available.
Finally, he submitts the form, wich is validated, and the data base is updated.

This morning, i have tried another approch, wich is an hybrid one: each time the form is validated, we stay in the same page, and choices are refreshed.
Wich means:

  • selected, and submitted elements, doesn’t display anymore
  • global filters remain
  • the user stays in the same page, if multiple.

To achieve this, i modified the POST redirection function, wich now, act the same as the get view use to change page (or just refreshing), with a forced available choices update.

So, in conclusion: i can scroll pages, check wanted items, submitt them, stay in the same page, and go on trought the other pages.
But if i check one, and change the page, checked is lost. Choices must be validated before changing page.

It is not what i have imagined in the first place, but the whole behaviour of the code, views is consistent with the other parts of the project.

But perhaps there is a better solution?

Very interesting.

Restating this to try and ensure I understand the situation:

  • You have one form that may contain n checkboxes.
  • The list identifying the n options is dynamic and chosen based upon some filter criteria.
  • The number n is sufficiently large such that they can’t effectively all be rendered on the same page.
    • This means that some subset of n is shown on each page, and the user must be able to switch freely among those sets.

It is not clear from the description provided so far whether the only form elements are the members of n, or if there are some form elements above n that don’t get switched by page. (The answer doesn’t matter much at this level of detail.)

If I am understanding the situation correctly, then my gut reaction is that I’m looking at either 2 or 3 views here. I’m definitely not thinking of trying to handle all this in one view.

One view handles the initial page GET. It also would handle initializing the “persistence layer”, which could be the session or it could be a temporary holding model. (The choice between them probably depends more upon factors outside this specific question.) Either way, the idea is to hold the selections made when switching between pages.

Alternative 1:

The second view is the pagination view. Changing pages requires a POST and not a GET to be able to save the selections made so far. Changing pages issues a POST to this second view, which saves which options have been selected and builds the next page to be viewed - applying selections that have been previously made.

Then, on a submit of the form, that view (either the POST handler of the original view or a third view) accepts the entries made on the last page seen, and builds and validates the form with that data and the data previously saved in the persistence layer.

Alternative 2

Add some JavaScript to the page (your own or something like HTMX) such that when each checkbox is clicked, that update is sent to the second view to update the persistence layer. That allows the pagination to send a GET instead of a POST to change pages - but that second view is still needed to properly prepare those other pages. (If there is other data on that page to be maintained, then JavaScript / HTMX may also be good options for this, too.)

The submit for that form works the same way as for Alternative 1.

Yes! It is exactly like that!
Again, thank you for your time :slight_smile:
I provide a screenshot:

Step by step, the idea:
From the Order detail page, user clicks OrderProduct shortcut, to get here (screenshot). This terms are still aliases, but the purpose is pretty much the same.
There is a first serverside filter applyed to the first bunch of choices.

The page contains two forms (maybe this point is a mistake of conception):

  • First form is on the left panel, it is a search form to filter with more precisions available choices (GET method > again, mabye a mistake). The search form is persistant, i succed to manage the search parameters and apply them each time the page is refreshed (or changing page), even when the main form is submitted.
  • Second form is the main form with those choices to validate and pages to scroll.

User has two possibilities, he scrolls trought the 120 results (or maybe much more). Each time user clicks on pagination buttons, GET is called. Or he can apply more filters (again, GET is called) to reduce available choices.
All those possibilites are in the same view for user workflow.

The first solution is the more consistent in my opinion (i avoid to use Javascript, if not really needed). And in fact, was one of the idea i get at some point. But failed to make it work. Specially managing two forms in the same page. One call GET, the other call POST.
How will you manage a page changing with POST?

There is the pagination HTML:

<div id="pagination" class="pagination">
    <span class="step-links">
        <a href="?page=1&{{ query_params }}" class="pagination_shortcut {% if not collection.has_previous %}disabled{% endif %}">&laquo; Premier</a>
        
        {% if collection.has_previous %}
            <a href="?page={{ collection.previous_page_number }}&{{ query_params }}" class="pagination_shortcut {% if not collection.has_previous %}disabled{% endif %}">Précédent</a>
        {% else %}
            <a  class="pagination_shortcut disabled">Précédent</a>
        {% endif %}

        <span class="current">
            {{ start_record }}-{{ end_record }} / <strong>{{ total_records }}</strong>
        </span>

        {% if collection.has_next %}
            <a href="?page={{ collection.next_page_number }}&{{ query_params }}" class="pagination_shortcut">Suivant</a>
        {% else %}
            <a  class="pagination_shortcut disabled">Suivant</a>   
        {% endif %}
        <a href="?page={{ collection.paginator.num_pages }}&{{ query_params }}" class="pagination_shortcut {% if not collection.has_next %}disabled{% endif %}">Dernier &raquo;</a>
    </span>
</div>

Nope, not a mistake from my perspective. This is probably how I would do it. But there’s no need to try to force this functionality into the same view as everything else.

You can avoid JavaScript, in which case you’re really going to want to make this at least two different views.

It may help you to mentally separate these different “steps” or “phases” into separate pieces. They are different things you want to have happen - which generally translates into different views.

This is an unnecessary and artificial constraint that you are trying to apply here, making your implementation a lot more difficult than it needs to be. There is nothing within Django that forces a connection between a “view” and a “page”. Pages can (and frequently do) result in any number of different views being invoked. (Think of a menu - each menu entry would typically dispatch to a different view - which all may be different than the page currently being viewed.)

There’s a lot of this that can be simplified by breaking it down into its individual steps.

Again - different view. That view accepts the selections currently made, saving them to be retrieved later. That view also then has the responsibility of creating the updated page.

Using JavaScript makes this a whole lot easier, because you can save and update only the portion of the page that needs to be changed. The left panel can remain unchanged.

Of course, as a result of this, you will want to produce your own pagination logic. (The HTML is ok, it’s the underlying functionality that needs to be customized.) I do not see where Django’s Pagination object is going to be useful without a lot of customization. It’s designed around a different use-case.

Well, thank you again for your reply and your time.
It is very interesting to confront ideas with someone else (i am a self-taught developper, alone in my company to make software and usefull tools).
It is the first time i share my work and ideas online.
I will work on this and fine tune this component.
I suppose the subject is closed with its solution.

Only if you want it to be.

If you get stuck along the way with getting this working, we’re still here to help.

1 Like