Paginate Django formset
Asked Answered
R

5

9

I have a model formset that I want to display 10 forms at a time using Django's Paginator, but it can't be done like paginator = Paginator(formset, 10). What's the correct way to do this, if there is a way?

Ravin answered 26/12, 2012 at 13:38 Comment(1)
I spend a lot of time trying to figure out this problem too. Turns out my (our) thinking is completely wrong. Why paginate a formset? you can only edit a single page at a time, right? So, paginate your whole query as usual and make formsets for a single page only! Boom. https://code.djangoproject.com/ticket/30632Chapter
R
17

This is a generic example of the solution I found to my problem:

In the forms.py file:

class MyForm(ModelForm):
    class Meta:
        model = MyModel
        fields = ('description',)

In the views.py file:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

FormSet = modelformset_factory(MyModel, form=MyForm, extra=0)
if request.method == 'POST':
    formset = FormSet(request.POST, request.FILES)
    # Your validation and rest of the 'POST' code
else:
    query = MyModel.objects.filter(condition)
    paginator = Paginator(query, 10) # Show 10 forms per page
    page = request.GET.get('page')
    try:
        objects = paginator.page(page)
    except PageNotAnInteger:
        objects = paginator.page(1)
    except EmptyPage:
        objects = paginator.page(paginator.num_pages)
    page_query = query.filter(id__in=[object.id for object in objects])
    formset = FormSet(queryset=page_query)
    context = {'objects': objects, 'formset': formset}
    return render_to_response('template.html', context,
                              context_instance=RequestContext(request))

You need to create the formset with the objects in the present page, otherwise, when you try to do formset = FormSet(request.POST, request.FILES) in the POST method, Django raises a MultiValueDictKeyError error.

In the template.html file:

{% if objects %}
    <form action="" method="post">
        {% csrf_token %}
        {{ formset.management_form }}
        {% for form in formset.forms %}
            {{ form.id }}
            <!-- Display each form -->
            {{ form.as_p }}
        {% endfor %}
        <input type="submit" value="Save" />
    </form>

    <div class="pagination">
        <span class="step-links">
            {% if objects.has_previous %}
                <a href="?page={{ objects.previous_page_number }}">Previous</a>
            {% endif %}

            <span class="current">
                Page {{ objects.number }} of {{ objects.paginator.num_pages }}
            </span>

            {% if objects.has_next %}
                <a href="?page={{ objects.next_page_number }}">next</a>
            {% endif %}
        </span>
    </div>
{% else %}
    <p>There are no objects.</p>
{% endif %}
Ravin answered 2/1, 2013 at 20:41 Comment(1)
Emphasis on "You need to create the formset with the objects in the present page". You can and will get errors other than MultiValueDictKeyErrorNubilous
N
3

A more elegant solution is to set ordered=True on the Page object so that it can be passed to a ModelFormSet.

Here is an example:

forms_per_page = 10
current_page = 1

ModelFormSet = modelformset_factory(MyModel, form=MyForm)
queryset = MyModel.objects.all()

paginator = Paginator(queryset, forms_per_page)
page_object = paginator.page(current_page)
page_object.ordered = True

form = ModelFormSet(queryset=page_object)

This is more efficient than the accepted answer because avoids the second database query that takes place in the line:

page_query = query.filter(id__in=[object.id for object in objects])
Nubilous answered 29/12, 2019 at 3:6 Comment(1)
Could you provide a more complete example since you are comparing your answer to the accepted answer?Oligocene
D
2

More correct way to use this

...
formset = FormSet(queryset=page_query.object_list)
...
Ding answered 15/3, 2013 at 14:33 Comment(3)
Can you elaborate on this? Looks interesting.Epoch
This will not work, because BaseModelFormSet require QuerySet object, and will fail with listMalfunction
This results into the following error: Cannot filter a query once a slice has been taken. @GillBates: object_list returns a QuerySet, not a list. So use @Filly's answer!Columbary
Q
0

The problem here is that you're using brands (a Page) in a context that's expecting a QuerySet. So, we need that damn QuerySet. You are in right way, but a lot of code.

In source code we have:

class Page(collections.Sequence):

    def __init__(self, object_list, number, paginator):
        self.object_list = object_list
        self.number = number
        self.paginator = paginator
        ...

So, our queryset in self.object_list attribute and just use it!

formset = SomeModelFormSet(queryset=objects.object_list)
Quadriga answered 28/12, 2014 at 0:4 Comment(0)
A
0

Agree with Elrond Supports Monica. Fake attribute is interesting way to resolve the ordering error (Cannot reorder a query once a slice has been taken.)

But it can be fixed in one line also queryset = queryset.order_by(Entry._meta.pk.name)

This fake ordering is need for avoid error in django.form.modelsBaseModelFormSet(BaseFormSet).get_queryset(): line #640
that make artificial ordering by pk but it impossible after slicing (LIMIT-ations in SQL )

More detailed example

    queryset = Entry.objects.all()
    queryset = queryset.order_by(Entry._meta.pk.name)

    paginator = Paginator(object_list=queryset, per_page=10)
    page_obj = paginator.get_page(request.GET.get('page'))

    EntryFormSet = modelformset_factory(Entry, EntryForm, extra=0)
    entryformset = EntryFormSet(queryset=page_obj.object_list)
Amateurish answered 2/11, 2020 at 20:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.