├── requirements.txt ├── setup.cfg ├── MANIFEST ├── django_timestamp_paginator ├── __init__.py └── paginator.py ├── .gitignore ├── setup.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | django -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | django_timestamp_paginator/__init__.py 5 | django_timestamp_paginator/paginator.py 6 | -------------------------------------------------------------------------------- /django_timestamp_paginator/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Timestamp Paginator for Django. 4 | 5 | Umut Bozkurt - @umutbozkurt 6 | MIT Licence 7 | 8 | """ 9 | 10 | 11 | from django_timestamp_paginator.paginator import TimestampPaginator, Page 12 | 13 | __all__ = [ 14 | 'TimestampPaginator', 15 | 'Page' 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | .idea 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Mkdocs documentation 54 | documentation/site/ 55 | 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='django-timestamp-paginator', 5 | version='0.0.2', 6 | description='Timestamp Paginator for Django.', 7 | packages=['django_timestamp_paginator', ], 8 | license='see licence on https://github.com/umutbozkurt/django-timestamp-paginator', 9 | long_description='see https://github.com/umutbozkurt/django-timestamp-paginator/blob/master/README.md', 10 | url='https://github.com/umutbozkurt/django-timestamp-paginator', 11 | download_url='https://github.com/umutbozkurt/django-timestamp-paginator/releases/', 12 | keywords=['django', 'timestamp', 'paginator'], 13 | author='Umut Bozkurt', 14 | author_email='umutbozkurt92@gmail.com', 15 | requires=['django', ], 16 | classifiers=[ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Natural Language :: English', 20 | 'Programming Language :: Python', 21 | 'Topic :: Software Development :: Libraries :: Python Modules', 22 | 'Topic :: Software Development :: Testing', 23 | 'Topic :: Internet', 24 | 'Topic :: Internet :: WWW/HTTP :: Site Management', 25 | 'Topic :: Text Processing :: Markup :: HTML', 26 | 'Intended Audience :: Developers' 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Timestamp Paginator 2 | 3 | ## Why we needed this? 4 | 5 | Classical pagination (?page=2) doesn't work properly with actively updated list pages. 6 | 7 | #### Example: 8 | Imagine a web page where people upload their pet's pictures. 9 | - Anyone can upload a picture anytime and it will be listed. 10 | - The newest picture appears at the top of the page. 11 | - When the list will be complete, the users will go to the second page (by a link or infinite loader). 12 | 13 | This will work fine. 14 | 15 | However, if during this time period, a new image will be uploaded, 16 | the last image of the first page will become the first item of the second page. 17 | 18 | In order to prevent this problem, there are many alternative ways that can be used. 19 | One of the best alternatives is to use timestamps instead of page numbers. 20 | 21 | This paginator makes the pagination by the timestamps of the items. 22 | 23 | ## Installation 24 | 25 | `pip install django-timestamp-paginator` 26 | 27 | ## Usage 28 | 29 | ```python 30 | from django_timestamp_paginator import TimestampPaginator 31 | 32 | queryset = MyModel.objects.all() # A queryset ordered by my timestamp field (ASC or DESC) 33 | my_timestamp_field = 'timestamp' # My model has a field named timestamp (DecimalField) 34 | results_per_page = 30 # I want to see 30 results per page 35 | 36 | paginator = TimestampPaginator(queryset, my_timestamp_field, results_per_page) 37 | 38 | page = paginator.page() # Get me the first page, regarding queryset ordering 39 | 40 | repr(page) 41 | >>> 42 | 43 | next_page_timestamp = page.next_page_timestamp() 44 | next_page = paginator.page(timestamp=next_page_timestamp) 45 | 46 | ``` 47 | 48 | ### Previous Page? 49 | No, there is no previous page. TimestampPaginator goes **only forwards** , ***regarding queryset ordering***. If you want to fetch latest results, go ahead and get first page by `paginator.page()` 50 | 51 | ### Requirements 52 | 53 | Django 54 | 55 | ### Django Rest Framework? 56 | If you are planning to use this package with Django Rest Framework, check out the [drf-timestamp-pagination](https://github.com/Hipo/drf-timestamp-pagination) package! 57 | 58 | ### Contribution 59 | ANY contribution is more than welcome. 60 | We suffered a lot because of this issue and we are willing to help anyone who tries to understand this package. 61 | Do not hesitate to open issues or pull requests. 62 | 63 | Improve the blurry parts of the readme. -------------------------------------------------------------------------------- /django_timestamp_paginator/paginator.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.paginator import Paginator, Page as BasePage 4 | from django.db.models.query import QuerySet 5 | from django.utils import six 6 | 7 | 8 | LOWER_THAN = 'lt' 9 | GREATER_THAN = 'gt' 10 | 11 | ASCENDING = '' 12 | DESCENDING = '-' 13 | 14 | 15 | class InvalidTimestamp(Exception): 16 | pass 17 | 18 | 19 | class TimestampPaginator(Paginator): 20 | 21 | def __init__(self, queryset, timestamp_field, per_page, allow_empty_first_page=True): 22 | assert isinstance(queryset, QuerySet), '`queryset` param needs to be instance of Django Queryset' 23 | super(TimestampPaginator, self).__init__(queryset, per_page, allow_empty_first_page=allow_empty_first_page) 24 | self.timestamp_field = timestamp_field 25 | self.queryset = queryset 26 | self.ordering = self._get_ordering() 27 | assert self.ordering is not None, '`queryset` must be ordered by ' \ 28 | '`{ts_field}` or `-{ts_field}`'.format(ts_field=self.timestamp_field) 29 | 30 | self.allow_empty_first_page = allow_empty_first_page 31 | 32 | def validate_timestamp(self, ts): 33 | pass 34 | 35 | def _get_ordering(self): 36 | for order in self.queryset.query.order_by + list(self.queryset.model._meta.ordering): 37 | if order == DESCENDING + self.timestamp_field: 38 | return DESCENDING 39 | elif order == ASCENDING + self.timestamp_field: 40 | return ASCENDING 41 | else: 42 | continue 43 | 44 | def page(self, timestamp=None): 45 | if not timestamp: 46 | timestamp = sys.float_info.max if self.ordering == DESCENDING else sys.float_info.min 47 | else: 48 | self.validate_timestamp(timestamp) 49 | 50 | timestamp_query = '{timestamp_field}__{condition}' 51 | condition = GREATER_THAN if self.ordering == ASCENDING else LOWER_THAN 52 | timestamp_query = timestamp_query.format(timestamp_field=self.timestamp_field, 53 | condition=condition) 54 | 55 | timestamp_query_kwarg = {timestamp_query: timestamp} 56 | filtered_queryset = self.queryset.filter(**timestamp_query_kwarg) 57 | 58 | return Page(filtered_queryset[:self.per_page + 1], self) 59 | 60 | 61 | class Page(BasePage): 62 | 63 | def __init__(self, object_list, paginator): 64 | self.paginator = paginator 65 | self._has_next = len(object_list) > self.paginator.per_page 66 | self.object_list = object_list[:self.paginator.per_page] 67 | 68 | def has_next(self): 69 | return self._has_next 70 | 71 | def has_previous(self): 72 | """ 73 | Not implemented 74 | """ 75 | return True 76 | 77 | def next_page_timestamp(self): 78 | return getattr(self.object_list[-1], self.paginator.timestamp_field) 79 | 80 | def __getitem__(self, index): 81 | if not isinstance(index, (slice,) + six.integer_types): 82 | raise TypeError 83 | # The object_list is converted to a list so that if it was a QuerySet 84 | # it won't be a database hit per __getitem__. 85 | if not isinstance(self.object_list, list): 86 | self.object_list = list(self.object_list) 87 | return self.object_list[index] 88 | 89 | def __repr__(self): 90 | return '' % self.next_page_timestamp() --------------------------------------------------------------------------------