Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit 03e1335799c04c2a07cf38da535b1ffcfb80a86c Andy McKay committed Feb 5, 2010
Showing with 166 additions and 0 deletions.
  1. +2 −0 license.txt
  2. +88 −0 readme.txt
  3. +13 −0 setup.py
  4. +3 −0 src/__init__.py
  5. +60 −0 src/paginator.py
@@ -0,0 +1,2 @@
+# Clearwind Consulting 2010
+# BSD License
@@ -0,0 +1,88 @@
+This is a paginator that does not do a count.
+
+** Warning this needs work :)
+
+It started with this:
+
+http://www.agmweb.ca/blog/andy/2226/
+
+Then there was excellent idea from mmalone and an excellent gist:
+
+http://gist.github.com/213702
+
+So then this got updated for the latest Django and became this.
+
+Example:
+
+>>> from django.db import connection
+>>> from listener.models.error import Error
+>>> queryset = Error.objects.filter(account=1, archived=1)
+
+The old way:
+
+>>> from django.core.paginator import Paginator
+>>> p = Paginator(queryset, 10)
+>>> p.page(1)
+<Page 1 of 859>
+
+Causes:
+
+>>> connection.queries
+[ {'time': '21.953', 'sql': 'SELECT COUNT(*) FROM "listener_error" WHERE ("listener_error"."account_id" = 1 AND "listener_error"."archived" = true )'}]
+
+That's one query. To get the object list it does another.
+
+>>> p.page(1).object_list
+[{'time': '0.000', 'sql': 'SELECT "listener_error"."id", ... FROM "listener_error" WHERE ("listener_error"."account_id" = 1 AND "listener_error"."archived" = true ) LIMIT 10'}]
+
+The problem is that count can be hideously expensive.
+
+The new way:
+
+>>> from lazy_paginator.paginator import LazyPaginator
+>>> p = LazyPaginator(queryset, 10)
+>>> p.page(1)
+<Page 1 of 1000>
+
+>>> connection.queries
+[{'time': '0.000', 'sql': 'SELECT "listener_error"."id",... FROM "listener_error" WHERE ("listener_error"."account_id" = 1 AND "listener_error"."archived" = true ) LIMIT 11'}]
+
+>>> p.page(1).object_list
+[{'time': '0.000', 'sql': 'SELECT "listener_error"."id",... FROM "listener_error" WHERE ("listener_error"."account_id" = 1 AND "listener_error"."archived" = true ) LIMIT 10'}]
+
+By doing a query for one more than you need, it figures out if there's a next. The difference is the select vs the count.
+
+What do you lose? You don't know how many records there are, you just know if there is a next and previous (and you can figure out how many came before). But if you are using postgresql, beware of how expensive those counts can be.
+
+The default assumes there's going to be 1000 pages, but we don't really know how many there. There's a max_safe_pages variable that gets updated as information is provided. For example if you set it to 3 pages... when try and access 4, it fail, thinking that there was no data.
+
+>>> p = LazyPaginator(queryset, 10, max_safe_pages=3)
+>>> p.has_next(1)
+True
+>>> p.has_next(2)
+True
+>>> p.has_next(3)
+False
+>>> p.has_next(4)
+False
+>>> p.page(4)
+Traceback (most recent call last):
+ File "<console>", line 1, in <module>
+ File "/var/arecibo/lazy_paginator/paginator.py", line 27, in page
+ number = self.validate_number(number)
+ File "/var/arecibo/lazy_paginator/paginator.py", line 20, in validate_number
+ return super(LazyPaginator, self).validate_number(number)
+ File "/usr/lib/python2.5/site-packages/django/core/paginator.py", line 32, in validate_number
+ raise EmptyPage('That page contains no results')
+EmptyPage: That page contains no results
+
+Now if you start at the beginning:
+
+>>> p.page(2)
+<Page 2 of 3>
+>>> p.page(3)
+<Page 3 of 4>
+>>> p.page(4)
+<Page 4 of 5>
+
+That can be a bit confusing, perhaps in the future it should check and if not then try to get it... improvements welcome.
@@ -0,0 +1,13 @@
+from distutils.core import setup
+setup(name='lazy_paginator',
+ version='0.1',
+ description='Lazy Paginator for Django',
+ author="Andy McKay",
+ author_email="andy@clearwind.ca",
+ packages=["lazy_paginator",],
+ package_dir = {'lazy_paginator':'src'},
+ classifiers = [
+ "Development Status :: 4 - Beta"
+ "Framework :: Django"
+ ]
+ )
@@ -0,0 +1,3 @@
+# Clearwind Consulting Ltd, 2010
+# BSD License
+# based on work by mmalone
@@ -0,0 +1,60 @@
+# Clearwind Consulting Ltd, 2010
+# BSD License
+# based on work by mmalone
+import sys
+from django.core.paginator import Paginator, Page, InvalidPage
+
+class LazyPaginator(Paginator):
+ max_safe_pages = 0
+
+ def __init__(self, object_list, per_page, orphans=0,
+ allow_empty_first_page=True, max_safe_pages=1000):
+ self.max_safe_pages = max_safe_pages
+ super(LazyPaginator, self).__init__(object_list, per_page,
+ orphans=orphans, allow_empty_first_page=allow_empty_first_page)
+
+ def validate_number(self, number):
+ try:
+ number = int(number)
+ except:
+ raise InvalidPage
+ if number <= self.max_safe_pages:
+ return number
+ return super(LazyPaginator, self).validate_number(number)
+
+ def _get_num_pages(self):
+ return self.max_safe_pages
+ num_pages = property(_get_num_pages)
+
+ def page(self, number):
+ number = self.validate_number(number)
+ bottom = (number - 1) * self.per_page
+ top = bottom + self.per_page
+
+ # get one extra object to see if there is a next page
+ page = list(self.object_list[bottom:top + 1])
+ if len(page) > self.per_page:
+ # if we got an extra object, update max_safe_pages
+ if number + 1 > self.max_safe_pages:
+ self.max_safe_pages = number + 1
+ page = page[:self.per_page]
+ if number > 0 and len(page) == 0:
+ raise InvalidPage
+
+ return Page(self.object_list[bottom:top], number, self)
+
+ def has_next(self, number):
+ if number < self.max_safe_pages:
+ return True
+ return super(LazyPaginator, self).has_next(number)
+
+ def last_on_page(self, number):
+ """
+ Returns the 1-based index of the last object on the given page,
+ relative to total objects found (hits).
+ """
+ number = self.validate_number(number)
+ if number >= self.max_safe_pages:
+ return super(LazyPaginator, self).last_on_page(number)
+ number += 1 # 1-base
+ return number * self.per_page

0 comments on commit 03e1335

Please sign in to comment.