Django: paginating differently on the first page

2019-03-31 09:03发布

问题:

At the minute, I'm using Django's object_list to handle pagination. I'm happy to move to the proper Paginator() class if you think I need it after you hear my problem:

On the homepage, I want to paginate by 7, but on all other pages I want to paginate by 10.

How would I go about doing this? I really can't get my head around it. The closest I've got to making it work resulted in a whole page of results being left out, so obviously I don't want that.

I'd be extremely appreciative of any answers. Let me know if you need any more information. Thanks a lot.

Joe

回答1:

Just from django.core.paginator import Paginator and make a paginator object as p = Paginator(thestuff, 7) in the view for the homepage, p = Paginator(thestuff, 10) everywhere else. Then in either case bind p in the context you use to render the template. p.object_list will be set appropriately in either case (and you appear to say that's the approach you're using, right? I.e., is that what you mean by "Django's object_list"?).

Django docs have excellent details and examples (assuming you're on 1.0 or better). If you can't make it work, can you show us (a simplified version that still fails, of) your template and view code?

Edit: problem has now been clearly shown and I think should be solved by subclassing Django's core paginator, as follows:

from django.core.paginator import Paginator, Page

class MyPaginator(Paginator):

  def __init__(self, **kw):
    self.deltafirst = kw.pop('deltafirst', 0)
    Paginator.__init__(self, **kw)

  def page(self, number):
    "Returns a Page object for the given 1-based page number."
    number = self.validate_number(number)
    if number == 1:
      bottom = 0
      top = self.per_page - self.deltafirst
    else:
      bottom = (number - 1) * self.per_page - self.deltafirst
      top = bottom + self.per_page
    if top + self.orphans >= self.count:
      top = self.count
    return Page(self.object_list[bottom:top], number, self)

Now use MyPaginator exactly as the above text and examples show the usage of Django's own, except, on creating it, use an extra named argument deltafirst=3 to make the first page 3 shorter than the normal per-page length (of 10). So you'd be using a single paginator with a nominal length of 10 but a deltafirst of 3 to make the first page 3 shorter than all others.

(There may be problems in validate_number but I'm not sure they'd appear -- if they do, then MyPaginator will need to override that method as well).



回答2:

The object_list() function uses the Paginator to break apart the list. So, if you alter the "size" of the chunks (7 vs 8) ... then it does what you see.

Choices: That last choice is wonky. Don't do that :-) Either "hack it" or move to the Paginator.

Hack it ... on requests > page 1, sneak something extra into the row_list. I'm guessing you could force it into a list via list(row_list), and prepend a None or something via row_list.insert(0, None)

Paginate ... use the Paginator

View + template ... have your view just pass the queryset row_list to the template. Have the template check for page 1. Do you own looping/display of the subset of the row_list there. Its mucky to have such logic in your template. It makes for a complex template.



回答3:

I realize this is an old question but in case somebody, like me, comes here and find this my unittest discovered a problem.

Alex is essentially right but don't forget to also implement _get_num_pages with self.orphans + self.deltafirst otherwise the last page will not be correct.

def _get_num_pages(self):
    """Returns the total number of pages."""
    if self._num_pages is None:
        if self.count == 0 and not self.allow_empty_first_page:
            self._num_pages = 0
        else:
            hits = max(1, self.count - self.orphans + self.deltafirst)
            self._num_pages = int(ceil(hits / float(self.per_page)))
    return self._num_pages
num_pages = property(_get_num_pages)

And the unittest:

def test_ads_paginator(self):
    per_page = 10
    num_items = 100
    num_firstpage = 5
    expected_num_pages = 11

    items = range(num_items)
    paginator = AdsPaginator(items, per_page, deltafirst=per_page-num_firstpage)
    last_item = paginator.page(paginator.num_pages).object_list[-1]

    self.assertEqual(len(paginator.page(1)), num_firstpage)
    self.assertEqual(paginator.num_pages, expected_num_pages)
    self.assertEqual(last_item, items[-1])


回答4:

For my specific scenario, I was looking to paginate objects within 1 rendered template (creating a multi-page document). For my case, I think the easiest approach is to forget about paginator and use a generator instead.

    def pages_iter():
        page_num = 1
        num_records = lambda x: 30 if x > 1 else 10
        end_idx = 0

        while True:
            start_idx = end_idx
            end_idx += num_records(page_num)
            yield {
                'number': page_num,
                'object_list': donors[start_idx:end_idx]
            }

            if end_idx > donors.count() - 2:
                break
            page_num += 1

Add { pages: page_iter } to your page context and then you have yourself a variable paginator. Note that num_records is a function that takes page_number, so you really have ultimate flexibility.

In your template, you can treat this just like you would paginator:

{% for page in pages %}
  {% if page.number == 1 %}
    {{ header }}
  {% endif %}

  {% for obj in page.object_list %}
    {{ obj }}
  {% endfor %}
{% endfor %}