Search code examples
djangodjango-modelsdjango-managersdjango-pagination

Django pagination (get page no. corresponding to the object)


I have a paginate I am trying to get the index page from an object page (sort of pagination in reverse)

The get_paginated_posts returns a paginator for the model Post:

class PostManager(models.Manager):
    def get_paginated_posts(self, request=None):
        if request and request.user.has_perm('blog.change_post'):
            posts = super(PostManager, self).filter(is_update=False)
        else:        
            posts = super(PostManager, self).filter(publish=True, is_update=False)
        return Paginator(posts, POSTS_PER_PAGE)
    .
    .

This is my model

class Post(models.Model):
    .
    .
    .
    def get_page(self, request=None):
        paginator = Post.objects.get_paginated_posts(request)
        for i in range(1, paginator.num_pages+1):
            if self in paginator.page(i).object_list:                
                return i
            pass
        return False 

My concern is the Post.objects.get_paginated_posts call in the get_page function.
Is it right to call Post class from an instance? Is there any other better way to do this possible?
Why cannot I call super(Post, self).objects.get_paginated_posts to do the same?
I understand that self.objects.get_paginated_posts wont work because of absent access for the object to its manager.

Solved

Final code as suggested by Tomasz Elendt:

class PostManager(models.Manager):
    def get_paginated_posts(self, user=None):
        if user and user.has_perm('blog.change_post'):
            posts = super(PostManager, self).filter(is_update=False)
        else:        
            posts = super(PostManager, self).filter(publish=True, is_update=False)
        return Paginator(posts, POSTS_PER_PAGE)

class Post(models.Model):
    .
    def get_page(self, request=None):
        return self._default_manager.filter(is_update = False, time__gt=self.time).count()/POSTS_PER_PAGE +1 
        #Just a one line now :P 

Solution

  • It's not the best idea what you're doing. Try to imagine how many queries it'll be translated to -- in the worst case you'd need to retrieve all user's posts from database!

    I assume that you have some predefined ordering in your Post model (the one that Paginator uses). Use that to obtain the number of user's posts that precede that specific post record. If you divide that number by the POSTS_PER_PAGE value you'll get your page number.

    IMHO using PostManager in Post methods is ok. What's not ok is that you're passing request object to it while I think you should use user_id for that (and permission checking should be really part of a view logic).

    EDIT: example

    from django.db import models
    from django.contrib.auth.models import User
    
    POSTS_PER_PAGE = 10
    
    class Post(models.Model):
        """
        >>> from datetime import datetime, timedelta
        >>> from django.db import connection
        >>> from django.conf import settings
        >>>
        >>> user = User.objects.create_user("test", "[email protected]")
        >>> for i in xrange(100):
        ...     p = Post.objects.create(author=user,
        ...                             pub_date=datetime.now() - timedelta(hours=i))
        >>> post = Post.objects.all()[68]
        >>> settings.DEBUG = True    # monkey-patching settings - ugly
        >>> connection.queries = []  # cleaning previous queries
        >>> post.get_page()
        7
        >>> len(connection.queries)  # print number of queries of `get_page` call
        1
        """
        pub_date = models.DateTimeField(auto_now_add=True)
        author = models.ForeignKey(User)
        class Meta:
            ordering = ["-pub_date"]
    
        def get_page(self):
            return self._default_manager.filter(author__id=self.author_id).filter(
                pub_date__gt=self.pub_date).count() / POSTS_PER_PAGE + 1