├── .coverage ├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── HACKING ├── INSTALL ├── LICENSE ├── MANIFEST ├── MANIFEST.in ├── Makefile ├── README.rst ├── doc ├── Makefile ├── _static │ └── TRACKME ├── changelog.rst ├── conf.py ├── contacts.rst ├── contributing.rst ├── current_page_number.rst ├── customization.rst ├── different_first_page.rst ├── digg_pagination.rst ├── generic_views.rst ├── index.rst ├── javascript.rst ├── lazy_pagination.rst ├── multiple_pagination.rst ├── start.rst ├── templatetags_reference.rst ├── thanks.rst └── twitter_pagination.rst ├── endless_pagination ├── __init__.py ├── decorators.py ├── exceptions.py ├── loaders.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_CN │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── paginators.py ├── settings.py ├── static │ └── endless_pagination │ │ └── js │ │ └── module.endless.js ├── templates │ └── endless │ │ ├── current_link.html │ │ ├── page_link.html │ │ ├── show_more.html │ │ ├── show_more_table.html │ │ └── show_pages.html ├── templatetags │ ├── __init__.py │ └── endless.py ├── tests │ ├── __init__.py │ ├── integration │ │ ├── __init__.py │ │ ├── test_callbacks.py │ │ ├── test_chunks.py │ │ ├── test_digg.py │ │ ├── test_multiple.py │ │ ├── test_onscroll.py │ │ └── test_twitter.py │ ├── templatetags │ │ ├── __init__.py │ │ └── test_endless.py │ ├── test_decorators.py │ ├── test_loaders.py │ ├── test_models.py │ ├── test_paginators.py │ ├── test_utils.py │ └── test_views.py ├── utils.py └── views.py ├── setup.py └── tests ├── .coverage ├── develop.py ├── manage.py ├── project ├── __init__.py ├── context_processors.py ├── static │ ├── endless_pagination │ │ └── js │ │ │ ├── module.endless.js │ │ │ └── module.test.js │ └── pagination.css ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── base_callbacks.html │ ├── callbacks │ │ ├── index.html │ │ └── page.html │ ├── chunks │ │ ├── index.html │ │ ├── items_page.html │ │ └── objects_page.html │ ├── complete │ │ ├── articles_page.html │ │ ├── entries_page.html │ │ ├── index.html │ │ ├── items_page.html │ │ └── objects_page.html │ ├── digg │ │ ├── index.html │ │ └── page.html │ ├── home.html │ ├── multiple │ │ ├── entries_page.html │ │ ├── index.html │ │ ├── items_page.html │ │ └── objects_page.html │ ├── onscroll │ │ ├── index.html │ │ └── page.html │ └── twitter │ │ ├── index.html │ │ ├── page.html │ │ └── table.html ├── urls.py └── views.py ├── requirements.pip ├── settings.py └── with_venv.sh /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/.coverage -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | /tests 4 | /example 5 | /*/__init__.py 6 | /setup.py 7 | /doc 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.mo 3 | migrations 4 | /staticfiles/ 5 | /static/ 6 | *.sqlite3 7 | /build/ 8 | /dist/ 9 | /django-endless-pagination-angular.egg-info/ 10 | /.venv/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | - "2.7" 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | before_install: 12 | - cd tests 13 | - pip install -r requirements.pip 14 | - cd .. 15 | 16 | install: 17 | - python setup.py install 18 | 19 | script: 20 | - cd tests 21 | - python manage.py test 22 | 23 | after_success: 24 | coveralls -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Martín Peveri 2 | Francesco Banconi 3 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Hacking Django Endless Pagination Angular 2 | ========================================= 3 | 4 | Here are the steps needed to set up a development and testing environment. 5 | 6 | Creating a development environment 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | The development environment is created in a virtualenv. The environment 10 | creation requires the *make* and *virtualenv* programs to be installed. 11 | 12 | To install *make* under Debian/Ubuntu:: 13 | 14 | $ sudo apt-get install build-essential 15 | 16 | Under Mac OS/X, *make* is available as part of XCode. 17 | 18 | To install virtualenv:: 19 | 20 | $ sudo pip install virtualenv 21 | 22 | At this point, from the root of this branch, run the command:: 23 | 24 | $ make 25 | 26 | This command will create a ``.venv`` directory in the branch root, ignored 27 | by DVCSes, containing the development virtual environment with all the 28 | dependencies. 29 | 30 | Testing the application 31 | ~~~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | Run the tests:: 34 | 35 | $ make test 36 | 37 | The command above also runs all the available integration tests. They use 38 | Selenium and require Firefox to be installed. To avoid executing integration 39 | tests, define the environment variable SKIP_SELENIUM, e.g.:: 40 | 41 | $ make test SKIP_SELENIUM=1 42 | 43 | Integration tests are excluded by default when using Python 3. The test suite 44 | requires Python >= 2.6.1. 45 | 46 | Run the tests and lint/pep8 checks:: 47 | 48 | $ make check 49 | 50 | Again, to exclude integration tests:: 51 | 52 | $ make check SKIP_SELENIUM=1 53 | 54 | Debugging 55 | ~~~~~~~~~ 56 | 57 | Run the Django shell (Python interpreter):: 58 | 59 | $ make shell 60 | 61 | Run the Django development server for manual testing:: 62 | 63 | $ make server 64 | 65 | After executing the command above, it is possible to navigate the testing 66 | project going to . 67 | 68 | See all the available make targets, including info on how to create a Python 3 69 | development environment:: 70 | 71 | $ make help 72 | 73 | Thanks for contributing, and have fun! 74 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | To install django-endless-pagination, run the following command 2 | inside this directory: 3 | 4 | make install 5 | 6 | Or if you'd prefer you can simply place the included ``endless_pagination`` 7 | package on your Python path. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2015 Martín Peveri & Francesco Banconi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | AUTHORS 3 | HACKING 4 | INSTALL 5 | LICENSE 6 | MANIFEST.in 7 | Makefile 8 | README.rst 9 | setup.py 10 | endless_pagination/__init__.py 11 | endless_pagination/decorators.py 12 | endless_pagination/exceptions.py 13 | endless_pagination/loaders.py 14 | endless_pagination/models.py 15 | endless_pagination/paginators.py 16 | endless_pagination/settings.py 17 | endless_pagination/utils.py 18 | endless_pagination/views.py 19 | endless_pagination/__pycache__/__init__.cpython-35.pyc 20 | endless_pagination/locale/de/LC_MESSAGES/django.mo 21 | endless_pagination/locale/de/LC_MESSAGES/django.po 22 | endless_pagination/locale/es/LC_MESSAGES/django.mo 23 | endless_pagination/locale/es/LC_MESSAGES/django.po 24 | endless_pagination/locale/fr/LC_MESSAGES/django.mo 25 | endless_pagination/locale/fr/LC_MESSAGES/django.po 26 | endless_pagination/locale/it/LC_MESSAGES/django.mo 27 | endless_pagination/locale/it/LC_MESSAGES/django.po 28 | endless_pagination/locale/zh_CN/LC_MESSAGES/django.mo 29 | endless_pagination/locale/zh_CN/LC_MESSAGES/django.po 30 | endless_pagination/static/endless_pagination/js/module.endless.js 31 | endless_pagination/templates/endless/current_link.html 32 | endless_pagination/templates/endless/page_link.html 33 | endless_pagination/templates/endless/show_more.html 34 | endless_pagination/templates/endless/show_more_table.html 35 | endless_pagination/templates/endless/show_pages.html 36 | endless_pagination/templatetags/__init__.py 37 | endless_pagination/templatetags/endless.py 38 | endless_pagination/tests/__init__.py 39 | endless_pagination/tests/test_decorators.py 40 | endless_pagination/tests/test_loaders.py 41 | endless_pagination/tests/test_models.py 42 | endless_pagination/tests/test_paginators.py 43 | endless_pagination/tests/test_utils.py 44 | endless_pagination/tests/test_views.py 45 | endless_pagination/tests/integration/__init__.py 46 | endless_pagination/tests/integration/test_callbacks.py 47 | endless_pagination/tests/integration/test_chunks.py 48 | endless_pagination/tests/integration/test_digg.py 49 | endless_pagination/tests/integration/test_multiple.py 50 | endless_pagination/tests/integration/test_onscroll.py 51 | endless_pagination/tests/integration/test_twitter.py 52 | endless_pagination/tests/templatetags/__init__.py 53 | endless_pagination/tests/templatetags/test_endless.py 54 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include HACKING 3 | include INSTALL 4 | include LICENSE 5 | include Makefile 6 | include MANIFEST.in 7 | include README.rst 8 | recursive-include endless_pagination/static * 9 | recursive-include endless_pagination/locale * 10 | recursive-include endless_pagination/templates * 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Makefile. 2 | 3 | # Define these variables based on the system Python versions. 4 | PYTHON2 = python 5 | PYTHON3 = python3 6 | 7 | VENV2 = .venv 8 | VENV3 = .venv3 9 | 10 | LINTER = flake8 --show-source endless_pagination/ tests/ 11 | MANAGE = python ./tests/manage.py 12 | 13 | ifdef PY3 14 | PYTHON = $(PYTHON3) 15 | VENV = $(VENV3) 16 | else 17 | PYTHON = $(PYTHON2) 18 | VENV = $(VENV2) 19 | endif 20 | 21 | DOC_INDEX = doc/_build/html/index.html 22 | VENV_ACTIVATE = $(VENV)/bin/activate 23 | WITH_VENV = ./tests/with_venv.sh $(VENV) 24 | 25 | all: develop 26 | 27 | $(DOC_INDEX): $(wildcard doc/*.rst) 28 | @$(WITH_VENV) make -C doc html 29 | 30 | doc: develop $(DOC_INDEX) 31 | 32 | clean: 33 | $(PYTHON) setup.py clean 34 | rm -rfv .coverage build/ dist/ doc/_build MANIFEST 35 | find . -name '*.pyc' -delete 36 | find . -name '__pycache__' -type d -delete 37 | 38 | cleanall: clean 39 | rm -rfv $(VENV2) $(VENV3) 40 | 41 | check: test lint 42 | 43 | $(VENV_ACTIVATE): tests/develop.py tests/requirements.pip 44 | @$(PYTHON) tests/develop.py 45 | @touch $(VENV_ACTIVATE) 46 | 47 | develop: $(VENV_ACTIVATE) 48 | 49 | help: 50 | @echo -e 'Django Endless Pagination Angular - list of make targets:\n' 51 | @echo 'make - Set up development and testing environment' 52 | @echo 'make test - Run tests' 53 | @echo 'make lint - Run linter and pep8' 54 | @echo 'make check - Run tests, linter and pep8' 55 | @echo 'make doc - Build Sphinx documentation' 56 | @echo 'make opendoc - Build Sphinx documentation and open it in browser' 57 | @echo 'make source - Create source package' 58 | @echo 'make install - Install on local system' 59 | @echo 'make shell - Enter Django interactive interpreter' 60 | @echo 'make server - Run Django development server' 61 | @echo 'make clean - Get rid of bytecode files, build dirs, dist files' 62 | @echo 'make cleanall - Clean and also get rid of the virtualenvs' 63 | @echo -e '\nDefine the env var PY3 to work using Python 3.' 64 | @echo 'E.g. to create a Python 3 development environment:' 65 | @echo ' - make PY3=1' 66 | @echo 'E.g. to run tests and linter under Python 3:' 67 | @echo ' - make check PY3=1' 68 | @echo -e '\nWhen testing the application, define the env var' 69 | @echo 'SKIP_SELENIUM to exclude integration tests, e.g.:' 70 | @echo ' - make check SKIP_SELENIUM=1' 71 | @echo -e '\nWhen running integration tests, by default all graphical' 72 | @echo 'operations are performed in memory where possible. However,' 73 | @echo 'it is possible to run tests using a visible browser instance' 74 | @echo 'by defining the env var SHOW_BROWSER, e.g.:' 75 | @echo ' - make check SHOW_BROWSER=1' 76 | 77 | install: 78 | python setup.py install 79 | 80 | lint: develop 81 | @$(WITH_VENV) $(LINTER) 82 | 83 | opendoc: doc 84 | @firefox $(DOC_INDEX) 85 | 86 | release: clean 87 | python setup.py register sdist upload 88 | 89 | server: develop 90 | @$(WITH_VENV) $(MANAGE) runserver 0.0.0.0:8000 91 | 92 | shell: develop 93 | @$(WITH_VENV) $(MANAGE) shell 94 | 95 | source: 96 | $(PYTHON) setup.py sdist 97 | 98 | test: develop 99 | @$(WITH_VENV) $(MANAGE) test 100 | 101 | .PHONY: all doc clean cleanall check develop install lint opendoc release \ 102 | server shell source test 103 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Django Endless Pagination Angular 3 | ================================= 4 | 5 | .. image:: https://coveralls.io/repos/mapeveri/django-endless-pagination-angular/badge.svg?branch=master&service=github 6 | :target: https://coveralls.io/github/mapeveri/django-endless-pagination-angular?branch=master 7 | 8 | .. image:: https://travis-ci.org/mapeveri/django-endless-pagination-angular.svg?branch=master 9 | :target: https://travis-ci.org/mapeveri/django-endless-pagination-angular 10 | 11 | .. image:: https://badge.fury.io/py/django-endless-pagination-angular.svg 12 | :target: http://badge.fury.io/py/django-endless-pagination-angular 13 | 14 | Django Endless Pagination Angular is a fork of the excellent application django-endless-pagination created by Francesco Banconi. 15 | This application get all code of version 2.0 and update for working in django >= 1.7 in addition to migration code jquery to angular.js. 16 | 17 | Django Endless Pagination Angular can be used to provide Twitter-style or Digg-style pagination, with optional Ajax support and other features 18 | like multiple or lazy pagination. 19 | 20 | Documentation 21 | ------------- 22 | 23 | **Documentation** is `avaliable online 24 | `_, or in the **doc** 25 | directory of the project. 26 | 27 | Installation 28 | ------------ 29 | 30 | Via pip:: 31 | 32 | pip install django-endless-pagination-angular 33 | 34 | Quick start 35 | ----------- 36 | 37 | 1. Add application 'endless_pagination' to INSTALLED_APPS. 38 | 2. Add this lines in settings.py:: 39 | 40 | from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS 41 | TEMPLATE_CONTEXT_PROCESSORS += ( 42 | 'django.core.context_processors.request', 43 | ) 44 | 45 | 46 | Getting started 47 | --------------- 48 | 49 | In this example it will be implemented twitter style pagination 50 | 51 | Base.html:: 52 | 53 | 54 | 55 | 56 | 57 | {% block title %}Testing project{% endblock %} - Django Endless Pagination Angular 58 | 59 | 60 | 61 | 62 |
63 | 66 |
67 |
68 | {% block content %}{% endblock %} 69 |
70 | 71 | {% block js %} 72 | 73 | 74 | {% endblock %} 75 | 76 | 77 | 78 | Index.html:: 79 | 80 | {% extends "base.html" %} 81 | 82 | {% block content %} 83 |
84 | {% include myapp/page_template.html %} 85 |
86 | {% endblock %} 87 | 88 | Page_template.html:: 89 | 90 | {% load endless %} 91 | 92 | {% paginate objects %} 93 | {% for object in objects %} 94 |
95 |

{{ object.title }}

96 | {{ object.contents }} 97 |
98 | {% endfor %} 99 | {% show_more "More results" %} 100 | 101 | In the views.py:: 102 | 103 | class TwitterView(View): 104 | 105 | def get(self, request, forum, *args, **kwargs): 106 | 107 | template_name = "myapp/index.html" 108 | page_template = "myapp/page_template.html" 109 | 110 | objects = MyModel.objects.all() 111 | 112 | data = { 113 | 'objects': objects, 114 | } 115 | 116 | if request.is_ajax(): 117 | template_name = page_template 118 | return render(request, template_name, data) 119 | 120 | In the urls.py:: 121 | 122 | url(r'^twitter/$', TwitterView.as_view(), name='twitter'), 123 | 124 | 125 | Run server:: 126 | 127 | python manage.py runserver 128 | 129 | Visit: 127.0.0.1:800/twitter/ 130 | 131 | If you have already declared an angular module all you have to do is inject the module EndlessPagination. As follow:: 132 | 133 | 'use strict'; 134 | angular.module('TestApp', ['EndlessPagination']); 135 | 136 | This way you will be able to use the directive endless-pagination. For more examples check the official repository: 137 | 138 | https://github.com/mapeveri/django-endless-pagination-angular/tree/master/tests 139 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoEndlessPagination.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoEndlessPagination.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoEndlessPagination" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoEndlessPagination" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/_static/TRACKME: -------------------------------------------------------------------------------- 1 | Placeholder file to let Hg include this directory. 2 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 1.0 5 | ~~~~~~~~~~~ 6 | 7 | **Feature**: Python 3 support. 8 | 9 | Django Endless Pagination Angular supports both Python 2 and **Python 3**. Dropped 10 | support for Python 2.5. See :doc:`start` for the new list of requirements. 11 | 12 | ---- 13 | 14 | **Feature**: the **JavaScript refactoring**. 15 | 16 | This version replace Jquery by Angular.Js a re-designed Ajax support for pagination. It was removed file 17 | ``static/endless_pagination/js/endless-pagination.js`` and replaced for ``static/endless_pagination/js/module.endless.js``. 18 | 19 | Usage: 20 | 21 | .. code-block:: html+django 22 | 23 | {% block js %} 24 | {{ block.super }} 25 | 26 | 27 | {% endblock %} 28 | 29 | The last line in the block above enables Ajax requests to retrieve new 30 | pages for each pagination in the page. That's basically the same as the old 31 | approach of loading the file ``endless_pagination.js``. The new approach, however, 32 | is more increases the flexibility thank you to Angular.js. 33 | 34 | This application not is compatibility with older version of django-endless-pagination. 35 | 36 | Please refer to the :doc:`javascript` for a detailed overview of the new 37 | features and for instructions on :ref:`how to migrate` from 38 | the old JavaScript files to the new one. 39 | 40 | ---- 41 | 42 | **Feature**: the :ref:`page_templates` decorator 43 | also accepts a sequence of ``(template, key)`` pairs, functioning as a dict 44 | mapping templates and keys (still present), e.g.:: 45 | 46 | from endless_pagination.decorators import page_templates 47 | 48 | @page_templates(( 49 | ('myapp/entries_page.html', None), 50 | ('myapp/other_entries_page.html', 'other_entries_page'), 51 | )) 52 | def entry_index(): 53 | ... 54 | 55 | This also supports serving different paginated objects with the same template. 56 | 57 | ---- 58 | 59 | **Feature**: ability to provide nested context variables in the 60 | :ref:`templatetags-paginate` and :ref:`templatetags-lazy-paginate` template 61 | tags, e.g.: 62 | 63 | .. code-block:: html+django 64 | 65 | {% paginate entries.all as myentries %} 66 | 67 | The code above is basically equivalent to: 68 | 69 | .. code-block:: html+django 70 | 71 | {% with entries.all as myentries %} 72 | {% paginate myentries %} 73 | {% endwith %} 74 | 75 | In this case, and only in this case, the `as` argument is mandatory, and a 76 | *TemplateSyntaxError* will be raised if the variable name is missing. 77 | 78 | ---- 79 | 80 | **Feature**: the page list object returned by the 81 | :ref:`templatetags-get-pages` template tag has been improved adding the 82 | following new methods: 83 | 84 | .. code-block:: html+django 85 | 86 | {# whether the page list contains more than one page #} 87 | {{ pages.paginated }} 88 | 89 | {# the 1-based index of the first item on the current page #} 90 | {{ pages.current_start_index }} 91 | 92 | {# the 1-based index of the last item on the current page #} 93 | {{ pages.current_end_index }} 94 | 95 | {# the total number of objects, across all pages #} 96 | {{ pages.total_count }} 97 | 98 | {# the first page represented as an arrow #} 99 | {{ pages.first_as_arrow }} 100 | 101 | {# the last page represented as an arrow #} 102 | {{ pages.last_as_arrow }} 103 | 104 | In the *arrow* representation, the page label defaults to ``<<`` for the first 105 | page and to ``>>`` for the last one. As a consequence, the labels of the 106 | previous and next pages are now single brackets, respectively ``<`` and ``>``. 107 | First and last pages' labels can be customized using 108 | ``settings.ENDLESS_PAGINATION_FIRST_LABEL`` and 109 | ``settings.ENDLESS_PAGINATION_LAST_LABEL``: see :doc:`customization`. 110 | 111 | ---- 112 | 113 | **Feature**: The sequence returned by the callable 114 | ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` can now contain two new 115 | values: 116 | 117 | - *'first'*: will display the first page as an arrow; 118 | - *'last'*: will display the last page as an arrow. 119 | 120 | The :ref:`templatetags-show-pages` template tag documentation describes how to 121 | customize Digg-style pagination defining your own page list callable. 122 | 123 | When using the default Digg-style pagination (i.e. when 124 | ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` is set to *None*), it is 125 | possible to enable first / last page arrows by setting the new flag 126 | ``settings.ENDLESS_PAGINATION_DEFAULT_CALLABLE_ARROWS`` to *True*. 127 | 128 | ---- 129 | 130 | **Feature**: ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` can now be 131 | either a callable or a **dotted path** to a callable, e.g.:: 132 | 133 | ENDLESS_PAGINATION_PAGE_LIST_CALLABLE = 'path.to.callable' 134 | 135 | In addition to the default, ``endless_pagination.utils.get_page_numbers``, an 136 | alternative implementation is now available: 137 | ``endless_pagination.utils.get_elastic_page_numbers``. It adapts its output 138 | to the number of pages, making it arguably more usable when there are many 139 | of them. To enable it, add the following line to your ``settings.py``:: 140 | 141 | ENDLESS_PAGINATION_PAGE_LIST_CALLABLE = ( 142 | 'endless_pagination.utils.get_elastic_page_numbers') 143 | 144 | ---- 145 | 146 | **Feature**: ability to create a development and testing environment 147 | (see :doc:`contributing`). 148 | 149 | ---- 150 | 151 | **Feature**: in addition to the ability to provide a customized pagination 152 | URL as a context variable, the :ref:`templatetags-paginate` and 153 | :ref:`templatetags-lazy-paginate` tags now support hardcoded pagination URL 154 | endpoints, e.g.: 155 | 156 | .. code-block:: html+django 157 | 158 | {% paginate 20 entries with "/mypage/" %} 159 | 160 | ---- 161 | 162 | **Feature**: ability to specify negative indexes as values for the 163 | ``starting from page`` argument of the :ref:`templatetags-paginate` template 164 | tag. 165 | 166 | When changing the default page, it is now possible to reference the last page 167 | (or the second last page, and so on) by using negative indexes, e.g: 168 | 169 | .. code-block:: html+django 170 | 171 | {% paginate entries starting from page -1 %} 172 | 173 | See :doc:`templatetags_reference`. 174 | 175 | ---- 176 | 177 | **Documentation**: general clean up. 178 | 179 | ---- 180 | 181 | **Documentation**: added a :doc:`contributing` page. Have a look! 182 | 183 | ---- 184 | 185 | **Documentation**: included a comprehensive :doc:`javascript`. 186 | 187 | ---- 188 | 189 | **Fix**: ``endless_pagination.views.AjaxListView`` no longer subclasses 190 | ``django.views.generic.list.ListView``. Instead, the base objects and 191 | mixins composing the final view are now defined by this app. 192 | 193 | This change eliminates the ambiguity of having two separate pagination 194 | machineries in place: the Django Endless Pagination Angular one and the built-in 195 | Django ``ListView`` one. 196 | 197 | ---- 198 | 199 | **Fix**: the *using* argument of :ref:`templatetags-paginate` and 200 | :ref:`templatetags-lazy-paginate` template tags now correctly handles 201 | querystring keys containing dashes, e.g.: 202 | 203 | .. code-block:: html+django 204 | 205 | {% lazy_paginate entries using "entries-page" %} 206 | 207 | ---- 208 | 209 | **Fix**: replaced namespace ``endless_pagination.paginator`` with 210 | ``endless_pagination.paginators``: the module contains more than one 211 | paginator classes. 212 | 213 | ---- 214 | 215 | **Fix**: in some corner cases, loading ``endless_pagination.models`` raised 216 | an *ImproperlyConfigured* error while trying to pre-load the templates. 217 | 218 | ---- 219 | 220 | **Fix**: replaced doctests with proper unittests. Improved the code coverage 221 | as a consequence. Also introduced integration tests exercising JavaScript, 222 | based on Selenium. 223 | 224 | ---- 225 | 226 | **Fix**: overall code lint and clean up. 227 | 228 | 229 | **Feature**: added ability to avoid Http requests when multiple pagination 230 | is used. 231 | 232 | A template for multiple pagination with Http support may look like this 233 | (see :doc:`multiple_pagination`): 234 | 235 | .. code-block:: html+django 236 | 237 | 238 |

Entries:

239 |
240 | {% include "myapp/entries_page.html" %} 241 |
242 | 243 |

Other entries:

244 |
245 | {% include "myapp/other_entries_page.html" %} 246 |
247 | 248 | {% block js %} 249 | 250 | 251 | {% endblock %} 252 | 253 | 254 | But what if you need Ajax pagination for *entries* but not for *other entries*? 255 | You will only have to add a class named ``endless_page_skip`` to the 256 | page container element, e.g.: 257 | 258 | .. code-block:: html+django 259 | 260 |

Other entries:

261 |
262 | {% include "myapp/other_entries_page.html" %} 263 |
264 | 265 | ---- 266 | 267 | **Feature**: implemented a class-based generic view allowing 268 | Ajax pagination of a list of objects (usually a queryset). 269 | 270 | Intended as a substitution of *django.views.generic.ListView*, it recreates 271 | the behaviour of the *page_template* decorator. 272 | 273 | For a complete explanation, see :doc:`generic_views`. 274 | 275 | ---- 276 | 277 | **Fix**: tests are now compatible with Django 1.3. 278 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | """Django Endless Pagination Angular documentation build configuration file.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | 6 | AUTHOR = 'Francesco Banconi and Martin Peveri' 7 | APP = 'Django Endless Pagination Angular' 8 | TITLE = APP + ' Documentation' 9 | VERSION = '1.0' 10 | 11 | 12 | # Add any Sphinx extension module names here, as strings. They can be 13 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 14 | extensions = [] 15 | 16 | # Add any paths that contain templates here, relative to this directory. 17 | templates_path = ['_templates'] 18 | 19 | # The suffix of source filenames. 20 | source_suffix = '.rst' 21 | 22 | # The master toctree document. 23 | master_doc = 'index' 24 | 25 | # General information about the project. 26 | project = APP 27 | copyright = '2009-2015, ' + AUTHOR 28 | 29 | # The short X.Y version. 30 | version = release = VERSION 31 | 32 | # List of patterns, relative to source directory, that match files and 33 | # directories to ignore when looking for source files. 34 | exclude_patterns = ['_build'] 35 | 36 | # The name of the Pygments (syntax highlighting) style to use. 37 | pygments_style = 'sphinx' 38 | 39 | # The theme to use for HTML and HTML Help pages. See the documentation for 40 | # a list of builtin themes. 41 | html_theme = 'default' 42 | 43 | # Add any paths that contain custom static files (such as style sheets) here, 44 | # relative to this directory. They are copied after the builtin static files, 45 | # so a file named "default.css" will overwrite the builtin "default.css". 46 | html_static_path = ['_static'] 47 | 48 | # Output file base name for HTML help builder. 49 | htmlhelp_basename = 'DjangoEndlessPaginationdoc' 50 | 51 | # Grouping the document tree into LaTeX files. List of tuples (source start 52 | # file, target name, title, author, documentclass [howto/manual]). 53 | latex_documents = [( 54 | 'index', 'DjangoEndlessPagination.tex', TITLE, AUTHOR, 'manual')] 55 | 56 | # One entry per manual page. List of tuples 57 | # (source start file, name, description, authors, manual section). 58 | man_pages = [('index', 'djangoendlesspagination', TITLE, [AUTHOR], 1)] 59 | -------------------------------------------------------------------------------- /doc/contacts.rst: -------------------------------------------------------------------------------- 1 | Source code and contacts 2 | ======================== 3 | 4 | Repository and bugs 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | The **source code** for this app is hosted on 8 | https://github.com/mapeveri/django-endless-pagination-angular. 9 | 10 | To file **bugs and requests**, please use 11 | https://github.com/mapeveri/django-endless-pagination-angular/issues. 12 | 13 | Contacts 14 | ~~~~~~~~ 15 | 16 | Martín Peveri 17 | 18 | - Email: ``martinpeveri at gmail.com`` 19 | -------------------------------------------------------------------------------- /doc/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Here are the steps needed to set up a development and testing environment. 5 | 6 | Creating a development environment 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | The development environment is created in a virtualenv. The environment 10 | creation requires the *make* and *virtualenv* programs to be installed. 11 | 12 | To install *make* under Debian/Ubuntu:: 13 | 14 | $ sudo apt-get install build-essential 15 | 16 | Under Mac OS/X, *make* is available as part of XCode. 17 | 18 | To install virtualenv:: 19 | 20 | $ sudo pip install virtualenv 21 | 22 | At this point, from the root of this branch, run the command:: 23 | 24 | $ make 25 | 26 | This command will create a ``.venv`` directory in the branch root, ignored 27 | by DVCSes, containing the development virtual environment with all the 28 | dependencies. 29 | 30 | Testing the application 31 | ~~~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | Run the tests:: 34 | 35 | $ make test 36 | 37 | The command above also runs all the available integration tests. They use 38 | Selenium and require Firefox to be installed. To avoid executing integration 39 | tests, define the environment variable SKIP_SELENIUM, e.g.:: 40 | 41 | $ make test SKIP_SELENIUM=1 42 | 43 | Integration tests are excluded by default when using Python 3. The test suite 44 | requires Python >= 2.6.1. 45 | 46 | Run the tests and lint/pep8 checks:: 47 | 48 | $ make check 49 | 50 | Again, to exclude integration tests:: 51 | 52 | $ make check SKIP_SELENIUM=1 53 | 54 | Debugging 55 | ~~~~~~~~~ 56 | 57 | Run the Django shell (Python interpreter):: 58 | 59 | $ make shell 60 | 61 | Run the Django development server for manual testing:: 62 | 63 | $ make server 64 | 65 | After executing the command above, it is possible to navigate the testing 66 | project going to . 67 | 68 | See all the available make targets, including info on how to create a Python 3 69 | development environment:: 70 | 71 | $ make help 72 | 73 | Thanks for contributing, and have fun! 74 | -------------------------------------------------------------------------------- /doc/current_page_number.rst: -------------------------------------------------------------------------------- 1 | Getting the current page number 2 | =============================== 3 | 4 | In the template 5 | ~~~~~~~~~~~~~~~ 6 | 7 | You can get and display the current page number in the template using 8 | the :ref:`templatetags-show-current-number` template tag, e.g.: 9 | 10 | .. code-block:: html+django 11 | 12 | {% show_current_number %} 13 | 14 | This call will display the current page number, but you can also 15 | insert the value in the context as a template variable: 16 | 17 | .. code-block:: html+django 18 | 19 | 20 | {% show_current_number as page_number %} 21 | {{ page_number }} 22 | 23 | See the :ref:`templatetags-show-current-number` refrence for more information 24 | on accepted arguments. 25 | 26 | In the view 27 | ~~~~~~~~~~~ 28 | 29 | If you need to get the current page number in the view, you can use an utility 30 | function called ``get_page_number_from_request``, e.g.:: 31 | 32 | from endless_pagination import utils 33 | 34 | page = utils.get_page_number_from_request(request) 35 | 36 | If you are using :doc:`multiple pagination`, or you have 37 | changed the default querystring for pagination, you can pass the querystring 38 | key as an optional argument:: 39 | 40 | page = utils.get_page_number_from_request(request, querystring_key=mykey) 41 | 42 | If the page number is not present in the request, by default *1* is returned. 43 | You can change this behaviour using:: 44 | 45 | page = utils.get_page_number_from_request(request, default=3) 46 | 47 | -------------------------------------------------------------------------------- /doc/customization.rst: -------------------------------------------------------------------------------- 1 | Customization 2 | ============= 3 | 4 | Settings 5 | ~~~~~~~~ 6 | 7 | You can customize the application using ``settings.py``. 8 | 9 | ================================================= =========== ============================================== 10 | Name Default Description 11 | ================================================= =========== ============================================== 12 | ``ENDLESS_PAGINATION_PER_PAGE`` 10 How many objects are normally displayed 13 | in a page (overwriteable by templatetag). 14 | ------------------------------------------------- ----------- ---------------------------------------------- 15 | ``ENDLESS_PAGINATION_PAGE_LABEL`` 'page' The querystring key of the page number 16 | (e.g. ``http://example.com?page=2``). 17 | ------------------------------------------------- ----------- ---------------------------------------------- 18 | ``ENDLESS_PAGINATION_ORPHANS`` 0 See Django *Paginator* definition of orphans. 19 | ------------------------------------------------- ----------- ---------------------------------------------- 20 | ``ENDLESS_PAGINATION_LOADING`` 'loading' If you use the default ``show_more`` template, 21 | here you can customize the content of the 22 | loader hidden element. HTML is safe here, 23 | e.g. you can show your pretty animated GIF 24 | ``ENDLESS_PAGINATION_LOADING = """loading"""``. 25 | ------------------------------------------------- ----------- ---------------------------------------------- 26 | ``ENDLESS_PAGINATION_PREVIOUS_LABEL`` '<' Default label for the *previous* page link. 27 | ------------------------------------------------- ----------- ---------------------------------------------- 28 | ``ENDLESS_PAGINATION_NEXT_LABEL`` '>' Default label for the *next* page link. 29 | ------------------------------------------------- ----------- ---------------------------------------------- 30 | ``ENDLESS_PAGINATION_FIRST_LABEL`` '<<' Default label for the *first* page link. 31 | ------------------------------------------------- ----------- ---------------------------------------------- 32 | ``ENDLESS_PAGINATION_LAST_LABEL`` '>>' Default label for the *last* page link. 33 | ------------------------------------------------- ----------- ---------------------------------------------- 34 | ``ENDLESS_PAGINATION_ADD_NOFOLLOW`` *False* Set to *True* if your SEO alchemist 35 | wants search engines not to follow 36 | pagination links. 37 | ------------------------------------------------- ----------- ---------------------------------------------- 38 | ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` *None* Callable (or dotted path to a callable) that 39 | returns pages to be displayed. 40 | If *None*, a default callable is used; 41 | that produces :doc:`digg_pagination`. 42 | The applicationt provides also a callable 43 | producing elastic pagination: 44 | ``endless_pagination.utils.get_elastic_page_numbers``. 45 | It adapts its output to the number of pages, 46 | making it arguably more usable when there are 47 | many of them. 48 | See :doc:`templatetags_reference` for 49 | information about writing custom callables. 50 | ------------------------------------------------- ----------- ---------------------------------------------- 51 | ``ENDLESS_PAGINATION_DEFAULT_CALLABLE_EXTREMES`` 3 Deafult number of *extremes* displayed when 52 | :doc:`digg_pagination` is used with the 53 | default callable. 54 | ------------------------------------------------- ----------- ---------------------------------------------- 55 | ``ENDLESS_PAGINATION_DEFAULT_CALLABLE_AROUNDS`` 2 Deafult number of *arounds* displayed when 56 | :doc:`digg_pagination` is used with the 57 | default callable. 58 | ------------------------------------------------- ----------- ---------------------------------------------- 59 | ``ENDLESS_PAGINATION_DEFAULT_CALLABLE_ARROWS`` *False* Whether or not the first and last pages arrows 60 | are displayed when :doc:`digg_pagination` is 61 | used with the default callable. 62 | ------------------------------------------------- ----------- ---------------------------------------------- 63 | ``ENDLESS_PAGINATION_TEMPLATE_VARNAME`` 'template' Template variable name used by the 64 | ``page_template`` decorator. You can change 65 | this value if you are going to decorate 66 | generic views using a different variable name 67 | for the template (e.g. ``template_name``). 68 | ================================================= =========== ============================================== 69 | 70 | Templates and CSS 71 | ~~~~~~~~~~~~~~~~~ 72 | 73 | You can override the default template for ``show_more`` templatetag following 74 | some rules: 75 | 76 | - *more* link is shown only if the variable ``querystring`` is not False; 77 | - the container (most external html element) class is *endless_container*; 78 | - the *more* link and the loader hidden element live inside the container; 79 | - the *more* link class is *endless_more*; 80 | - the *more* link rel attribute is ``{{ querystring_key }}``; 81 | - the loader hidden element class is *endless_loading*. 82 | -------------------------------------------------------------------------------- /doc/different_first_page.rst: -------------------------------------------------------------------------------- 1 | Different number of items on the first page 2 | =========================================== 3 | 4 | Sometimes you might want to show on the first page a different number of 5 | items than on subsequent pages (e.g. in a movie detail page you want to show 6 | 4 images of the movie as a reminder, making the user click to see the next 20 7 | images). To achieve this, use the :ref:`templatetags-paginate` or 8 | :ref:`templatetags-lazy-paginate` tags with comma separated *first page* and 9 | *per page* arguments, e.g.: 10 | 11 | .. code-block:: html+django 12 | 13 | {% load endless %} 14 | 15 | {% lazy_paginate 4,20 entries %} 16 | {% for entry in entries %} 17 | {# your code to show the entry #} 18 | {% endfor %} 19 | {% show_more %} 20 | 21 | This code will display 4 entries on the first page and 20 entries on the other 22 | pages. 23 | 24 | Of course the *first page* and *per page* arguments can be passed 25 | as template variables, e.g.: 26 | 27 | .. code-block:: html+django 28 | 29 | {% lazy_paginate first_page,per_page entries %} 30 | -------------------------------------------------------------------------------- /doc/digg_pagination.rst: -------------------------------------------------------------------------------- 1 | Digg-style pagination 2 | ===================== 3 | 4 | Digg-style pagination of queryset objects is really easy to implement. If Ajax 5 | pagination is not needed, all you have to do is modifying the template, e.g.: 6 | 7 | .. code-block:: html+django 8 | 9 | {% load endless %} 10 | 11 | {% paginate entries %} 12 | {% for entry in entries %} 13 | {# your code to show the entry #} 14 | {% endfor %} 15 | {% show_pages %} 16 | 17 | That's it! As seen, the :ref:`templatetags-paginate` template tag takes care of 18 | customizing the given queryset and the current template context. The 19 | :ref:`templatetags-show-pages` one displays the page links allowing for 20 | navigation to other pages. 21 | 22 | Page by page 23 | ~~~~~~~~~~~~ 24 | 25 | If you only want to display previous and next links (in a page-by-page 26 | pagination) you have to use the lower level :ref:`templatetags-get-pages` 27 | template tag, e.g.: 28 | 29 | .. code-block:: html+django 30 | 31 | {% load endless %} 32 | 33 | {% paginate entries %} 34 | {% for entry in entries %} 35 | {# your code to show the entry #} 36 | {% endfor %} 37 | {% get_pages %} 38 | {{ pages.previous }} {{ pages.next }} 39 | 40 | :doc:`customization` explains how to customize the arrows that go to previous 41 | and next pages. 42 | 43 | Showing indexes 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The :ref:`templatetags-get-pages` template tag adds to the current template 47 | context a ``pages`` variable containing several methods that can be used to 48 | fully customize how the page links are displayed. For example, assume you want 49 | to show the indexes of the entries in the current page, followed by the total 50 | number of entries: 51 | 52 | .. code-block:: html+django 53 | 54 | {% load endless %} 55 | 56 | {% paginate entries %} 57 | {% for entry in entries %} 58 | {# your code to show the entry #} 59 | {% endfor %} 60 | {% get_pages %} 61 | Showing entries 62 | {{ pages.current_start_index }}-{{ pages.current_end_index }} of 63 | {{ pages.total_count }}. 64 | {# Just print pages to render the Digg-style pagination. #} 65 | {{ pages }} 66 | 67 | Number of pages 68 | ~~~~~~~~~~~~~~~ 69 | 70 | You can use ``{{ pages|length }}`` to retrieve and display the pages count. 71 | A common use case is to change the layout or display additional info based 72 | on whether the page list contains more than one page. This can be achieved 73 | checking ``{% if pages|length > 1 %}``, or, in a more convenient way, using 74 | ``{{ pages.paginated }}``. For example, assume you want to change the layout, 75 | or display some info, only if the page list contains more than one page, i.e. 76 | the results are actually paginated: 77 | 78 | .. code-block:: html+django 79 | 80 | {% load endless %} 81 | 82 | {% paginate entries %} 83 | {% for entry in entries %} 84 | {# your code to show the entry #} 85 | {% endfor %} 86 | {% get_pages %} 87 | {% if pages.paginated %} 88 | Some info/layout to display only if the available 89 | objects span multiple pages... 90 | {{ pages }} 91 | {% endif %} 92 | 93 | Again, for a full overview of the :ref:`templatetags-get-pages` and all the 94 | other template tags, see the :doc:`templatetags_reference`. 95 | 96 | .. _digg-ajax: 97 | 98 | Adding Ajax 99 | ~~~~~~~~~~~ 100 | 101 | The view is exactly the same as the one used in 102 | :ref:`Twitter-style Pagination`:: 103 | 104 | from endless_pagination.decorators import page_template 105 | 106 | @page_template('myapp/entry_index_page.html') # just add this decorator 107 | def entry_index( 108 | request, template='myapp/entry_index.html', extra_context=None): 109 | context = { 110 | 'entries': Entry.objects.all(), 111 | } 112 | if extra_context is not None: 113 | context.update(extra_context) 114 | return render_to_response( 115 | template, context, context_instance=RequestContext(request)) 116 | 117 | As seen before in :doc:`twitter_pagination`, you have to 118 | :ref:`split the templates`, separating the main one from 119 | the fragment representing the single page. However, this time a container for 120 | the page template is also required and, by default, must be an element having a 121 | class named *endless_page_template*. 122 | 123 | *myapp/entry_index.html* becomes: 124 | 125 | .. code-block:: html+django 126 | 127 | 128 |

Entries:

129 |
130 | {% include page_template %} 131 |
132 | 133 | {% block js %} 134 | 135 | 136 | {% endblock %} 137 | 138 | 139 | *myapp/entry_index_page.html* becomes: 140 | 141 | .. code-block:: html+django 142 | 143 | {% load endless %} 144 | 145 | {% paginate entries %} 146 | {% for entry in entries %} 147 | {# your code to show the entry #} 148 | {% endfor %} 149 | {% show_pages %} 150 | 151 | Done. 152 | 153 | It is possible to manually 154 | :ref:`override the container selector` used by 155 | *$.endlessPaginate()* to update the page contents. This can be easily achieved 156 | by customizing the *pageSelector* option of directive *endless-pagination*, e.g.: 157 | 158 | .. code-block:: html+django 159 | 160 | 161 |

Entries:

162 |
163 | {% include page_template %} 164 |
165 | 166 | {% block js %} 167 | 168 | 169 | {% endblock %} 170 | 171 | 172 | See the :doc:`javascript` for a detailed explanation of how to integrate 173 | JavaScript and Ajax features in Django Endless Pagination Angular. 174 | -------------------------------------------------------------------------------- /doc/generic_views.rst: -------------------------------------------------------------------------------- 1 | Generic views 2 | ============= 3 | 4 | Django 1.3 introduced class-based generic views 5 | (see https://docs.djangoproject.com/en/1.3/topics/class-based-views/). 6 | 7 | This application provides a customized class-based view, similar to 8 | *django.views.generic.ListView*, that allows Ajax pagination of a 9 | list of objects (usually a queryset). 10 | 11 | AjaxListView reference 12 | ~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | .. py:module:: endless_pagination.views 15 | 16 | .. py:class:: AjaxListView(django.views.generic.ListView) 17 | 18 | A class based view, similar to *django.views.generic.ListView*, 19 | that allows Ajax pagination of a list of objects. 20 | 21 | You can use this class based view in place of *ListView* in order to 22 | recreate the behaviour of the *page_template* decorator. 23 | 24 | For instance, assume you have this code (taken from Django docs):: 25 | 26 | from django.conf.urls import patterns 27 | from django.views.generic import ListView 28 | from books.models import Publisher 29 | 30 | urlpatterns = patterns('', 31 | (r'^publishers/$', ListView.as_view(model=Publisher)), 32 | ) 33 | 34 | You want to Ajax paginate publishers, so, as seen, you need to switch 35 | the template if the request is Ajax and put the page template 36 | into the context as a variable named *page_template*. 37 | 38 | This is straightforward, you only need to replace the view class, e.g.:: 39 | 40 | from django.conf.urls import patterns 41 | from books.models import Publisher 42 | 43 | from endless_pagination.views import AjaxListView 44 | 45 | urlpatterns = patterns('', 46 | (r'^publishers/$', AjaxListView.as_view(model=Publisher)), 47 | ) 48 | 49 | NOTE: Django >= 1.3 is required to use this view. 50 | 51 | 52 | .. py:attribute:: key 53 | 54 | the querystring key used for the current pagination 55 | (default: *settings.ENDLESS_PAGINATION_PAGE_LABEL*) 56 | 57 | .. py:attribute:: page_template 58 | 59 | the template used for the paginated objects 60 | 61 | .. py:attribute:: page_template_suffix 62 | 63 | the template suffix used for autogenerated page_template name 64 | (when not given, default='_page') 65 | 66 | 67 | .. py:method:: get_context_data(self, **kwargs) 68 | 69 | Adds the *page_template* variable in the context. 70 | 71 | If the *page_template* is not given as a kwarg of the *as_view* 72 | method then it is invented using app label, model name 73 | (obviously if the list is a queryset), *self.template_name_suffix* 74 | and *self.page_template_suffix*. 75 | 76 | For instance, if the list is a queryset of *blog.Entry*, 77 | the template will be *blog/entry_list_page.html*. 78 | 79 | .. py:method:: get_template_names(self) 80 | 81 | Switch the templates for Ajax requests. 82 | 83 | .. py:method:: get_page_template(self, **kwargs) 84 | 85 | Only called if *page_template* is not given as a kwarg of 86 | *self.as_view*. 87 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Django Endless Pagination Angular 3 | ================================= 4 | 5 | Django Endless Pagination Angular is a fork of the excellent application django-endless-pagination created for Francesco Banconi. 6 | This application get all code of version 2.0 and update for working in django >= 1.7 in addition to migrate code jquery to angular.js. 7 | 8 | This application provides Twitter- and Digg-style pagination, with multiple 9 | and lazy pagination and optional Ajax support. It is devoted to implementing 10 | web pagination in very few steps. 11 | 12 | The **source code** for this app is hosted at 13 | https://github.com/mapeveri/django-endless-pagination-angular. 14 | 15 | :doc:`start` is easy! 16 | 17 | Contents: 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | changelog 23 | start 24 | twitter_pagination 25 | digg_pagination 26 | multiple_pagination 27 | lazy_pagination 28 | different_first_page 29 | current_page_number 30 | templatetags_reference 31 | javascript 32 | generic_views 33 | customization 34 | contributing 35 | contacts 36 | thanks 37 | -------------------------------------------------------------------------------- /doc/lazy_pagination.rst: -------------------------------------------------------------------------------- 1 | Lazy pagination 2 | =============== 3 | 4 | Usually pagination requires hitting the database to get the total number of 5 | items to display. Lazy pagination avoids this *select count* query and results 6 | in a faster page load, with a disadvantage: you won't know the total number of 7 | pages in advance. 8 | 9 | For this reason it is better to use lazy pagination in conjunction with 10 | :doc:`twitter_pagination` (e.g. using the :ref:`templatetags-show-more` 11 | template tag). 12 | 13 | In order to switch to lazy pagination you have to use the 14 | :ref:`templatetags-lazy-paginate` template tag instead of the 15 | :ref:`templatetags-paginate` one, e.g.: 16 | 17 | .. code-block:: html+django 18 | 19 | {% load endless %} 20 | 21 | {% lazy_paginate entries %} 22 | {% for entry in entries %} 23 | {# your code to show the entry #} 24 | {% endfor %} 25 | {% show_more %} 26 | 27 | The :ref:`templatetags-lazy-paginate` tag can take all the args of the 28 | :ref:`templatetags-paginate` one, with one exception: negative indexes can not 29 | be passed to the ``starting from page`` argument. 30 | -------------------------------------------------------------------------------- /doc/multiple_pagination.rst: -------------------------------------------------------------------------------- 1 | Multiple paginations in the same page 2 | ===================================== 3 | 4 | Sometimes it is necessary to show different types of paginated objects in the 5 | same page. In this case we have to associate a different querystring key 6 | to every pagination. 7 | 8 | Normally, the key used is the one specified in 9 | ``settings.ENDLESS_PAGINATION_PAGE_LABEL`` (see :doc:`customization`), 10 | but in the case of multiple pagination the application provides a simple way to 11 | override the settings. 12 | 13 | If you do not need Ajax, the only file you need to edit is the template. 14 | Here is an example with 2 different paginations (*entries* and *other_entries*) 15 | in the same page, but there is no limit to the number of different paginations 16 | in a page: 17 | 18 | .. code-block:: html+django 19 | 20 | {% load endless %} 21 | 22 | {% paginate entries %} 23 | {% for entry in entries %} 24 | {# your code to show the entry #} 25 | {% endfor %} 26 | {% show_pages %} 27 | 28 | {# "other_entries_page" is the new querystring key #} 29 | {% paginate other_entries using "other_entries_page" %} 30 | {% for entry in other_entries %} 31 | {# your code to show the entry #} 32 | {% endfor %} 33 | {% show_pages %} 34 | 35 | The ``using`` argument of the :ref:`templatetags-paginate` template tag allows 36 | you to choose the name of the querystring key used to track the page number. 37 | If not specified the system falls back to 38 | ``settings.ENDLESS_PAGINATION_PAGE_LABEL``. 39 | 40 | In the example above, the url *http://example.com?page=2&other_entries_page=3* 41 | requests the second page of *entries* and the third page of *other_entries*. 42 | 43 | The name of the querystring key can also be dinamically passed in the template 44 | context, e.g.: 45 | 46 | .. code-block:: html+django 47 | 48 | {# page_variable is not surrounded by quotes #} 49 | {% paginate other_entries using page_variable %} 50 | 51 | You can use any style of pagination: :ref:`templatetags-show-pages`, 52 | :ref:`templatetags-get-pages`, :ref:`templatetags-show-more` etc... 53 | (see :doc:`templatetags_reference`). 54 | 55 | Adding Ajax for multiple pagination 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | Obviously each pagination needs a template for the page contents. Remember to 59 | box each page in a div with a class called *endless_page_template*, or to 60 | specify the container selector passing an option to *$.endlessPaginate()* as 61 | seen in :ref:`Digg-style pagination and Ajax`. 62 | 63 | *myapp/entry_index.html*: 64 | 65 | .. code-block:: html+django 66 | 67 | 68 |

Entries:

69 |
70 | {% include "myapp/entries_page.html" %} 71 |
72 | 73 |

Other entries:

74 |
75 | {% include "myapp/other_entries_page.html" %} 76 |
77 | 78 | {% block js %} 79 | 80 | 81 | {% endblock %} 82 | 83 | 84 | See the :doc:`javascript` for further details on how to use the directive of Angular.js. 85 | 86 | *myapp/entries_page.html*: 87 | 88 | .. code-block:: html+django 89 | 90 | {% load endless %} 91 | 92 | {% paginate entries %} 93 | {% for entry in entries %} 94 | {# your code to show the entry #} 95 | {% endfor %} 96 | {% show_pages %} 97 | 98 | *myapp/other_entries_page.html*: 99 | 100 | .. code-block:: html+django 101 | 102 | {% load endless %} 103 | 104 | {% paginate other_entries using other_entries_page %} 105 | {% for entry in other_entries %} 106 | {# your code to show the entry #} 107 | {% endfor %} 108 | {% show_pages %} 109 | 110 | As seen :ref:`before`, the decorator ``page_template`` 111 | simplifies the management of Ajax requests in views. You must, however, map 112 | different paginations to different page templates. 113 | 114 | You can chain decorator calls relating a template to the associated 115 | querystring key, e.g.:: 116 | 117 | from endless_pagination.decorators import page_template 118 | 119 | @page_template('myapp/entries_page.html') 120 | @page_template('myapp/other_entries_page.html', key='other_entries_page') 121 | def entry_index( 122 | request, template='myapp/entry_index.html', extra_context=None): 123 | context = { 124 | 'entries': Entry.objects.all(), 125 | 'other_entries': OtherEntry.objects.all(), 126 | } 127 | if extra_context is not None: 128 | context.update(extra_context) 129 | return render_to_response( 130 | template, context, context_instance=RequestContext(request)) 131 | 132 | As seen in previous examples, if you do not specify the *key* kwarg in the 133 | decorator, then the page template is associated to the querystring key 134 | defined in the settings. 135 | 136 | .. _multiple-page-templates: 137 | 138 | You can use the ``page_templates`` (note the trailing *s*) decorator in 139 | substitution of a decorator chain when you need multiple Ajax paginations. 140 | The previous example can be written as:: 141 | 142 | from endless_pagination.decorators import page_templates 143 | 144 | @page_templates({ 145 | 'myapp/entries_page.html': None, 146 | 'myapp/other_entries_page.html': 'other_entries_page', 147 | }) 148 | def entry_index(): 149 | ... 150 | 151 | As seen, a dict object is passed to the ``page_templates`` decorator, mapping 152 | templates to querystring keys. Alternatively, you can also pass a sequence 153 | of ``(template, key)`` pairs, e.g.:: 154 | 155 | from endless_pagination.decorators import page_templates 156 | 157 | @page_templates(( 158 | ('myapp/entries_page.html', None), 159 | ('myapp/other_entries_page.html', 'other_entries_page'), 160 | )) 161 | def entry_index(): 162 | ... 163 | 164 | This also supports serving different paginated objects with the same template. 165 | 166 | Manually selecting what to bind 167 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 168 | 169 | What if you need Ajax pagination only for *entries* and not for 170 | *other entries*? You can do this added this directive *endless-pagination* where is necessary. 171 | 172 | .. code-block:: html+django 173 | 174 | {% block js %} 175 | 176 | 177 | {% endblock %} 178 | 179 | The call to directive *endless-pagination* applies Ajax pagination starting 180 | from the DOM node with id *entries* and to all sub-nodes. This means that 181 | *other entries* are left intact. 182 | -------------------------------------------------------------------------------- /doc/start.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Requirements 5 | ~~~~~~~~~~~~ 6 | 7 | ====== ==================== 8 | Python >= 2.6 (or Python 3) 9 | Django >= 1.3 10 | Angular >= 1.x 11 | ====== ==================== 12 | 13 | Installation 14 | ~~~~~~~~~~~~ 15 | 16 | The Git repository can be cloned with this command:: 17 | 18 | git clone https://github.com/mapeveri/django-endless-pagination-angular.git 19 | 20 | The ``endless_pagination`` package, included in the distribution, should be 21 | placed on the ``PYTHONPATH``. 22 | 23 | Via pip ``pip install django-endless-pagination-angular``. 24 | 25 | Settings 26 | ~~~~~~~~ 27 | 28 | Add the request context processor to your *settings.py*, e.g.:: 29 | 30 | from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS 31 | TEMPLATE_CONTEXT_PROCESSORS += ( 32 | 'django.core.context_processors.request', 33 | ) 34 | 35 | Add ``'endless_pagination'`` to the ``INSTALLED_APPS`` to your *settings.py*. 36 | 37 | See the :doc:`customization` section for other settings. 38 | 39 | Quickstart 40 | ~~~~~~~~~~ 41 | 42 | Given a template like this: 43 | 44 | .. code-block:: html+django 45 | 46 | {% for entry in entries %} 47 | {# your code to show the entry #} 48 | {% endfor %} 49 | 50 | you can use Digg-style pagination to display objects just by adding: 51 | 52 | .. code-block:: html+django 53 | 54 | {% load endless %} 55 | 56 | {% paginate entries %} 57 | {% for entry in entries %} 58 | {# your code to show the entry #} 59 | {% endfor %} 60 | {% show_pages %} 61 | 62 | Done. 63 | 64 | This is just a basic example. To continue exploring all the Django Endless 65 | Pagination Angular features, have a look at :doc:`twitter_pagination` or 66 | :doc:`digg_pagination`. 67 | -------------------------------------------------------------------------------- /doc/templatetags_reference.rst: -------------------------------------------------------------------------------- 1 | Templatetags reference 2 | ====================== 3 | 4 | .. _templatetags-paginate: 5 | 6 | paginate 7 | ~~~~~~~~ 8 | 9 | Usage: 10 | 11 | .. code-block:: html+django 12 | 13 | {% paginate entries %} 14 | 15 | After this call, the *entries* variable in the template context is replaced 16 | by only the entries of the current page. 17 | 18 | You can also keep your *entries* original variable (usually a queryset) 19 | and add to the context another name that refers to entries of the current page, 20 | e.g.: 21 | 22 | .. code-block:: html+django 23 | 24 | {% paginate entries as page_entries %} 25 | 26 | The *as* argument is also useful when a nested context variable is provided 27 | as queryset. In this case, and only in this case, the resulting variable 28 | name is mandatory, e.g.: 29 | 30 | .. code-block:: html+django 31 | 32 | {% paginate entries.all as entries %} 33 | 34 | The number of paginated entries is taken from settings, but you can 35 | override the default locally, e.g.: 36 | 37 | .. code-block:: html+django 38 | 39 | {% paginate 20 entries %} 40 | 41 | Of course you can mix it all: 42 | 43 | .. code-block:: html+django 44 | 45 | {% paginate 20 entries as paginated_entries %} 46 | 47 | By default, the first page is displayed the first time you load the page, 48 | but you can change this, e.g.: 49 | 50 | .. code-block:: html+django 51 | 52 | {% paginate entries starting from page 3 %} 53 | 54 | When changing the default page, it is also possible to reference the last 55 | page (or the second last page, and so on) by using negative indexes, e.g: 56 | 57 | .. code-block:: html+django 58 | 59 | {% paginate entries starting from page -1 %} 60 | 61 | This can be also achieved using a template variable that was passed to the 62 | context, e.g.: 63 | 64 | .. code-block:: html+django 65 | 66 | {% paginate entries starting from page page_number %} 67 | 68 | If the passed page number does not exist, the first page is displayed. 69 | Note that negative indexes are specific to the ``{% paginate %}`` tag: this 70 | feature cannot be used when contents are lazy paginated (see `lazy_paginate`_ 71 | below). 72 | 73 | If you have multiple paginations in the same page, you can change the 74 | querydict key for the single pagination, e.g.: 75 | 76 | .. code-block:: html+django 77 | 78 | {% paginate entries using article_page %} 79 | 80 | In this case *article_page* is intended to be a context variable, but you can 81 | hardcode the key using quotes, e.g.: 82 | 83 | .. code-block:: html+django 84 | 85 | {% paginate entries using 'articles_at_page' %} 86 | 87 | Again, you can mix it all (the order of arguments is important): 88 | 89 | .. code-block:: html+django 90 | 91 | {% paginate 20 entries starting from page 3 using page_key as paginated_entries %} 92 | 93 | Additionally you can pass a path to be used for the pagination: 94 | 95 | .. code-block:: html+django 96 | 97 | {% paginate 20 entries using page_key with pagination_url as paginated_entries %} 98 | 99 | This way you can easily create views acting as API endpoints, and point your 100 | Ajax calls to that API. In this case *pagination_url* is considered a 101 | context variable, but it is also possible to hardcode the URL, e.g.: 102 | 103 | .. code-block:: html+django 104 | 105 | {% paginate 20 entries with "/mypage/" %} 106 | 107 | If you want the first page to contain a different number of items than 108 | subsequent pages, you can separate the two values with a comma, e.g. if 109 | you want 3 items on the first page and 10 on other pages: 110 | 111 | .. code-block:: html+django 112 | 113 | {% paginate 3,10 entries %} 114 | 115 | You must use this tag before calling the `show_more`_, `get_pages`_ or 116 | `show_pages`_ ones. 117 | 118 | .. _templatetags-lazy-paginate: 119 | 120 | lazy_paginate 121 | ~~~~~~~~~~~~~ 122 | 123 | Paginate objects without hitting the database with a *select count* query. 124 | Usually pagination requires hitting the database to get the total number of 125 | items to display. Lazy pagination avoids this *select count* query and results 126 | in a faster page load, with a disadvantage: you won't know the total number of 127 | pages in advance. 128 | 129 | Use this in the same way as `paginate`_ tag when you are not interested in the 130 | total number of pages. 131 | 132 | The ``lazy_paginate`` tag can take all the args of the ``paginate`` one, with 133 | one exception: negative indexes can not be passed to the ``starting from page`` 134 | argument. 135 | 136 | .. _templatetags-show-more: 137 | 138 | show_more 139 | ~~~~~~~~~ 140 | 141 | Show the link to get the next page in a :doc:`twitter_pagination`. Usage: 142 | 143 | .. code-block:: html+django 144 | 145 | {% show_more %} 146 | 147 | Alternatively you can override the label passed to the default template: 148 | 149 | .. code-block:: html+django 150 | 151 | {% show_more "even more" %} 152 | 153 | You can override the loading text too: 154 | 155 | .. code-block:: html+django 156 | 157 | {% show_more "even more" "working" %} 158 | 159 | Must be called after `paginate`_ or `lazy_paginate`_. 160 | 161 | .. _templatetags-get-pages: 162 | 163 | get_pages 164 | ~~~~~~~~~ 165 | 166 | Usage: 167 | 168 | .. code-block:: html+django 169 | 170 | {% get_pages %} 171 | 172 | This is mostly used for :doc:`digg_pagination`. 173 | 174 | This call inserts in the template context a *pages* variable, as a sequence 175 | of page links. You can use *pages* in different ways: 176 | 177 | - just print *pages* and you will get Digg-style pagination displayed: 178 | 179 | .. code-block:: html+django 180 | 181 | {{ pages }} 182 | 183 | - display pages count: 184 | 185 | .. code-block:: html+django 186 | 187 | {{ pages|length }} 188 | 189 | - check if the page list contains more than one page: 190 | 191 | .. code-block:: html+django 192 | 193 | {{ pages.paginated }} 194 | {# the following is equivalent #} 195 | {{ pages|length > 1 }} 196 | 197 | - get a specific page: 198 | 199 | .. code-block:: html+django 200 | 201 | {# the current selected page #} 202 | {{ pages.current }} 203 | 204 | {# the first page #} 205 | {{ pages.first }} 206 | 207 | {# the last page #} 208 | {{ pages.last }} 209 | 210 | {# the previous page (or nothing if you are on first page) #} 211 | {{ pages.previous }} 212 | 213 | {# the next page (or nothing if you are in last page) #} 214 | {{ pages.next }} 215 | 216 | {# the third page #} 217 | {{ pages.3 }} 218 | {# this means page.1 is the same as page.first #} 219 | 220 | {# the 1-based index of the first item on the current page #} 221 | {{ pages.current_start_index }} 222 | 223 | {# the 1-based index of the last item on the current page #} 224 | {{ pages.current_end_index }} 225 | 226 | {# the total number of objects, across all pages #} 227 | {{ pages.total_count }} 228 | 229 | {# the first page represented as an arrow #} 230 | {{ pages.first_as_arrow }} 231 | 232 | {# the last page represented as an arrow #} 233 | {{ pages.last_as_arrow }} 234 | 235 | - iterate over *pages* to get all pages: 236 | 237 | .. code-block:: html+django 238 | 239 | {% for page in pages %} 240 | {# display page link #} 241 | {{ page }} 242 | 243 | {# the page url (beginning with "?") #} 244 | {{ page.url }} 245 | 246 | {# the page path #} 247 | {{ page.path }} 248 | 249 | {# the page number #} 250 | {{ page.number }} 251 | 252 | {# a string representing the page (commonly the page number) #} 253 | {{ page.label }} 254 | 255 | {# check if the page is the current one #} 256 | {{ page.is_current }} 257 | 258 | {# check if the page is the first one #} 259 | {{ page.is_first }} 260 | 261 | {# check if the page is the last one #} 262 | {{ page.is_last }} 263 | {% endfor %} 264 | 265 | You can change the variable name, e.g.: 266 | 267 | .. code-block:: html+django 268 | 269 | {% get_pages as page_links %} 270 | 271 | This must be called after `paginate`_ or `lazy_paginate`_. 272 | 273 | .. _templatetags-show-pages: 274 | 275 | show_pages 276 | ~~~~~~~~~~ 277 | 278 | Show page links. Usage: 279 | 280 | .. code-block:: html+django 281 | 282 | {% show_pages %} 283 | 284 | It is just a shortcut for: 285 | 286 | .. code-block:: html+django 287 | 288 | {% get_pages %} 289 | {{ pages }} 290 | 291 | You can set ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` in your *settings.py* to 292 | a callable used to customize the pages that are displayed. 293 | ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` can also be a dotted path 294 | representing a callable, e.g.:: 295 | 296 | ENDLESS_PAGINATION_PAGE_LIST_CALLABLE = 'path.to.callable' 297 | 298 | The callable takes the current page number and the total number of pages, 299 | and must return a sequence of page numbers that will be displayed. 300 | 301 | The sequence can contain other values: 302 | 303 | - *'previous'*: will display the previous page in that position; 304 | - *'next'*: will display the next page in that position; 305 | - *'first'*: will display the first page as an arrow; 306 | - *'last'*: will display the last page as an arrow; 307 | - *None*: a separator will be displayed in that position. 308 | 309 | Here is an example of a custom callable that displays the previous page, then 310 | the first page, then a separator, then the current page, and finally the last 311 | page:: 312 | 313 | def get_page_numbers(current_page, num_pages): 314 | return ('previous', 1, None, current_page, 'last') 315 | 316 | If ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` is *None* the internal callable 317 | ``endless_pagination.utils.get_page_numbers`` is used, generating a Digg-style 318 | pagination. 319 | 320 | An alternative implementation is available: 321 | ``endless_pagination.utils.get_elastic_page_numbers``: it adapts its output 322 | to the number of pages, making it arguably more usable when there are many 323 | of them. 324 | 325 | This must be called after `paginate`_ or `lazy_paginate`_. 326 | 327 | .. _templatetags-show-current-number: 328 | 329 | show_current_number 330 | ~~~~~~~~~~~~~~~~~~~ 331 | 332 | Show the current page number, or insert it in the context. 333 | 334 | This tag can for example be useful to change the page title according to 335 | the current page number. 336 | 337 | To just show current page number: 338 | 339 | .. code-block:: html+django 340 | 341 | {% show_current_number %} 342 | 343 | If you use multiple paginations in the same page, you can get the page 344 | number for a specific pagination using the querystring key, e.g.: 345 | 346 | .. code-block:: html+django 347 | 348 | {% show_current_number using mykey %} 349 | 350 | The default page when no querystring is specified is 1. If you changed it 351 | in the `paginate`_ template tag, you have to call ``show_current_number`` 352 | according to your choice, e.g.: 353 | 354 | .. code-block:: html+django 355 | 356 | {% show_current_number starting from page 3 %} 357 | 358 | This can be also achieved using a template variable you passed to the 359 | context, e.g.: 360 | 361 | .. code-block:: html+django 362 | 363 | {% show_current_number starting from page page_number %} 364 | 365 | You can of course mix it all (the order of arguments is important): 366 | 367 | .. code-block:: html+django 368 | 369 | {% show_current_number starting from page 3 using mykey %} 370 | 371 | If you want to insert the current page number in the context, without 372 | actually displaying it in the template, use the *as* argument, i.e.: 373 | 374 | .. code-block:: html+django 375 | 376 | {% show_current_number as page_number %} 377 | {% show_current_number starting from page 3 using mykey as page_number %} 378 | -------------------------------------------------------------------------------- /doc/thanks.rst: -------------------------------------------------------------------------------- 1 | Thanks 2 | ====== 3 | 4 | This application was fork by the excellent tool 5 | *django-endless-pagination* (see https://github.com/frankban/django-endless-pagination). 6 | 7 | Thanks to Jannis Leidel for improving the application with some new features, 8 | and for contributing the German translation. 9 | 10 | And thanks to Nicola 'tekNico' Larosa for reviewing the documentation and for 11 | implementing the elastic pagination feature. 12 | -------------------------------------------------------------------------------- /doc/twitter_pagination.rst: -------------------------------------------------------------------------------- 1 | Twitter-style Pagination 2 | ======================== 3 | 4 | Assuming the developer wants Twitter-style pagination of 5 | entries of a blog post, in *views.py* we have:: 6 | 7 | def entry_index(request, template='myapp/entry_index.html'): 8 | context = { 9 | 'entries': Entry.objects.all(), 10 | } 11 | return render_to_response( 12 | template, context, context_instance=RequestContext(request)) 13 | 14 | In *myapp/entry_index.html*: 15 | 16 | .. code-block:: html+django 17 | 18 |

Entries:

19 | {% for entry in entries %} 20 | {# your code to show the entry #} 21 | {% endfor %} 22 | 23 | .. _twitter-split-template: 24 | 25 | Split the template 26 | ~~~~~~~~~~~~~~~~~~ 27 | 28 | The response to an Ajax request should not return the entire template, 29 | but only the portion of the page to be updated or added. 30 | So it is convenient to extract from the template the part containing the 31 | entries, and use it to render the context if the request is Ajax. 32 | The main template will include the extracted part, so it is convenient 33 | to put the page template name in the context. 34 | 35 | *views.py* becomes:: 36 | 37 | def entry_index( 38 | request, 39 | template='myapp/entry_index.html', 40 | page_template='myapp/entry_index_page.html'): 41 | context = { 42 | 'entries': Entry.objects.all(), 43 | 'page_template': page_template, 44 | } 45 | if request.is_ajax(): 46 | template = page_template 47 | return render_to_response( 48 | template, context, context_instance=RequestContext(request)) 49 | 50 | See :ref:`below` how to obtain the same result 51 | **just decorating the view** (in a way compatible with generic views too). 52 | 53 | *myapp/entry_index.html* becomes: 54 | 55 | .. code-block:: html+django 56 | 57 |

Entries:

58 | {% include page_template %} 59 | 60 | *myapp/entry_index_page.html* becomes: 61 | 62 | .. code-block:: html+django 63 | 64 | {% for entry in entries %} 65 | {# your code to show the entry #} 66 | {% endfor %} 67 | 68 | .. _twitter-page-template: 69 | 70 | A shortcut for ajaxed views 71 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 72 | 73 | A good practice in writing views is to allow other developers to inject 74 | the template name and extra data, so that they are added to the context. 75 | This allows the view to be easily reused. Let's resume the original view 76 | with extra context injection: 77 | 78 | *views.py*:: 79 | 80 | def entry_index( 81 | request, template='myapp/entry_index.html', extra_context=None): 82 | context = { 83 | 'entries': Entry.objects.all(), 84 | } 85 | if extra_context is not None: 86 | context.update(extra_context) 87 | return render_to_response( 88 | template, context, context_instance=RequestContext(request)) 89 | 90 | Splitting templates and putting the Ajax template name in the context 91 | is easily achievable by using an included decorator. 92 | 93 | *views.py* becomes:: 94 | 95 | from endless_pagination.decorators import page_template 96 | 97 | @page_template('myapp/entry_index_page.html') # just add this decorator 98 | def entry_index( 99 | request, template='myapp/entry_index.html', extra_context=None): 100 | context = { 101 | 'entries': Entry.objects.all(), 102 | } 103 | if extra_context is not None: 104 | context.update(extra_context) 105 | return render_to_response( 106 | template, context, context_instance=RequestContext(request)) 107 | 108 | This way, *endless-pagination* can be included in **generic views** too. 109 | 110 | See :doc:`generic_views` if you use Django >= 1.3 and you want to replicate 111 | the same behavior using a class-based generic view. 112 | 113 | Paginating objects 114 | ~~~~~~~~~~~~~~~~~~ 115 | 116 | All that's left is changing the page template and loading the 117 | :doc:`endless templatetags`, the jQuery library and the 118 | jQuery plugin ``endless-pagination.js`` included in the distribution under 119 | ``/static/endless_pagination/js/``. 120 | 121 | *myapp/entry_index.html* becomes: 122 | 123 | .. code-block:: html+django 124 | 125 | 126 |

Entries:

127 |
128 | {% include page_template %} 129 |
130 | 131 | {% block js %} 132 | 133 | 134 | {% endblock %} 135 | 136 | 137 | *myapp/entry_index_page.html* becomes: 138 | 139 | .. code-block:: html+django 140 | 141 | {% load endless %} 142 | 143 | {% paginate entries %} 144 | {% for entry in entries %} 145 | {# your code to show the entry #} 146 | {% endfor %} 147 | {% show_more %} 148 | 149 | The :ref:`templatetags-paginate` template tag takes care of customizing the 150 | given queryset and the current template context. In the context of a 151 | Twitter-style pagination the :ref:`templatetags-paginate` tag is often replaced 152 | by the :ref:`templatetags-lazy-paginate` one, which offers, more or less, the 153 | same functionalities and allows for reducing database access: see 154 | :doc:`lazy_pagination`. 155 | 156 | The :ref:`templatetags-show-more` one displays the link to navigate to the next 157 | page. 158 | 159 | You might want to glance at the :doc:`javascript` for a detailed explanation of 160 | how to integrate JavaScript and Ajax features in Django Endless Pagination Angular. 161 | 162 | Pagination on scroll 163 | ~~~~~~~~~~~~~~~~~~~~ 164 | 165 | If you want new items to load when the user scroll down the browser page, 166 | you can use the :ref:`pagination on scroll` 167 | feature: just set the *paginateOnScroll* option of the directive *endless-pagination* to 168 | *true*, e.g.: 169 | 170 | .. code-block:: html+django 171 | 172 | 173 |

Entries:

174 |
177 | 178 | {% block js %} 179 | 180 | 181 | {% endblock %} 182 | 183 | 184 | That's all. See the :doc:`templatetags_reference` to improve the use of 185 | included templatetags. 186 | 187 | It is possible to set the bottom margin used for 188 | :ref:`pagination on scroll` (default is 1 189 | pixel). For example, if you want the pagination on scroll to be activated when 190 | 20 pixels remain to the end of the page: 191 | 192 | .. code-block:: html+django 193 | 194 | 195 |

Entries:

196 |
199 | 200 | {% block js %} 201 | 202 | 203 | {% endblock %} 204 | 205 | 206 | Again, see the :doc:`javascript`. 207 | 208 | On scroll pagination using chunks 209 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 210 | 211 | Sometimes, when using on scroll pagination, you may want to still display 212 | the *show more* link after each *N* pages. In Django Endless Pagination Angular this is 213 | called *chunk size*. For instance, a chunk size of 5 means that a *show more* 214 | link is displayed after page 5 is loaded, then after page 10, then after page 215 | 15 and so on. Activating :ref:`chunks` is straightforward, 216 | just use the *paginateOnScrollChunkSize* option: 217 | 218 | .. code-block:: html+django 219 | 220 | 221 |

Entries:

222 |
225 | 226 | {% block js %} 227 | 228 | 229 | {% endblock %} 230 | 231 | -------------------------------------------------------------------------------- /endless_pagination/__init__.py: -------------------------------------------------------------------------------- 1 | """Django pagination tools supporting Ajax, multiple and lazy pagination, 2 | Twitter-style and Digg-style pagination. 3 | """ 4 | 5 | from __future__ import unicode_literals 6 | 7 | 8 | VERSION = (1, 2) 9 | 10 | 11 | def get_version(): 12 | """Return the Django Endless Pagination Angular version as a string.""" 13 | return '.'.join(map(str, VERSION)) 14 | -------------------------------------------------------------------------------- /endless_pagination/decorators.py: -------------------------------------------------------------------------------- 1 | """View decorators for Ajax powered pagination.""" 2 | 3 | from __future__ import unicode_literals 4 | from functools import wraps 5 | 6 | from endless_pagination.settings import ( 7 | PAGE_LABEL, 8 | TEMPLATE_VARNAME, 9 | ) 10 | 11 | 12 | def page_template(template, key=PAGE_LABEL): 13 | """Return a view dynamically switching template if the request is Ajax. 14 | 15 | Decorate a view that takes a *template* and *extra_context* keyword 16 | arguments (like generic views). 17 | The template is switched to *page_template* if request is ajax and 18 | if *querystring_key* variable passed by the request equals to *key*. 19 | This allows multiple Ajax paginations in the same page. 20 | The name of the page template is given as *page_template* in the 21 | extra context. 22 | """ 23 | def decorator(view): 24 | @wraps(view) 25 | def decorated(request, *args, **kwargs): 26 | # Trust the developer: he wrote ``context.update(extra_context)`` 27 | # in his view. 28 | extra_context = kwargs.setdefault('extra_context', {}) 29 | extra_context['page_template'] = template 30 | # Switch the template when the request is Ajax. 31 | if request.method == 'POST': 32 | querystring_key = request.POST.get( 33 | 'querystring_key', PAGE_LABEL) 34 | else: 35 | querystring_key = request.GET.get( 36 | 'querystring_key', PAGE_LABEL) 37 | if request.is_ajax() and querystring_key == key: 38 | kwargs[TEMPLATE_VARNAME] = template 39 | return view(request, *args, **kwargs) 40 | return decorated 41 | 42 | return decorator 43 | 44 | 45 | def _get_template(querystring_key, mapping): 46 | """Return the template corresponding to the given ``querystring_key``.""" 47 | default = None 48 | try: 49 | template_and_keys = mapping.items() 50 | except AttributeError: 51 | template_and_keys = mapping 52 | for template, key in template_and_keys: 53 | if key is None: 54 | key = PAGE_LABEL 55 | default = template 56 | if key == querystring_key: 57 | return template 58 | return default 59 | 60 | 61 | def page_templates(mapping): 62 | """Like the *page_template* decorator but manage multiple paginations. 63 | 64 | You can map multiple templates to *querystring_keys* using the *mapping* 65 | dict, e.g.:: 66 | 67 | @page_templates({ 68 | 'page_contents1.html': None, 69 | 'page_contents2.html': 'go_to_page', 70 | }) 71 | def myview(request): 72 | ... 73 | 74 | When the value of the dict is None then the default *querystring_key* 75 | (defined in settings) is used. You can use this decorator instead of 76 | chaining multiple *page_template* calls. 77 | """ 78 | def decorator(view): 79 | @wraps(view) 80 | def decorated(request, *args, **kwargs): 81 | # Trust the developer: he wrote ``context.update(extra_context)`` 82 | # in his view. 83 | extra_context = kwargs.setdefault('extra_context', {}) 84 | 85 | if request.method == 'POST': 86 | querystring_key = request.POST.get( 87 | 'querystring_key', PAGE_LABEL) 88 | else: 89 | querystring_key = request.GET.get( 90 | 'querystring_key', PAGE_LABEL) 91 | template = _get_template(querystring_key, mapping) 92 | extra_context['page_template'] = template 93 | # Switch the template when the request is Ajax. 94 | if request.is_ajax() and template: 95 | kwargs[TEMPLATE_VARNAME] = template 96 | return view(request, *args, **kwargs) 97 | return decorated 98 | 99 | return decorator 100 | -------------------------------------------------------------------------------- /endless_pagination/exceptions.py: -------------------------------------------------------------------------------- 1 | """Pagination exceptions.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | 6 | class PaginationError(Exception): 7 | """Error in the pagination process.""" 8 | -------------------------------------------------------------------------------- /endless_pagination/loaders.py: -------------------------------------------------------------------------------- 1 | """Django Endless Pagination object loaders.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | try: 8 | from importlib import import_module 9 | except ImportError: 10 | from django.utils.importlib import import_module 11 | 12 | 13 | def load_object(path): 14 | """Return the Python object represented by dotted *path*.""" 15 | i = path.rfind('.') 16 | module_name, object_name = path[:i], path[i + 1:] 17 | # Load module. 18 | try: 19 | module = import_module(module_name) 20 | except ImportError: 21 | raise ImproperlyConfigured('Module %r not found' % module_name) 22 | except ValueError: 23 | raise ImproperlyConfigured('Invalid module %r' % module_name) 24 | # Load object. 25 | try: 26 | return getattr(module, object_name) 27 | except AttributeError: 28 | msg = 'Module %r does not define an object named %r' 29 | raise ImproperlyConfigured(msg % (module_name, object_name)) 30 | -------------------------------------------------------------------------------- /endless_pagination/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /endless_pagination/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Locale 2 | # Copyright (C) 2009-2013 Francesco Banconi 3 | # This file is distributed under the same license as the django-endless-pagination package. 4 | # Francesco Banconi , 2009. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-03-04 17:52+0100\n" 12 | "PO-Revision-Date: 2010-03-04 18:00+0100\n" 13 | "Last-Translator: Jannis Leidel \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: templates/endless/show_more.html:4 21 | msgid "more" 22 | msgstr "mehr" 23 | -------------------------------------------------------------------------------- /endless_pagination/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /endless_pagination/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Locale 2 | # Copyright (C) 2009-2013 Francesco Banconi 3 | # This file is distributed under the same license as the django-endless-pagination package. 4 | # Francesco Banconi , 2013. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-02-11 18:03+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Francesco Banconi \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | 21 | #: templates/endless/show_more.html:5 22 | msgid "more" 23 | msgstr "Más resultados" 24 | -------------------------------------------------------------------------------- /endless_pagination/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /endless_pagination/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Locale 2 | # Copyright (C) 2009-2013 Francesco Banconi 3 | # This file is distributed under the same license as the django-endless-pagination package. 4 | # Francesco Banconi , 2013. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-02-11 16:06+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 20 | 21 | #: templates/endless/show_more.html:5 22 | msgid "more" 23 | msgstr "En voir plus" 24 | -------------------------------------------------------------------------------- /endless_pagination/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /endless_pagination/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Locale 2 | # Copyright (C) 2009-2013 Francesco Banconi 3 | # This file is distributed under the same license as the django-endless-pagination package. 4 | # Francesco Banconi , 2009. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2009-08-22 19:21+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Francesco Banconi \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: templates/endless/show_more.html:4 20 | msgid "more" 21 | msgstr "mostra altri" 22 | -------------------------------------------------------------------------------- /endless_pagination/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /endless_pagination/locale/zh_CN/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Locale 2 | # Copyright (C) 2009-2013 Francesco Banconi 3 | # This file is distributed under the same license as the django-endless-pagination package. 4 | # Francesco Banconi , 2013. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2009-08-22 19:21+0200\n" 12 | "PO-Revision-Date: 2013-02-11 16:54+0800\n" 13 | "Last-Translator: mozillazg \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: templates/endless/show_more.html:4 20 | msgid "more" 21 | msgstr "查看更多" 22 | -------------------------------------------------------------------------------- /endless_pagination/models.py: -------------------------------------------------------------------------------- 1 | """Ephemeral models used to represent a page and a list of pages.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.template import ( 6 | loader, 7 | RequestContext, 8 | ) 9 | from django.utils.encoding import iri_to_uri 10 | 11 | from endless_pagination import ( 12 | loaders, 13 | settings, 14 | utils, 15 | ) 16 | 17 | 18 | # Page templates cache. 19 | _template_cache = {} 20 | 21 | 22 | class EndlessPage(utils.UnicodeMixin): 23 | """A page link representation. 24 | 25 | Interesting attributes: 26 | 27 | - *self.number*: the page number; 28 | - *self.label*: the label of the link 29 | (usually the page number as string); 30 | - *self.url*: the url of the page (strting with "?"); 31 | - *self.path*: the path of the page; 32 | - *self.is_current*: return True if page is the current page displayed; 33 | - *self.is_first*: return True if page is the first page; 34 | - *self.is_last*: return True if page is the last page. 35 | """ 36 | 37 | def __init__( 38 | self, request, number, current_number, total_number, 39 | querystring_key, label=None, default_number=1, override_path=None): 40 | self._request = request 41 | self.number = number 42 | self.label = utils.text(number) if label is None else label 43 | self.querystring_key = querystring_key 44 | 45 | self.is_current = number == current_number 46 | self.is_first = number == 1 47 | self.is_last = number == total_number 48 | 49 | self.url = utils.get_querystring_for_page( 50 | request, number, self.querystring_key, 51 | default_number=default_number) 52 | path = iri_to_uri(override_path or request.path) 53 | self.path = '{0}{1}'.format(path, self.url) 54 | 55 | def __unicode__(self): 56 | """Render the page as a link.""" 57 | context = { 58 | 'add_nofollow': settings.ADD_NOFOLLOW, 59 | 'page': self, 60 | 'querystring_key': self.querystring_key, 61 | } 62 | if self.is_current: 63 | template_name = 'endless/current_link.html' 64 | else: 65 | template_name = 'endless/page_link.html' 66 | template = _template_cache.setdefault( 67 | template_name, loader.get_template(template_name)) 68 | return template.render(RequestContext(self._request, context)) 69 | 70 | 71 | class PageList(utils.UnicodeMixin): 72 | """A sequence of endless pages.""" 73 | 74 | def __init__( 75 | self, request, page, querystring_key, 76 | default_number=None, override_path=None): 77 | self._request = request 78 | self._page = page 79 | if default_number is None: 80 | self._default_number = 1 81 | else: 82 | self._default_number = int(default_number) 83 | self._querystring_key = querystring_key 84 | self._override_path = override_path 85 | 86 | def _endless_page(self, number, label=None): 87 | """Factory function that returns a *EndlessPage* instance. 88 | 89 | This method works just like a partial constructor. 90 | """ 91 | return EndlessPage( 92 | self._request, 93 | number, 94 | self._page.number, 95 | len(self), 96 | self._querystring_key, 97 | label=label, 98 | default_number=self._default_number, 99 | override_path=self._override_path, 100 | ) 101 | 102 | def __getitem__(self, value): 103 | # The type conversion is required here because in templates Django 104 | # performs a dictionary lookup before the attribute lokups 105 | # (when a dot is encountered). 106 | try: 107 | value = int(value) 108 | except (TypeError, ValueError): 109 | # A TypeError says to django to continue with an attribute lookup. 110 | raise TypeError 111 | if 1 <= value <= len(self): 112 | return self._endless_page(value) 113 | raise IndexError('page list index out of range') 114 | 115 | def __len__(self): 116 | """The length of the sequence is the total number of pages.""" 117 | return self._page.paginator.num_pages 118 | 119 | def __iter__(self): 120 | """Iterate over all the endless pages (from first to last).""" 121 | for i in range(len(self)): 122 | yield self[i + 1] 123 | 124 | def __unicode__(self): 125 | """Return a rendered Digg-style pagination (by default). 126 | 127 | The callable *settings.PAGE_LIST_CALLABLE* can be used to customize 128 | how the pages are displayed. The callable takes the current page number 129 | and the total number of pages, and must return a sequence of page 130 | numbers that will be displayed. The sequence can contain other values: 131 | 132 | - *'previous'*: will display the previous page in that position; 133 | - *'next'*: will display the next page in that position; 134 | - *'first'*: will display the first page as an arrow; 135 | - *'last'*: will display the last page as an arrow; 136 | - *None*: a separator will be displayed in that position. 137 | 138 | Here is an example of custom calable that displays the previous page, 139 | then the first page, then a separator, then the current page, and 140 | finally the last page:: 141 | 142 | def get_page_numbers(current_page, num_pages): 143 | return ('previous', 1, None, current_page, 'last') 144 | 145 | If *settings.PAGE_LIST_CALLABLE* is None an internal callable is used, 146 | generating a Digg-style pagination. The value of 147 | *settings.PAGE_LIST_CALLABLE* can also be a dotted path to a callable. 148 | """ 149 | if len(self) > 1: 150 | callable_or_path = settings.PAGE_LIST_CALLABLE 151 | if callable_or_path: 152 | if callable(callable_or_path): 153 | pages_callable = callable_or_path 154 | else: 155 | pages_callable = loaders.load_object(callable_or_path) 156 | else: 157 | pages_callable = utils.get_page_numbers 158 | pages = [] 159 | for item in pages_callable(self._page.number, len(self)): 160 | if item is None: 161 | pages.append(None) 162 | elif item == 'previous': 163 | pages.append(self.previous()) 164 | elif item == 'next': 165 | pages.append(self.next()) 166 | elif item == 'first': 167 | pages.append(self.first_as_arrow()) 168 | elif item == 'last': 169 | pages.append(self.last_as_arrow()) 170 | else: 171 | pages.append(self[item]) 172 | context = RequestContext(self._request, {'pages': pages}) 173 | return loader.render_to_string('endless/show_pages.html', context) 174 | return '' 175 | 176 | def current(self): 177 | """Return the current page.""" 178 | return self._endless_page(self._page.number) 179 | 180 | def current_start_index(self): 181 | """Return the 1-based index of the first item on the current page.""" 182 | return self._page.start_index() 183 | 184 | def current_end_index(self): 185 | """Return the 1-based index of the last item on the current page.""" 186 | return self._page.end_index() 187 | 188 | def total_count(self): 189 | """Return the total number of objects, across all pages.""" 190 | return self._page.paginator.count 191 | 192 | def first(self, label=None): 193 | """Return the first page.""" 194 | return self._endless_page(1, label=label) 195 | 196 | def last(self, label=None): 197 | """Return the last page.""" 198 | return self._endless_page(len(self), label=label) 199 | 200 | def first_as_arrow(self): 201 | """Return the first page as an arrow. 202 | 203 | The page label (arrow) is defined in ``settings.FIRST_LABEL``. 204 | """ 205 | return self.first(label=settings.FIRST_LABEL) 206 | 207 | def last_as_arrow(self): 208 | """Return the last page as an arrow. 209 | 210 | The page label (arrow) is defined in ``settings.LAST_LABEL``. 211 | """ 212 | return self.last(label=settings.LAST_LABEL) 213 | 214 | def previous(self): 215 | """Return the previous page. 216 | 217 | The page label is defined in ``settings.PREVIOUS_LABEL``. 218 | Return an empty string if current page is the first. 219 | """ 220 | if self._page.has_previous(): 221 | return self._endless_page( 222 | self._page.previous_page_number(), 223 | label=settings.PREVIOUS_LABEL) 224 | return '' 225 | 226 | def next(self): 227 | """Return the next page. 228 | 229 | The page label is defined in ``settings.NEXT_LABEL``. 230 | Return an empty string if current page is the last. 231 | """ 232 | if self._page.has_next(): 233 | return self._endless_page( 234 | self._page.next_page_number(), 235 | label=settings.NEXT_LABEL) 236 | return '' 237 | 238 | def paginated(self): 239 | """Return True if this page list contains more than one page.""" 240 | return len(self) > 1 241 | -------------------------------------------------------------------------------- /endless_pagination/paginators.py: -------------------------------------------------------------------------------- 1 | """Customized Django paginators.""" 2 | 3 | from __future__ import unicode_literals 4 | from math import ceil 5 | 6 | from django.core.paginator import ( 7 | EmptyPage, 8 | Page, 9 | PageNotAnInteger, 10 | Paginator, 11 | ) 12 | 13 | 14 | class CustomPage(Page): 15 | """Handle different number of items on the first page.""" 16 | 17 | def start_index(self): 18 | """Return the 1-based index of the first item on this page.""" 19 | paginator = self.paginator 20 | # Special case, return zero if no items. 21 | if paginator.count == 0: 22 | return 0 23 | elif self.number == 1: 24 | return 1 25 | return ( 26 | (self.number - 2) * paginator.per_page + paginator.first_page + 1) 27 | 28 | def end_index(self): 29 | """Return the 1-based index of the last item on this page.""" 30 | paginator = self.paginator 31 | # Special case for the last page because there can be orphans. 32 | if self.number == paginator.num_pages: 33 | return paginator.count 34 | return (self.number - 1) * paginator.per_page + paginator.first_page 35 | 36 | 37 | class BasePaginator(Paginator): 38 | """A base paginator class subclassed by the other real paginators. 39 | 40 | Handle different number of items on the first page. 41 | """ 42 | 43 | def __init__(self, object_list, per_page, **kwargs): 44 | self._num_pages = None 45 | if 'first_page' in kwargs: 46 | self.first_page = kwargs.pop('first_page') 47 | else: 48 | self.first_page = per_page 49 | super(BasePaginator, self).__init__(object_list, per_page, **kwargs) 50 | 51 | def get_current_per_page(self, number): 52 | return self.first_page if number == 1 else self.per_page 53 | 54 | 55 | class DefaultPaginator(BasePaginator): 56 | """The default paginator used by this application.""" 57 | 58 | def page(self, number): 59 | number = self.validate_number(number) 60 | if number == 1: 61 | bottom = 0 62 | else: 63 | bottom = ((number - 2) * self.per_page + self.first_page) 64 | top = bottom + self.get_current_per_page(number) 65 | if top + self.orphans >= self.count: 66 | top = self.count 67 | return CustomPage(self.object_list[bottom:top], number, self) 68 | 69 | def _get_num_pages(self): 70 | if self._num_pages is None: 71 | if self.count == 0 and not self.allow_empty_first_page: 72 | self._num_pages = 0 73 | else: 74 | hits = max(0, self.count - self.orphans - self.first_page) 75 | self._num_pages = int(ceil(hits / float(self.per_page))) + 1 76 | return self._num_pages 77 | num_pages = property(_get_num_pages) 78 | 79 | 80 | class LazyPaginator(BasePaginator): 81 | """Implement lazy pagination.""" 82 | 83 | def validate_number(self, number): 84 | try: 85 | number = int(number) 86 | except ValueError: 87 | raise PageNotAnInteger('That page number is not an integer') 88 | if number < 1: 89 | raise EmptyPage('That page number is less than 1') 90 | return number 91 | 92 | def page(self, number): 93 | number = self.validate_number(number) 94 | current_per_page = self.get_current_per_page(number) 95 | if number == 1: 96 | bottom = 0 97 | else: 98 | bottom = ((number - 2) * self.per_page + self.first_page) 99 | top = bottom + current_per_page 100 | # Retrieve more objects to check if there is a next page. 101 | objects = list(self.object_list[bottom:top + self.orphans + 1]) 102 | objects_count = len(objects) 103 | if objects_count > (current_per_page + self.orphans): 104 | # If another page is found, increase the total number of pages. 105 | self._num_pages = number + 1 106 | # In any case, return only objects for this page. 107 | objects = objects[:current_per_page] 108 | elif (number != 1) and (objects_count <= self.orphans): 109 | raise EmptyPage('That page contains no results') 110 | else: 111 | # This is the last page. 112 | self._num_pages = number 113 | return CustomPage(objects, number, self) 114 | 115 | def _get_count(self): 116 | raise NotImplementedError 117 | 118 | count = property(_get_count) 119 | 120 | def _get_num_pages(self): 121 | return self._num_pages 122 | 123 | num_pages = property(_get_num_pages) 124 | 125 | def _get_page_range(self): 126 | raise NotImplementedError 127 | 128 | page_range = property(_get_page_range) 129 | -------------------------------------------------------------------------------- /endless_pagination/settings.py: -------------------------------------------------------------------------------- 1 | # """Django Endless Pagination settings file.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | 7 | 8 | # How many objects are normally displayed in a page 9 | # (overwriteable by templatetag). 10 | PER_PAGE = getattr(settings, 'ENDLESS_PAGINATION_PER_PAGE', 10) 11 | # The querystring key of the page number. 12 | PAGE_LABEL = getattr(settings, 'ENDLESS_PAGINATION_PAGE_LABEL', 'page') 13 | # See django *Paginator* definition of orphans. 14 | ORPHANS = getattr(settings, 'ENDLESS_PAGINATION_ORPHANS', 0) 15 | 16 | # If you use the default *show_more* template, here you can customize 17 | # the content of the loader hidden element. 18 | # Html is safe here, e.g. you can show your pretty animated gif: 19 | # ENDLESS_PAGINATION_LOADING = """ 20 | # loading 21 | # """ 22 | LOADING = getattr( 23 | settings, 'ENDLESS_PAGINATION_LOADING', 'loading') 24 | 25 | # Labels for previous and next page links. 26 | PREVIOUS_LABEL = getattr( 27 | settings, 'ENDLESS_PAGINATION_PREVIOUS_LABEL', '<') 28 | NEXT_LABEL = getattr(settings, 'ENDLESS_PAGINATION_NEXT_LABEL', '>') 29 | 30 | # Labels for first and last page links. 31 | FIRST_LABEL = getattr( 32 | settings, 'ENDLESS_PAGINATION_FIRST_LABEL', '<<') 33 | LAST_LABEL = getattr(settings, 'ENDLESS_PAGINATION_LAST_LABEL', '>>') 34 | 35 | # Set to True if your SEO alchemist wants all the links in Digg-style 36 | # pagination to be ``nofollow``. 37 | ADD_NOFOLLOW = getattr(settings, 'ENDLESS_PAGINATION_ADD_NOFOLLOW', False) 38 | 39 | # Callable (or dotted path to a callable) returning pages to be displayed. 40 | # If None, a default callable is used (which produces Digg-style pagination). 41 | PAGE_LIST_CALLABLE = getattr( 42 | settings, 'ENDLESS_PAGINATION_PAGE_LIST_CALLABLE', None) 43 | 44 | # The default callable returns a sequence of pages producing Digg-style 45 | # pagination, and depending on the settings below. 46 | DEFAULT_CALLABLE_EXTREMES = getattr( 47 | settings, 'ENDLESS_PAGINATION_DEFAULT_CALLABLE_EXTREMES', 3) 48 | DEFAULT_CALLABLE_AROUNDS = getattr( 49 | settings, 'ENDLESS_PAGINATION_DEFAULT_CALLABLE_AROUNDS', 2) 50 | # Whether or not the first and last pages arrows are displayed. 51 | DEFAULT_CALLABLE_ARROWS = getattr( 52 | settings, 'ENDLESS_PAGINATION_DEFAULT_CALLABLE_ARROWS', False) 53 | 54 | # Template variable name for *page_template* decorator. 55 | TEMPLATE_VARNAME = getattr( 56 | settings, 'ENDLESS_PAGINATION_TEMPLATE_VARNAME', 'template') 57 | -------------------------------------------------------------------------------- /endless_pagination/static/endless_pagination/js/module.endless.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var App = angular.module('EndlessPagination', []); 3 | 4 | /*Closest*/ 5 | (function (ELEMENT) { 6 | ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector; 7 | ELEMENT.closest = ELEMENT.closest || function closest(selector) { 8 | var element = this; 9 | while (element) { 10 | if (element.matches(selector)) { 11 | break; 12 | } 13 | element = element.parentElement; 14 | } 15 | return element; 16 | }; 17 | }(Element.prototype)); 18 | 19 | /* polyfill for Element.prototype.matches */ 20 | if ( !Element.prototype.matches ) { 21 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector; 22 | if ( !Element.prototype.matches ) { 23 | Element.prototype.matches = function matches( selector ) { 24 | var element = this; 25 | var matches = ( element.document || element.ownerDocument ).querySelectorAll( selector ); 26 | var i = 0; 27 | while ( matches[i] && matches[i] !== element ) { 28 | i++; 29 | } 30 | return matches[i] ? true : false; 31 | } 32 | } 33 | } 34 | 35 | /* addDelegatedEventListener */ 36 | Element.prototype.addDelegatedEventListener = function addDelegatedEventListener( type, selector, listener, useCapture, wantsUntrusted ) { 37 | this.addEventListener( type, function ( evt ) { 38 | var element = evt.target; 39 | do { 40 | if ( !element.matches || !element.matches( selector ) ) continue; 41 | listener.apply( element, arguments ); 42 | return; 43 | } while ( ( element = element.parentNode ) ); 44 | }, useCapture, wantsUntrusted ); 45 | } 46 | 47 | 48 | 49 | App.directive('endlessPagination', function($http, $window, $document) { 50 | return function (scope, element, attrs) { 51 | 52 | var defaults = { 53 | // Twitter-style pagination container selector. 54 | containerSelector: '.endless_container', 55 | // Twitter-style pagination loading selector. 56 | loadingSelector: '.endless_loading', 57 | // Twitter-style pagination link selector. 58 | moreSelector: 'a.endless_more', 59 | // Digg-style pagination page template selector. 60 | pageSelector: '.endless_page_template', 61 | // Digg-style pagination link selector. 62 | pagesSelector: 'a.endless_page_link', 63 | // Callback called when the user clicks to get another page. 64 | onClick: function() {}, 65 | // Callback called when the new page is correctly displayed. 66 | onCompleted: function() {}, 67 | // Set this to true to use the paginate-on-scroll feature. 68 | paginateOnScroll: false, 69 | // If paginate-on-scroll is on, this margin will be used. 70 | paginateOnScrollMargin : 1, 71 | // If paginate-on-scroll is on, it is possible to define chunks. 72 | paginateOnScrollChunkSize: 0 73 | }, 74 | 75 | settings = angular.extend(defaults, (attrs.endlessPagination ? eval('(' + attrs.endlessPagination + ')') : "")); 76 | 77 | var getContext = function(link) { 78 | return { 79 | key: link.attr('rel').split(' ')[0], 80 | url: link.attr('href') 81 | }; 82 | }; 83 | 84 | return angular.forEach(element, function() { 85 | var loadedPages = 1; 86 | // Twitter-style pagination. 87 | element[0].addDelegatedEventListener('click', settings.moreSelector, function ($event) { 88 | var link = angular.element(this); 89 | var html_link = link[0]; 90 | var container = angular.element(html_link.closest(settings.containerSelector)); 91 | var loading = container.children(settings.loadingSelector); 92 | // Avoid multiple Ajax calls. 93 | if (loading.offsetWidth > 0 && loading.offsetHeight > 0) { 94 | $event.preventDefault(); 95 | } 96 | link[0].style.display="none"; 97 | loading[0].style.display="block"; 98 | var context = getContext(link); 99 | //For get function onClick 100 | if(typeof settings.onClick == 'string'){ 101 | var onClick = scope.$eval(settings.onClick); 102 | }else{ 103 | var onClick = settings.onClick; 104 | } 105 | //For get function onComplete 106 | if(typeof settings.onCompleted == 'string'){ 107 | var onCompleted = scope.$eval(settings.onCompleted); 108 | }else{ 109 | var onCompleted = settings.onCompleted; 110 | } 111 | // Fire onClick callback. 112 | if (onClick.apply(html_link, [context]) !== false) { 113 | // Send the Ajax request. 114 | $http({ 115 | method: "GET", 116 | url: context.url, 117 | params: {querystring_key: context.key}, 118 | headers: { 119 | 'X-Requested-With': 'XMLHttpRequest' 120 | } 121 | }).success(function(fragment){ 122 | container.parent().append(fragment); 123 | container.remove(); 124 | // Increase the number of loaded pages. 125 | loadedPages += 1; 126 | // Fire onCompleted callback. 127 | onCompleted.apply(html_link, [context, fragment.trim()]); 128 | }); 129 | } 130 | $event.preventDefault(); 131 | }); 132 | 133 | // On scroll pagination. 134 | if (settings.paginateOnScroll) { 135 | angular.element($window).bind("scroll", function() { 136 | if ($document[0].body.offsetHeight - $window.innerHeight - 137 | $window.pageYOffset <= settings.paginateOnScrollMargin) { 138 | // Do not paginate on scroll if chunks are used and 139 | // the current chunk is complete. 140 | var chunckSize = settings.paginateOnScrollChunkSize; 141 | if (!chunckSize || loadedPages % chunckSize) { 142 | if(document.querySelector(settings.moreSelector) != null){ 143 | document.querySelector(settings.moreSelector).click(); 144 | } 145 | } 146 | } 147 | }); 148 | } 149 | 150 | // Digg-style pagination. 151 | element[0].addDelegatedEventListener('click', settings.pagesSelector, function ($event) { 152 | var link = angular.element(this), 153 | html_link = link[0], 154 | context = getContext(link); 155 | //For get function onClick 156 | if(typeof settings.onClick == 'string'){ 157 | var onClick = scope.$eval(settings.onClick); 158 | }else{ 159 | var onClick = settings.onClick; 160 | } 161 | //For get function onComplete 162 | if(typeof settings.onCompleted == 'string'){ 163 | var onCompleted = scope.$eval(settings.onCompleted); 164 | }else{ 165 | var onCompleted = settings.onCompleted; 166 | } 167 | // Fire onClick callback. 168 | if (onClick.apply(html_link, [context]) !== false) { 169 | var page_template = angular.element(html_link.closest(settings.pageSelector)); 170 | $http({ 171 | method: "GET", 172 | url: context.url, 173 | params: {querystring_key: context.key}, 174 | headers: { 175 | 'X-Requested-With': 'XMLHttpRequest' 176 | } 177 | }).success(function(fragment){ 178 | page_template.html(fragment); 179 | onCompleted.apply(html_link, [context, fragment.trim()]); 180 | }); 181 | } 182 | $event.preventDefault(); 183 | }); 184 | }); 185 | }; 186 | }); 187 | -------------------------------------------------------------------------------- /endless_pagination/templates/endless/current_link.html: -------------------------------------------------------------------------------- 1 | 2 | {{ page.label|safe }} 3 | 4 | -------------------------------------------------------------------------------- /endless_pagination/templates/endless/page_link.html: -------------------------------------------------------------------------------- 1 | {{ page.label|safe }} 4 | -------------------------------------------------------------------------------- /endless_pagination/templates/endless/show_more.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if querystring %} 3 | 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /endless_pagination/templates/endless/show_more_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if querystring %} 3 | 4 | 5 | 6 | {% if label %}{{ label }}{% else %}{% trans "more" %}{% endif %} 7 | 8 | 9 | 10 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /endless_pagination/templates/endless/show_pages.html: -------------------------------------------------------------------------------- 1 | {% for page in pages %} 2 | {{ page|default_if_none:'...' }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /endless_pagination/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/templatetags/__init__.py -------------------------------------------------------------------------------- /endless_pagination/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test model definitions.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.management import call_command 6 | from django.db import models 7 | 8 | from endless_pagination import utils 9 | 10 | 11 | def make_model_instances(number): 12 | """Make a ``number`` of test model instances and return a queryset.""" 13 | for _ in range(number): 14 | TestModel.objects.create() 15 | return TestModel.objects.all() 16 | 17 | 18 | class TestModel(models.Model, utils.UnicodeMixin): 19 | """A model used in tests.""" 20 | 21 | def __unicode__(self): 22 | return 'TestModel: {0}'.format(self.id) 23 | 24 | 25 | call_command('syncdb', verbosity=0) 26 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests base objects definitions.""" 2 | 3 | from __future__ import unicode_literals 4 | from contextlib import contextmanager 5 | import os 6 | 7 | from django.core.urlresolvers import reverse 8 | from django.http import QueryDict 9 | from django.test import LiveServerTestCase 10 | from django.utils import unittest 11 | from selenium.common import exceptions 12 | from selenium.webdriver import Firefox 13 | from selenium.webdriver.support import ui 14 | from xvfbwrapper.xvfbwrapper import Xvfb 15 | 16 | from endless_pagination.utils import PYTHON3 17 | 18 | 19 | SHOW_BROWSER = os.getenv('SHOW_BROWSER', False) 20 | SKIP_SELENIUM = os.getenv('SKIP_SELENIUM', False) 21 | # FIXME: do not exclude integration tests on Python3 once Selenium is updated 22 | # (bug #17). 23 | tests_are_run = not (PYTHON3 or SKIP_SELENIUM) 24 | 25 | 26 | def setup_package(): 27 | """Set up the Selenium driver once for all tests.""" 28 | # Just skipping *setup_package* and *teardown_package* generates an 29 | # uncaught exception under Python 2.6. 30 | if tests_are_run: 31 | if not SHOW_BROWSER: 32 | # Perform all graphical operations in memory. 33 | vdisplay = SeleniumTestCase.vdisplay = Xvfb(width=1280, height=720) 34 | vdisplay.start() 35 | # Create a Selenium browser instance. 36 | selenium = SeleniumTestCase.selenium = Firefox() 37 | SeleniumTestCase.wait = ui.WebDriverWait(selenium, 10) 38 | 39 | 40 | def teardown_package(): 41 | """Quit the Selenium driver.""" 42 | if tests_are_run: 43 | SeleniumTestCase.selenium.quit() 44 | if not SHOW_BROWSER: 45 | SeleniumTestCase.vdisplay.stop() 46 | 47 | 48 | # FIXME: do not exclude integration tests on Python3 once Selenium is updated 49 | # (bug #17). 50 | @unittest.skipIf( 51 | PYTHON3, 52 | 'excluding integration tests: Python 3 tests are still not supported.') 53 | @unittest.skipIf( 54 | SKIP_SELENIUM, 55 | 'excluding integration tests: environment variable SKIP_SELENIUM is set.') 56 | class SeleniumTestCase(LiveServerTestCase): 57 | """Base test class for integration tests.""" 58 | 59 | PREVIOUS = '<' 60 | NEXT = '>' 61 | MORE = 'More results' 62 | 63 | def setUp(self): 64 | self.url = self.live_server_url + reverse(self.view_name) 65 | 66 | def get(self, url=None, data=None, **kwargs): 67 | """Load a web page in the current browser session. 68 | 69 | If *url* is None, *self.url* is used. 70 | The querydict can be expressed providing *data* or *kwargs*. 71 | """ 72 | if url is None: 73 | url = self.url 74 | querydict = QueryDict('', mutable=True) 75 | if data is not None: 76 | querydict.update(data) 77 | querydict.update(kwargs) 78 | path = '{0}?{1}'.format(url, querydict.urlencode()) 79 | return self.selenium.get(path) 80 | 81 | def wait_ajax(self): 82 | """Wait for the document to be ready.""" 83 | def document_ready(driver): 84 | script = """ 85 | return ( 86 | document.readyState === 'complete' && 87 | jQuery.active === 0 88 | ); 89 | """ 90 | return driver.execute_script(script) 91 | self.wait.until(document_ready) 92 | return self.wait 93 | 94 | def click_link(self, text, index=0): 95 | """Click the link with the given *text* and *index*.""" 96 | link = self.selenium.find_elements_by_link_text(str(text))[index] 97 | link.click() 98 | return link 99 | 100 | def scroll_down(self): 101 | """Scroll down to the bottom of the page.""" 102 | script = 'window.scrollTo(0, document.body.scrollHeight);' 103 | self.selenium.execute_script(script) 104 | 105 | def get_current_elements(self, class_name, driver=None): 106 | """Return the range of current elements as a list of numbers.""" 107 | elements = [] 108 | selector = 'div.{0} > h4'.format(class_name) 109 | if driver is None: 110 | driver = self.selenium 111 | for element in driver.find_elements_by_css_selector(selector): 112 | elements.append(int(element.text.split()[1])) 113 | return elements 114 | 115 | def asserLinksEqual(self, count, text): 116 | """Assert the page contains *count* links with given *text*.""" 117 | links = self.selenium.find_elements_by_link_text(str(text)) 118 | self.assertEqual(count, len(links)) 119 | 120 | def assertElements(self, class_name, elements): 121 | """Assert the current page contains the given *elements*.""" 122 | current_elements = self.get_current_elements(class_name) 123 | self.assertSequenceEqual( 124 | elements, current_elements, ( 125 | 'Elements differ: {expected} != {actual}\n' 126 | 'Class name: {class_name}\n' 127 | 'Expected elements: {expected}\n' 128 | 'Actual elements: {actual}' 129 | ).format( 130 | actual=current_elements, 131 | expected=elements, 132 | class_name=class_name, 133 | ) 134 | ) 135 | 136 | @contextmanager 137 | def assertNewElements(self, class_name, new_elements): 138 | """Fail when new elements are not found in the page.""" 139 | def new_elements_loaded(driver): 140 | elements = self.get_current_elements(class_name, driver=driver) 141 | return elements == new_elements 142 | yield 143 | try: 144 | self.wait_ajax().until(new_elements_loaded) 145 | except exceptions.TimeoutException: 146 | self.assertElements(class_name, new_elements) 147 | 148 | @contextmanager 149 | def assertSameURL(self): 150 | """Assert the URL does not change after executing the yield block.""" 151 | current_url = self.selenium.current_url 152 | yield 153 | self.wait_ajax() 154 | self.assertEqual(current_url, self.selenium.current_url) 155 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_callbacks.py: -------------------------------------------------------------------------------- 1 | """Javascript callbacks integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class CallbacksTest(SeleniumTestCase): 9 | 10 | view_name = 'callbacks' 11 | 12 | def notifications_loaded(self, driver): 13 | return driver.find_elements_by_id('fragment') 14 | 15 | def assertNotificationsEqual(self, notifications): 16 | """Assert the given *notifications* equal the ones in the DOM.""" 17 | self.wait_ajax().until(self.notifications_loaded) 18 | find = self.selenium.find_element_by_id 19 | for key, value in notifications.items(): 20 | self.assertEqual(value, find(key).text) 21 | 22 | def test_on_click(self): 23 | # Ensure the onClick callback is correctly called. 24 | self.get() 25 | self.click_link(2) 26 | self.assertNotificationsEqual({ 27 | 'onclick': 'Object 1', 28 | 'onclick-label': '2', 29 | 'onclick-url': '/callbacks/?page=2', 30 | 'onclick-key': 'page', 31 | }) 32 | 33 | def test_on_completed(self): 34 | # Ensure the onCompleted callback is correctly called. 35 | self.get(page=10) 36 | self.click_link(1) 37 | self.assertNotificationsEqual({ 38 | 'oncompleted': 'Object 1', 39 | 'oncompleted-label': '1', 40 | 'oncompleted-url': '/callbacks/', 41 | 'oncompleted-key': 'page', 42 | 'fragment': 'Object 3', 43 | }) 44 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_chunks.py: -------------------------------------------------------------------------------- 1 | """On scroll chunks integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class ChunksPaginationTest(SeleniumTestCase): 9 | 10 | view_name = 'chunks' 11 | 12 | def test_new_elements_loaded(self): 13 | # Ensure new pages are loaded on scroll. 14 | self.get() 15 | with self.assertNewElements('object', range(1, 11)): 16 | with self.assertNewElements('item', range(1, 11)): 17 | self.scroll_down() 18 | 19 | def test_url_not_changed(self): 20 | # Ensure the request is done using Ajax (the page does not refresh). 21 | self.get() 22 | with self.assertSameURL(): 23 | self.scroll_down() 24 | 25 | def test_direct_link(self): 26 | # Ensure direct links work. 27 | self.get(data={'page': 2, 'items-page': 3}) 28 | current_url = self.selenium.current_url 29 | self.assertElements('object', range(6, 11)) 30 | self.assertElements('item', range(11, 16)) 31 | self.assertIn('page=2', current_url) 32 | self.assertIn('items-page=3', current_url) 33 | 34 | def test_subsequent_page(self): 35 | # Ensure next page is correctly loaded in a subsequent page, even if 36 | # normally it is the last page of the chunk. 37 | self.get(page=3) 38 | with self.assertNewElements('object', range(11, 21)): 39 | self.scroll_down() 40 | 41 | def test_chunks(self): 42 | # Ensure new items are not loaded on scroll if the chunk is complete. 43 | self.get() 44 | for i in range(5): 45 | self.scroll_down() 46 | self.wait_ajax() 47 | self.assertElements('object', range(1, 16)) 48 | self.assertElements('item', range(1, 21)) 49 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_digg.py: -------------------------------------------------------------------------------- 1 | """Digg-style pagination integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class DiggPaginationTest(SeleniumTestCase): 9 | 10 | view_name = 'digg' 11 | 12 | def test_new_elements_loaded(self): 13 | # Ensure a new page is loaded on click. 14 | self.get() 15 | with self.assertNewElements('object', range(6, 11)): 16 | self.click_link(2) 17 | 18 | def test_url_not_changed(self): 19 | # Ensure the request is done using Ajax (the page does not refresh). 20 | self.get() 21 | with self.assertSameURL(): 22 | self.click_link(2) 23 | 24 | def test_direct_link(self): 25 | # Ensure direct links work. 26 | self.get(page=4) 27 | self.assertElements('object', range(16, 21)) 28 | self.assertIn('page=4', self.selenium.current_url) 29 | 30 | def test_next(self): 31 | # Ensure next page is correctly loaded. 32 | self.get() 33 | with self.assertSameURL(): 34 | with self.assertNewElements('object', range(6, 11)): 35 | self.click_link(self.NEXT) 36 | 37 | def test_previous(self): 38 | # Ensure previous page is correctly loaded. 39 | self.get(page=4) 40 | with self.assertSameURL(): 41 | with self.assertNewElements('object', range(11, 16)): 42 | self.click_link(self.PREVIOUS) 43 | 44 | def test_no_previous_link_in_first_page(self): 45 | # Ensure there is no previous link on the first page. 46 | self.get() 47 | self.asserLinksEqual(0, self.PREVIOUS) 48 | 49 | def test_no_next_link_in_last_page(self): 50 | # Ensure there is no forward link on the last page. 51 | self.get(page=10) 52 | self.asserLinksEqual(0, self.NEXT) 53 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_multiple.py: -------------------------------------------------------------------------------- 1 | """Multiple pagination integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class MultiplePaginationTest(SeleniumTestCase): 9 | 10 | view_name = 'multiple' 11 | 12 | def test_new_elements_loaded(self): 13 | # Ensure a new page is loaded on click for each paginated elements. 14 | self.get() 15 | with self.assertNewElements('object', range(4, 7)): 16 | with self.assertNewElements('item', range(7, 10)): 17 | with self.assertNewElements('entry', range(1, 5)): 18 | self.click_link(2, 0) 19 | self.click_link(3, 1) 20 | self.click_link(self.MORE) 21 | 22 | def test_url_not_changed(self): 23 | # Ensure the requests are done using Ajax (the page does not refresh). 24 | self.get() 25 | with self.assertSameURL(): 26 | self.click_link(2, 0) 27 | self.click_link(3, 1) 28 | self.click_link(self.MORE) 29 | 30 | def test_direct_link(self): 31 | # Ensure direct links work. 32 | self.get(data={'objects-page': 3, 'items-page': 4, 'entries-page': 5}) 33 | self.assertElements('object', range(7, 10)) 34 | self.assertElements('item', range(10, 13)) 35 | self.assertElements('entry', range(11, 14)) 36 | self.assertIn('objects-page=3', self.selenium.current_url) 37 | self.assertIn('items-page=4', self.selenium.current_url) 38 | self.assertIn('entries-page=5', self.selenium.current_url) 39 | 40 | def test_subsequent_pages(self): 41 | # Ensure elements are correctly loaded starting from a subsequent page. 42 | self.get(data={'objects-page': 2, 'items-page': 2, 'entries-page': 2}) 43 | with self.assertNewElements('object', range(1, 4)): 44 | with self.assertNewElements('item', range(7, 10)): 45 | with self.assertNewElements('entry', range(2, 8)): 46 | self.click_link(self.PREVIOUS, 0) 47 | self.click_link(self.NEXT, 1) 48 | self.click_link(self.MORE) 49 | 50 | def test_no_more_link_in_last_page(self): 51 | # Ensure there is no more or forward links on the last pages. 52 | self.get(data={'objects-page': 7, 'items-page': 7, 'entries-page': 8}) 53 | self.asserLinksEqual(0, self.NEXT) 54 | self.asserLinksEqual(0, self.MORE) 55 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_onscroll.py: -------------------------------------------------------------------------------- 1 | """On scroll pagination integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class OnScrollPaginationTest(SeleniumTestCase): 9 | 10 | view_name = 'onscroll' 11 | 12 | def test_new_elements_loaded(self): 13 | # Ensure a new page is loaded on scroll. 14 | self.get() 15 | with self.assertNewElements('object', range(1, 21)): 16 | self.scroll_down() 17 | 18 | def test_url_not_changed(self): 19 | # Ensure the request is done using Ajax (the page does not refresh). 20 | self.get() 21 | with self.assertSameURL(): 22 | self.scroll_down() 23 | 24 | def test_direct_link(self): 25 | # Ensure direct links work. 26 | self.get(page=3) 27 | self.assertElements('object', range(21, 31)) 28 | self.assertIn('page=3', self.selenium.current_url) 29 | 30 | def test_subsequent_page(self): 31 | # Ensure next page is correctly loaded in a subsequent page. 32 | self.get(page=2) 33 | with self.assertNewElements('object', range(11, 31)): 34 | self.scroll_down() 35 | 36 | def test_multiple_show_more(self): 37 | # Ensure new pages are loaded again and again. 38 | self.get() 39 | for page in range(2, 5): 40 | expected_range = range(1, 10 * page + 1) 41 | with self.assertSameURL(): 42 | with self.assertNewElements('object', expected_range): 43 | self.scroll_down() 44 | 45 | def test_scrolling_last_page(self): 46 | # Ensure scrolling on the last page is a no-op. 47 | self.get(page=5) 48 | with self.assertNewElements('object', range(41, 51)): 49 | self.scroll_down() 50 | -------------------------------------------------------------------------------- /endless_pagination/tests/integration/test_twitter.py: -------------------------------------------------------------------------------- 1 | """Twitter-style pagination integration tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from endless_pagination.tests.integration import SeleniumTestCase 6 | 7 | 8 | class TwitterPaginationTest(SeleniumTestCase): 9 | 10 | view_name = 'twitter' 11 | 12 | def test_new_elements_loaded(self): 13 | # Ensure a new page is loaded on click. 14 | self.get() 15 | with self.assertNewElements('object', range(1, 11)): 16 | self.click_link(self.MORE) 17 | 18 | def test_url_not_changed(self): 19 | # Ensure the request is done using Ajax (the page does not refresh). 20 | self.get() 21 | with self.assertSameURL(): 22 | self.click_link(self.MORE) 23 | 24 | def test_direct_link(self): 25 | # Ensure direct links work. 26 | self.get(page=4) 27 | self.assertElements('object', range(16, 21)) 28 | self.assertIn('page=4', self.selenium.current_url) 29 | 30 | def test_subsequent_page(self): 31 | # Ensure next page is correctly loaded in a subsequent page. 32 | self.get(page=2) 33 | with self.assertNewElements('object', range(6, 16)): 34 | self.click_link(self.MORE) 35 | 36 | def test_multiple_show_more(self): 37 | # Ensure new pages are loaded again and again. 38 | self.get() 39 | for page in range(2, 5): 40 | expected_range = range(1, 5 * page + 1) 41 | with self.assertSameURL(): 42 | with self.assertNewElements('object', expected_range): 43 | self.click_link(self.MORE) 44 | 45 | def test_no_more_link_in_last_page(self): 46 | # Ensure there is no more link on the last page. 47 | self.get(page=10) 48 | self.asserLinksEqual(0, self.MORE) 49 | -------------------------------------------------------------------------------- /endless_pagination/tests/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/endless_pagination/tests/templatetags/__init__.py -------------------------------------------------------------------------------- /endless_pagination/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | """Decorator tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | 8 | from endless_pagination import decorators 9 | 10 | 11 | class DecoratorsTestMixin(object): 12 | """Base test mixin for decorators. 13 | 14 | Subclasses (actual test cases) must implement the ``get_decorator`` method 15 | and the ``arg`` attribute to be used as argument for the decorator. 16 | """ 17 | 18 | def setUp(self): 19 | self.factory = RequestFactory() 20 | self.ajax_headers = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} 21 | self.default = 'default.html' 22 | self.page = 'page.html' 23 | self.page_url = '/?page=2&mypage=10&querystring_key=page' 24 | self.mypage = 'mypage.html' 25 | self.mypage_url = '/?page=2&mypage=10&querystring_key=mypage' 26 | 27 | def get_decorator(self): 28 | """Return the decorator that must be exercised.""" 29 | raise NotImplementedError 30 | 31 | def assertTemplatesEqual(self, expected_active, expected_page, templates): 32 | """Assert active template and page template are the ones given.""" 33 | self.assertSequenceEqual([expected_active, expected_page], templates) 34 | 35 | def decorate(self, *args, **kwargs): 36 | """Return a view decorated with ``self.decorator(*args, **kwargs)``.""" 37 | 38 | def view(request, extra_context=None, template=self.default): 39 | """Test view that will be decorated in tests.""" 40 | context = {} 41 | if extra_context is not None: 42 | context.update(extra_context) 43 | return template, context['page_template'] 44 | 45 | decorator = self.get_decorator() 46 | return decorator(*args, **kwargs)(view) 47 | 48 | def test_decorated(self): 49 | # Ensure the view is correctly decorated. 50 | view = self.decorate(self.arg) 51 | templates = view(self.factory.get('/')) 52 | self.assertTemplatesEqual(self.default, self.page, templates) 53 | 54 | def test_request_with_querystring_key(self): 55 | # If the querystring key refers to the handled template, 56 | # the view still uses the default tempate if the request is not Ajax. 57 | view = self.decorate(self.arg) 58 | templates = view(self.factory.get(self.page_url)) 59 | self.assertTemplatesEqual(self.default, self.page, templates) 60 | 61 | def test_ajax_request(self): 62 | # Ensure the view serves the template fragment if the request is Ajax. 63 | view = self.decorate(self.arg) 64 | templates = view(self.factory.get('/', **self.ajax_headers)) 65 | self.assertTemplatesEqual(self.page, self.page, templates) 66 | 67 | def test_ajax_request_with_querystring_key(self): 68 | # If the querystring key refers to the handled template, 69 | # the view switches the template if the request is Ajax. 70 | view = self.decorate(self.arg) 71 | templates = view(self.factory.get(self.page_url, **self.ajax_headers)) 72 | self.assertTemplatesEqual(self.page, self.page, templates) 73 | 74 | def test_unexistent_page(self): 75 | # Ensure the default page and is returned if the querystring points 76 | # to a page that is not defined. 77 | view = self.decorate(self.arg) 78 | templates = view(self.factory.get('/?querystring_key=does-not-exist')) 79 | self.assertTemplatesEqual(self.default, self.page, templates) 80 | 81 | 82 | class PageTemplateTest(DecoratorsTestMixin, TestCase): 83 | 84 | arg = 'page.html' 85 | 86 | def get_decorator(self): 87 | return decorators.page_template 88 | 89 | def test_request_with_querystring_key_to_mypage(self): 90 | # If the querystring key refers to another template, 91 | # the view still uses the default tempate if the request is not Ajax. 92 | view = self.decorate(self.arg) 93 | templates = view(self.factory.get(self.mypage_url)) 94 | self.assertTemplatesEqual(self.default, self.page, templates) 95 | 96 | def test_ajax_request_with_querystring_key_to_mypage(self): 97 | # If the querystring key refers to another template, 98 | # the view still uses the default tempate even if the request is Ajax. 99 | view = self.decorate(self.arg) 100 | templates = view( 101 | self.factory.get(self.mypage_url, **self.ajax_headers)) 102 | self.assertTemplatesEqual(self.default, self.page, templates) 103 | 104 | def test_ajax_request_to_mypage(self): 105 | # Ensure the view serves the template fragment if the request is Ajax 106 | # and another template fragment is requested. 107 | view = self.decorate(self.mypage, key='mypage') 108 | templates = view( 109 | self.factory.get(self.mypage_url, **self.ajax_headers)) 110 | self.assertTemplatesEqual(self.mypage, self.mypage, templates) 111 | 112 | 113 | class PageTemplatesTest(DecoratorsTestMixin, TestCase): 114 | 115 | arg = {'page.html': None, 'mypage.html': 'mypage'} 116 | 117 | def get_decorator(self): 118 | return decorators.page_templates 119 | 120 | def test_request_with_querystring_key_to_mypage(self): 121 | # If the querystring key refers to another template, 122 | # the view still uses the default tempate if the request is not Ajax. 123 | view = self.decorate(self.arg) 124 | templates = view(self.factory.get(self.mypage_url)) 125 | self.assertTemplatesEqual(self.default, self.mypage, templates) 126 | 127 | def test_ajax_request_with_querystring_key_to_mypage(self): 128 | # If the querystring key refers to another template, 129 | # the view switches to the givent template if the request is Ajax. 130 | view = self.decorate(self.arg) 131 | templates = view( 132 | self.factory.get(self.mypage_url, **self.ajax_headers)) 133 | self.assertTemplatesEqual(self.mypage, self.mypage, templates) 134 | 135 | 136 | class PageTemplatesWithTupleTest(PageTemplatesTest): 137 | 138 | arg = (('page.html', None), ('mypage.html', 'mypage')) 139 | -------------------------------------------------------------------------------- /endless_pagination/tests/test_loaders.py: -------------------------------------------------------------------------------- 1 | """Loader tests.""" 2 | 3 | from __future__ import unicode_literals 4 | from contextlib import contextmanager 5 | 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.test import TestCase 8 | 9 | from endless_pagination import loaders 10 | 11 | 12 | test_object = 'test object' 13 | 14 | 15 | class ImproperlyConfiguredTestMixin(object): 16 | """Include an ImproperlyConfigured assertion.""" 17 | 18 | @contextmanager 19 | def assertImproperlyConfigured(self, message): 20 | """Assert the code in the context manager block raises an error. 21 | 22 | The error must be ImproperlyConfigured, and the error message must 23 | include the given *message*. 24 | """ 25 | try: 26 | yield 27 | except ImproperlyConfigured as err: 28 | self.assertIn(message, str(err).lower()) 29 | else: 30 | self.fail('ImproperlyConfigured not raised') 31 | 32 | 33 | class AssertImproperlyConfiguredTest(ImproperlyConfiguredTestMixin, TestCase): 34 | 35 | def test_assertion(self): 36 | # Ensure the assertion does not fail if ImproperlyConfigured is raised 37 | # with the given error message. 38 | with self.assertImproperlyConfigured('error'): 39 | raise ImproperlyConfigured('Example error text') 40 | 41 | def test_case_insensitive(self): 42 | # Ensure the error message test is case insensitive. 43 | with self.assertImproperlyConfigured('error'): 44 | raise ImproperlyConfigured('Example ERROR text') 45 | 46 | def test_assertion_fails_different_message(self): 47 | # Ensure the assertion fails if ImproperlyConfigured is raised with 48 | # a different message. 49 | with self.assertRaises(AssertionError): 50 | with self.assertImproperlyConfigured('failure'): 51 | raise ImproperlyConfigured('Example error text') 52 | 53 | def test_assertion_fails_no_exception(self): 54 | # Ensure the assertion fails if ImproperlyConfigured is not raised. 55 | with self.assertRaises(AssertionError) as cm: 56 | with self.assertImproperlyConfigured('error'): 57 | pass 58 | self.assertEqual('ImproperlyConfigured not raised', str(cm.exception)) 59 | 60 | def test_assertion_fails_different_exception(self): 61 | # Ensure other exceptions are not swallowed. 62 | with self.assertRaises(TypeError): 63 | with self.assertImproperlyConfigured('error'): 64 | raise TypeError 65 | 66 | 67 | class LoadObjectTest(ImproperlyConfiguredTestMixin, TestCase): 68 | 69 | def setUp(self): 70 | self.module = self.__class__.__module__ 71 | 72 | def test_valid_path(self): 73 | # Ensure the object is correctly loaded if the provided path is valid. 74 | path = '.'.join((self.module, 'test_object')) 75 | self.assertIs(test_object, loaders.load_object(path)) 76 | 77 | def test_module_not_found(self): 78 | # An error is raised if the module cannot be found. 79 | with self.assertImproperlyConfigured('not found'): 80 | loaders.load_object('__invalid__.module') 81 | 82 | def test_invalid_module(self): 83 | # An error is raised if the provided path is not a valid dotted string. 84 | with self.assertImproperlyConfigured('invalid'): 85 | loaders.load_object('') 86 | 87 | def test_object_not_found(self): 88 | # An error is raised if the object cannot be found in the module. 89 | path = '.'.join((self.module, '__does_not_exist__')) 90 | with self.assertImproperlyConfigured('object'): 91 | loaders.load_object(path) 92 | -------------------------------------------------------------------------------- /endless_pagination/tests/test_paginators.py: -------------------------------------------------------------------------------- 1 | """Paginator tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.test import TestCase 6 | 7 | from endless_pagination import paginators 8 | 9 | 10 | class PaginatorTestMixin(object): 11 | """Base test mixin for paginators. 12 | 13 | Subclasses (actual test cases) must define the ``paginator_class`` name. 14 | """ 15 | 16 | def setUp(self): 17 | self.items = list(range(30)) 18 | self.per_page = 7 19 | self.paginator = self.paginator_class( 20 | self.items, self.per_page, orphans=2) 21 | 22 | def test_object_list(self): 23 | # Ensure the paginator correctly returns objects for each page. 24 | first_page = self.paginator.first_page 25 | expected = self.items[first_page:first_page + self.per_page] 26 | object_list = self.paginator.page(2).object_list 27 | self.assertSequenceEqual(expected, object_list) 28 | 29 | def test_orphans(self): 30 | # Ensure orphans are included in the last page. 31 | object_list = self.paginator.page(4).object_list 32 | self.assertSequenceEqual(self.items[-9:], object_list) 33 | 34 | def test_no_orphans(self): 35 | # Ensure exceeding orphans generate a new page. 36 | paginator = self.paginator_class(range(11), 8, orphans=2) 37 | object_list = paginator.page(2).object_list 38 | self.assertEqual(3, len(object_list)) 39 | 40 | def test_empty_page(self): 41 | # En error if raised if the requested page does not exist. 42 | with self.assertRaises(paginators.EmptyPage): 43 | self.paginator.page(5) 44 | 45 | def test_invalid_page(self): 46 | # En error is raised if the requested page is not valid. 47 | with self.assertRaises(paginators.PageNotAnInteger): 48 | self.paginator.page('__not_valid__') 49 | with self.assertRaises(paginators.EmptyPage): 50 | self.paginator.page(0) 51 | 52 | 53 | class DifferentFirstPagePaginatorTestMixin(PaginatorTestMixin): 54 | """Base test mixin for paginators. 55 | 56 | This time the paginator defined in ``setUp`` has different number of 57 | items on the first page. 58 | Subclasses (actual test cases) must define the ``paginator_class`` name. 59 | """ 60 | 61 | def setUp(self): 62 | self.items = list(range(26)) 63 | self.per_page = 7 64 | self.paginator = self.paginator_class( 65 | self.items, self.per_page, first_page=3, orphans=2) 66 | 67 | def test_no_orphans(self): 68 | # Ensure exceeding orphans generate a new page. 69 | paginator = self.paginator_class(range(11), 5, first_page=3, orphans=2) 70 | object_list = paginator.page(3).object_list 71 | self.assertEqual(3, len(object_list)) 72 | 73 | 74 | class DefaultPaginatorTest(PaginatorTestMixin, TestCase): 75 | 76 | paginator_class = paginators.DefaultPaginator 77 | 78 | def test_indexes(self): 79 | # Ensure start and end indexes are correct. 80 | page = self.paginator.page(2) 81 | self.assertEqual(self.per_page + 1, page.start_index()) 82 | self.assertEqual(self.per_page * 2, page.end_index()) 83 | 84 | def test_items_count(self): 85 | # Ensure the paginator reflects the number of items. 86 | self.assertEqual(len(self.items), self.paginator.count) 87 | 88 | def test_num_pages(self): 89 | # Ensure the number of pages is correctly calculated. 90 | self.assertEqual(4, self.paginator.num_pages) 91 | 92 | def test_page_range(self): 93 | # Ensure the page range is correctly calculated. 94 | self.assertSequenceEqual([1, 2, 3, 4], self.paginator.page_range) 95 | 96 | def test_no_items(self): 97 | # Ensure the right values are returned if the page contains no items. 98 | paginator = self.paginator_class([], 10) 99 | page = paginator.page(1) 100 | self.assertEqual(0, paginator.count) 101 | self.assertEqual(0, page.start_index()) 102 | 103 | def test_single_page_indexes(self): 104 | # Ensure the returned indexes are correct for a single page pagination. 105 | paginator = self.paginator_class(range(6), 5, orphans=2) 106 | page = paginator.page(1) 107 | self.assertEqual(1, page.start_index()) 108 | self.assertEqual(6, page.end_index()) 109 | 110 | 111 | class LazyPaginatorTest(PaginatorTestMixin, TestCase): 112 | 113 | paginator_class = paginators.LazyPaginator 114 | 115 | def test_items_count(self): 116 | # The lazy paginator does not implement items count. 117 | with self.assertRaises(NotImplementedError): 118 | self.paginator.count 119 | 120 | def test_num_pages(self): 121 | # The number of pages depends on the current page. 122 | self.paginator.page(2) 123 | self.assertEqual(3, self.paginator.num_pages) 124 | 125 | def test_page_range(self): 126 | # The lazy paginator does not implement page range. 127 | with self.assertRaises(NotImplementedError): 128 | self.paginator.page_range 129 | 130 | 131 | class DifferentFirstPageDefaultPaginatorTest( 132 | DifferentFirstPagePaginatorTestMixin, TestCase): 133 | 134 | paginator_class = paginators.DefaultPaginator 135 | 136 | 137 | class DifferentFirstPageLazyPaginatorTest( 138 | DifferentFirstPagePaginatorTestMixin, TestCase): 139 | 140 | paginator_class = paginators.LazyPaginator 141 | -------------------------------------------------------------------------------- /endless_pagination/tests/test_views.py: -------------------------------------------------------------------------------- 1 | """View tests.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.http import Http404 7 | from django.test import TestCase 8 | from django.test.client import RequestFactory 9 | 10 | from endless_pagination import views 11 | from endless_pagination.tests import ( 12 | make_model_instances, 13 | TestModel, 14 | ) 15 | 16 | 17 | class CustomizedListView(views.AjaxListView): 18 | """An AjaxListView subclass overriding the *get* method.""" 19 | 20 | def get(self, request, *args, **kwargs): 21 | self.object_list = self.queryset 22 | context = self.get_context_data(object_list=self.object_list) 23 | return self.render_to_response(context) 24 | 25 | 26 | class AjaxListViewTest(TestCase): 27 | 28 | model_page_template = 'endless_pagination/testmodel_list_page.html' 29 | model_template_name = 'endless_pagination/testmodel_list.html' 30 | page_template = 'page_template.html' 31 | template_name = 'template.html' 32 | url = '/?page=2' 33 | 34 | def setUp(self): 35 | factory = RequestFactory() 36 | self.request = factory.get(self.url) 37 | self.ajax_request = factory.get( 38 | self.url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 39 | 40 | def check_response(self, response, template_name, object_list): 41 | """Execute several assertions on the response. 42 | 43 | Check that the response has a successful status code, 44 | uses ``template_name`` and contains ``object_list``. 45 | """ 46 | self.assertEqual(200, response.status_code) 47 | self.assertSequenceEqual([template_name], response.template_name) 48 | self.assertSequenceEqual( 49 | list(object_list), response.context_data['object_list']) 50 | 51 | def make_view(self, *args, **kwargs): 52 | """Return an instance of AjaxListView.""" 53 | return views.AjaxListView.as_view(*args, **kwargs) 54 | 55 | def test_list(self): 56 | # Ensure the view correctly adds the list to context. 57 | view = self.make_view( 58 | queryset=range(30), 59 | template_name=self.template_name, 60 | page_template=self.page_template, 61 | ) 62 | response = view(self.request) 63 | self.check_response(response, self.template_name, range(30)) 64 | 65 | def test_list_ajax(self): 66 | # Ensure the list view switches templates when the request is Ajax. 67 | view = self.make_view( 68 | queryset=range(30), 69 | template_name=self.template_name, 70 | page_template=self.page_template, 71 | ) 72 | response = view(self.ajax_request) 73 | self.check_response(response, self.page_template, range(30)) 74 | 75 | def test_queryset(self): 76 | # Ensure the view correctly adds the queryset to context. 77 | queryset = make_model_instances(30) 78 | view = self.make_view(queryset=queryset) 79 | response = view(self.request) 80 | self.check_response(response, self.model_template_name, queryset) 81 | 82 | def test_queryset_ajax(self): 83 | # Ensure the queryset view switches templates when the request is Ajax. 84 | queryset = make_model_instances(30) 85 | view = self.make_view(queryset=queryset) 86 | response = view(self.ajax_request) 87 | self.check_response(response, self.model_page_template, queryset) 88 | 89 | def test_model(self): 90 | # Ensure the view correctly uses the model to generate the template. 91 | queryset = make_model_instances(30) 92 | view = self.make_view(model=TestModel) 93 | response = view(self.request) 94 | self.check_response(response, self.model_template_name, queryset) 95 | 96 | def test_model_ajax(self): 97 | # Ensure the model view switches templates when the request is Ajax. 98 | queryset = make_model_instances(30) 99 | view = self.make_view(model=TestModel) 100 | response = view(self.ajax_request) 101 | self.check_response(response, self.model_page_template, queryset) 102 | 103 | def test_missing_queryset_or_model(self): 104 | # An error is raised if both queryset and model are not provided. 105 | view = self.make_view() 106 | with self.assertRaises(ImproperlyConfigured) as cm: 107 | view(self.request) 108 | self.assertIn('queryset', str(cm.exception)) 109 | self.assertIn('model', str(cm.exception)) 110 | 111 | def test_missing_page_template(self): 112 | # An error is raised if the ``page_template`` name is not provided. 113 | view = self.make_view(queryset=range(30)) 114 | with self.assertRaises(ImproperlyConfigured) as cm: 115 | view(self.request) 116 | self.assertIn('page_template', str(cm.exception)) 117 | 118 | def test_do_not_allow_empty(self): 119 | # An error is raised if the list is empty and ``allow_empty`` is 120 | # set to False. 121 | view = self.make_view(model=TestModel, allow_empty=False) 122 | with self.assertRaises(Http404) as cm: 123 | view(self.request) 124 | self.assertIn('allow_empty', str(cm.exception)) 125 | 126 | def test_view_in_context(self): 127 | # Ensure the view is included in the template context. 128 | view = self.make_view( 129 | queryset=range(30), 130 | page_template=self.page_template, 131 | ) 132 | response = view(self.request) 133 | view_instance = response.context_data['view'] 134 | self.assertIsInstance(view_instance, views.AjaxListView) 135 | 136 | def test_customized_view(self): 137 | # Ensure the customized view correctly adds the queryset to context. 138 | queryset = make_model_instances(30) 139 | view = CustomizedListView.as_view(queryset=queryset) 140 | response = view(self.request) 141 | self.check_response(response, self.model_template_name, queryset) 142 | -------------------------------------------------------------------------------- /endless_pagination/utils.py: -------------------------------------------------------------------------------- 1 | """Django Endless Pagination utility functions.""" 2 | 3 | from __future__ import unicode_literals 4 | import sys 5 | 6 | from endless_pagination import exceptions 7 | from endless_pagination.settings import ( 8 | DEFAULT_CALLABLE_AROUNDS, 9 | DEFAULT_CALLABLE_ARROWS, 10 | DEFAULT_CALLABLE_EXTREMES, 11 | PAGE_LABEL, 12 | ) 13 | 14 | 15 | # Handle the Python 2 to 3 migration. 16 | if sys.version_info[0] >= 3: 17 | PYTHON3 = True 18 | text = str 19 | else: 20 | PYTHON3 = False 21 | # Avoid lint errors under Python 3. 22 | text = unicode # NOQA 23 | 24 | 25 | def get_data_from_context(context): 26 | """Get the django paginator data object from the given *context*. 27 | 28 | The context is a dict-like object. If the context key ``endless`` 29 | is not found, a *PaginationError* is raised. 30 | """ 31 | try: 32 | return context['endless'] 33 | except KeyError: 34 | raise exceptions.PaginationError( 35 | 'Cannot find endless data in context.') 36 | 37 | 38 | def get_page_number_from_request( 39 | request, querystring_key=PAGE_LABEL, default=1): 40 | """Retrieve the current page number from *GET* or *POST* data. 41 | 42 | If the page does not exists in *request*, or is not a number, 43 | then *default* number is returned. 44 | """ 45 | try: 46 | if request.method == 'POST': 47 | page_number = request.POST[querystring_key] 48 | else: 49 | page_number = request.GET[querystring_key] 50 | 51 | return int(page_number) 52 | except (KeyError, TypeError, ValueError): 53 | return default 54 | 55 | 56 | def get_page_numbers( 57 | current_page, num_pages, extremes=DEFAULT_CALLABLE_EXTREMES, 58 | arounds=DEFAULT_CALLABLE_AROUNDS, arrows=DEFAULT_CALLABLE_ARROWS): 59 | """Default callable for page listing. 60 | 61 | Produce a Digg-style pagination. 62 | """ 63 | page_range = range(1, num_pages + 1) 64 | pages = [] 65 | if current_page != 1: 66 | if arrows: 67 | pages.append('first') 68 | pages.append('previous') 69 | 70 | # Get first and last pages (extremes). 71 | first = page_range[:extremes] 72 | pages.extend(first) 73 | last = page_range[-extremes:] 74 | 75 | # Get the current pages (arounds). 76 | current_start = current_page - 1 - arounds 77 | if current_start < 0: 78 | current_start = 0 79 | current_end = current_page + arounds 80 | if current_end > num_pages: 81 | current_end = num_pages 82 | current = page_range[current_start:current_end] 83 | 84 | # Mix first with current pages. 85 | to_add = current 86 | if extremes: 87 | diff = current[0] - first[-1] 88 | if diff > 1: 89 | pages.append(None) 90 | elif diff < 1: 91 | to_add = current[abs(diff) + 1:] 92 | pages.extend(to_add) 93 | 94 | # Mix current with last pages. 95 | if extremes: 96 | diff = last[0] - current[-1] 97 | to_add = last 98 | if diff > 1: 99 | pages.append(None) 100 | elif diff < 1: 101 | to_add = last[abs(diff) + 1:] 102 | pages.extend(to_add) 103 | 104 | if current_page != num_pages: 105 | pages.append('next') 106 | if arrows: 107 | pages.append('last') 108 | return pages 109 | 110 | 111 | def _iter_factors(starting_factor=1): 112 | """Generator yielding something like 1, 3, 10, 30, 100, 300 etc. 113 | 114 | The series starts from starting_factor. 115 | """ 116 | while True: 117 | yield starting_factor 118 | yield starting_factor * 3 119 | starting_factor *= 10 120 | 121 | 122 | def _make_elastic_range(begin, end): 123 | """Generate an S-curved range of pages. 124 | 125 | Start from both left and right, adding exponentially growing indexes, 126 | until the two trends collide. 127 | """ 128 | # Limit growth for huge numbers of pages. 129 | starting_factor = max(1, (end - begin) // 100) 130 | factor = _iter_factors(starting_factor) 131 | left_half, right_half = [], [] 132 | left_val, right_val = begin, end 133 | right_val = end 134 | while left_val < right_val: 135 | left_half.append(left_val) 136 | right_half.append(right_val) 137 | next_factor = next(factor) 138 | left_val = begin + next_factor 139 | right_val = end - next_factor 140 | # If the trends happen to meet exactly at one point, retain it. 141 | if left_val == right_val: 142 | left_half.append(left_val) 143 | right_half.reverse() 144 | return left_half + right_half 145 | 146 | 147 | def get_elastic_page_numbers(current_page, num_pages): 148 | """Alternative callable for page listing. 149 | 150 | Produce an adaptive pagination, useful for big numbers of pages, by 151 | splitting the num_pages ranges in two parts at current_page. Each part 152 | will have its own S-curve. 153 | """ 154 | if num_pages <= 10: 155 | return list(range(1, num_pages + 1)) 156 | if current_page == 1: 157 | pages = [1] 158 | else: 159 | pages = ['first', 'previous'] 160 | pages.extend(_make_elastic_range(1, current_page)) 161 | if current_page != num_pages: 162 | pages.extend(_make_elastic_range(current_page, num_pages)[1:]) 163 | pages.extend(['next', 'last']) 164 | return pages 165 | 166 | 167 | def get_querystring_for_page( 168 | request, page_number, querystring_key, default_number=1): 169 | """Return a querystring pointing to *page_number*.""" 170 | querydict = request.GET.copy() 171 | querydict[querystring_key] = page_number 172 | # For the default page number (usually 1) the querystring is not required. 173 | if page_number == default_number: 174 | del querydict[querystring_key] 175 | if 'querystring_key' in querydict: 176 | del querydict['querystring_key'] 177 | if querydict: 178 | return '?' + querydict.urlencode() 179 | return '' 180 | 181 | 182 | def normalize_page_number(page_number, page_range): 183 | """Handle a negative *page_number*. 184 | 185 | Return a positive page number contained in *page_range*. 186 | If the negative index is out of range, return the page number 1. 187 | """ 188 | try: 189 | return page_range[page_number] 190 | except IndexError: 191 | return page_range[0] 192 | 193 | 194 | class UnicodeMixin(object): 195 | """Mixin class to handle defining the proper unicode and string methods.""" 196 | 197 | if PYTHON3: 198 | def __str__(self): 199 | return self.__unicode__() 200 | -------------------------------------------------------------------------------- /endless_pagination/views.py: -------------------------------------------------------------------------------- 1 | """Django Endless Pagination class-based views.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.http import Http404 7 | from django.utils.encoding import smart_str 8 | from django.utils.translation import ugettext as _ 9 | from django.views.generic.base import View 10 | from django.views.generic.list import MultipleObjectTemplateResponseMixin 11 | 12 | from endless_pagination.settings import PAGE_LABEL 13 | 14 | 15 | class MultipleObjectMixin(object): 16 | 17 | allow_empty = True 18 | context_object_name = None 19 | model = None 20 | queryset = None 21 | 22 | def get_queryset(self): 23 | """Get the list of items for this view. 24 | 25 | This must be an interable, and may be a queryset 26 | (in which qs-specific behavior will be enabled). 27 | 28 | See original in ``django.views.generic.list.MultipleObjectMixin``. 29 | """ 30 | if self.queryset is not None: 31 | queryset = self.queryset 32 | if hasattr(queryset, '_clone'): 33 | queryset = queryset._clone() 34 | elif self.model is not None: 35 | queryset = self.model._default_manager.all() 36 | else: 37 | msg = '{0} must define ``queryset`` or ``model``' 38 | raise ImproperlyConfigured(msg.format(self.__class__.__name__)) 39 | return queryset 40 | 41 | def get_allow_empty(self): 42 | """Returns True if the view should display empty lists. 43 | 44 | Return False if a 404 should be raised instead. 45 | 46 | See original in ``django.views.generic.list.MultipleObjectMixin``. 47 | """ 48 | return self.allow_empty 49 | 50 | def get_context_object_name(self, object_list): 51 | """Get the name of the item to be used in the context. 52 | 53 | See original in ``django.views.generic.list.MultipleObjectMixin``. 54 | """ 55 | if self.context_object_name: 56 | return self.context_object_name 57 | elif hasattr(object_list, 'model'): 58 | object_name = object_list.model._meta.object_name.lower() 59 | return smart_str('{0}_list'.format(object_name)) 60 | else: 61 | return None 62 | 63 | def get_context_data(self, **kwargs): 64 | """Get the context for this view. 65 | 66 | Also adds the *page_template* variable in the context. 67 | 68 | If the *page_template* is not given as a kwarg of the *as_view* 69 | method then it is generated using app label, model name 70 | (obviously if the list is a queryset), *self.template_name_suffix* 71 | and *self.page_template_suffix*. 72 | 73 | For instance, if the list is a queryset of *blog.Entry*, 74 | the template will be ``blog/entry_list_page.html``. 75 | """ 76 | queryset = kwargs.pop('object_list') 77 | page_template = kwargs.pop('page_template', None) 78 | 79 | context_object_name = self.get_context_object_name(queryset) 80 | context = {'object_list': queryset, 'view': self} 81 | context.update(kwargs) 82 | if context_object_name is not None: 83 | context[context_object_name] = queryset 84 | 85 | if page_template is None: 86 | if hasattr(queryset, 'model'): 87 | page_template = self.get_page_template(**kwargs) 88 | else: 89 | raise ImproperlyConfigured( 90 | 'AjaxListView requires a page_template') 91 | context['page_template'] = self.page_template = page_template 92 | 93 | return context 94 | 95 | 96 | class BaseListView(MultipleObjectMixin, View): 97 | 98 | def get(self, request, *args, **kwargs): 99 | self.object_list = self.get_queryset() 100 | allow_empty = self.get_allow_empty() 101 | if not allow_empty and len(self.object_list) == 0: 102 | msg = _('Empty list and ``%(class_name)s.allow_empty`` is False.') 103 | raise Http404(msg % {'class_name': self.__class__.__name__}) 104 | context = self.get_context_data( 105 | object_list=self.object_list, page_template=self.page_template) 106 | return self.render_to_response(context) 107 | 108 | 109 | class AjaxMultipleObjectTemplateResponseMixin( 110 | MultipleObjectTemplateResponseMixin): 111 | 112 | key = PAGE_LABEL 113 | page_template = None 114 | page_template_suffix = '_page' 115 | template_name_suffix = '_list' 116 | 117 | def get_page_template(self, **kwargs): 118 | """Return the template name used for this request. 119 | 120 | Only called if *page_template* is not given as a kwarg of 121 | *self.as_view*. 122 | """ 123 | opts = self.object_list.model._meta 124 | return '{0}/{1}{2}{3}.html'.format( 125 | opts.app_label, 126 | opts.object_name.lower(), 127 | self.template_name_suffix, 128 | self.page_template_suffix, 129 | ) 130 | 131 | def get_template_names(self): 132 | """Switch the templates for Ajax requests.""" 133 | request = self.request 134 | querystring_key = request.REQUEST.get('querystring_key', PAGE_LABEL) 135 | if request.is_ajax() and querystring_key == self.key: 136 | return [self.page_template] 137 | return super( 138 | AjaxMultipleObjectTemplateResponseMixin, self).get_template_names() 139 | 140 | 141 | class AjaxListView(AjaxMultipleObjectTemplateResponseMixin, BaseListView): 142 | """Allows Ajax pagination of a list of objects. 143 | 144 | You can use this class-based view in place of *ListView* in order to 145 | recreate the behaviour of the *page_template* decorator. 146 | 147 | For instance, assume you have this code (taken from Django docs):: 148 | 149 | from django.conf.urls import patterns 150 | from django.views.generic import ListView 151 | 152 | from books.models import Publisher 153 | 154 | urlpatterns = patterns('', 155 | (r'^publishers/$', ListView.as_view(model=Publisher)), 156 | ) 157 | 158 | You want to Ajax paginate publishers, so, as seen, you need to switch 159 | the template if the request is Ajax and put the page template 160 | into the context as a variable named *page_template*. 161 | 162 | This is straightforward, you only need to replace the view class, e.g.:: 163 | 164 | from django.conf.urls import patterns 165 | 166 | from books.models import Publisher 167 | 168 | from endless_pagination.views import AjaxListView 169 | 170 | urlpatterns = patterns('', 171 | (r'^publishers/$', AjaxListView.as_view(model=Publisher)), 172 | ) 173 | 174 | NOTE: Django >= 1.3 is required to use this view. 175 | """ 176 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import os 3 | 4 | 5 | PROJECT_NAME = 'endless_pagination' 6 | ROOT = os.path.abspath(os.path.dirname(__file__)) 7 | # VENV = os.path.join(ROOT, '.venv') 8 | # VENV_LINK = os.path.join(VENV, 'local') 9 | 10 | 11 | project = __import__(PROJECT_NAME) 12 | 13 | root_dir = os.path.dirname(__file__) 14 | if root_dir: 15 | os.chdir(root_dir) 16 | 17 | data_files = [] 18 | for dirpath, dirnames, filenames in os.walk(PROJECT_NAME): 19 | for i, dirname in enumerate(dirnames): 20 | if dirname.startswith('.'): 21 | del dirnames[i] 22 | if '__init__.py' in filenames: 23 | continue 24 | elif filenames: 25 | for f in filenames: 26 | data_files.append(os.path.join( 27 | dirpath[len(PROJECT_NAME) + 1:], f)) 28 | 29 | 30 | def read(filename): 31 | return open(os.path.join(ROOT, filename)).read() 32 | 33 | 34 | class VenvLinkDeleted(object): 35 | 36 | restore_link = False 37 | 38 | def __enter__(self): 39 | """Remove the link.""" 40 | # if os.path.islink(VENV_LINK): 41 | # os.remove(VENV_LINK) 42 | # self.restore_link = True 43 | pass 44 | 45 | def __exit__(self, exc_type, exc_val, exc_tb): 46 | """Restore the link.""" 47 | # if self.restore_link: 48 | # os.symlink(VENV, VENV_LINK) 49 | pass 50 | 51 | 52 | with VenvLinkDeleted(): 53 | setup( 54 | name='django-endless-pagination-angular', 55 | version=project.get_version(), 56 | description=project.__doc__, 57 | long_description=read('README.rst'), 58 | author='Martin Peveri & Francesco Banconi', 59 | zip_safe=False, 60 | author_email='martinpeveri@gmail.com', 61 | url='https://github.com/mapeveri/django-endless-pagination-angular', 62 | keywords='django pagination ajax angular', 63 | packages=[ 64 | PROJECT_NAME, 65 | '{0}.templatetags'.format(PROJECT_NAME), 66 | '{0}.tests'.format(PROJECT_NAME), 67 | '{0}.tests.integration'.format(PROJECT_NAME), 68 | '{0}.tests.templatetags'.format(PROJECT_NAME), 69 | ], 70 | package_data={PROJECT_NAME: data_files}, 71 | classifiers=[ 72 | 'Development Status :: 5 - Production/Stable', 73 | 'Environment :: Web Environment', 74 | 'Framework :: Django', 75 | 'Intended Audience :: Developers', 76 | 'License :: OSI Approved :: MIT License', 77 | 'Operating System :: OS Independent', 78 | 'Programming Language :: Python', 79 | 'Programming Language :: Python :: 3', 80 | 'Topic :: Utilities', 81 | ], 82 | ) 83 | -------------------------------------------------------------------------------- /tests/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/tests/.coverage -------------------------------------------------------------------------------- /tests/develop.py: -------------------------------------------------------------------------------- 1 | """Create a development and testing environment using a virtualenv.""" 2 | 3 | from __future__ import unicode_literals 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | 9 | if sys.version_info[0] >= 3: 10 | VENV_NAME = '.venv3' 11 | # FIXME: running 2to3 on django-nose will no longer be required once 12 | # the project supports Python 3 (bug #16). 13 | PATCH_DJANGO_NOSE = True 14 | else: 15 | VENV_NAME = '.venv' 16 | PATCH_DJANGO_NOSE = False 17 | 18 | 19 | TESTS = os.path.abspath(os.path.dirname(__file__)) 20 | REQUIREMENTS = os.path.join(TESTS, 'requirements.pip') 21 | WITH_VENV = os.path.join(TESTS, 'with_venv.sh') 22 | VENV = os.path.abspath(os.path.join(TESTS, '..', VENV_NAME)) 23 | 24 | 25 | def call(*args): 26 | """Simple ``subprocess.call`` wrapper.""" 27 | if subprocess.call(args): 28 | raise SystemExit('Error running {0}.'.format(args)) 29 | 30 | 31 | def pip_install(*args): 32 | """Install packages using pip inside the virtualenv.""" 33 | call(WITH_VENV, VENV_NAME, 'pip', 'install', '--use-mirrors', *args) 34 | 35 | 36 | def patch_django_nose(): 37 | """Run 2to3 on django-nose and remove ``import new`` from its runner.""" 38 | # FIXME: delete once django-nose supports Python 3 (bug #16). 39 | python = 'python' + '.'.join(map(str, sys.version_info[:2])) 40 | django_nose = os.path.join( 41 | VENV, 'lib', python, 'site-packages', 'django_nose') 42 | call('2to3', '-w', '--no-diffs', django_nose) 43 | with open(os.path.join(django_nose, 'runner.py'), 'r+') as f: 44 | lines = [line for line in f.readlines() if 'import new' not in line] 45 | f.seek(0) 46 | f.truncate() 47 | f.writelines(lines) 48 | 49 | 50 | if __name__ == '__main__': 51 | call('virtualenv', '--distribute', '-p', sys.executable, VENV) 52 | pip_install('-r', REQUIREMENTS) 53 | # FIXME: delete from now on once django-nose supports Python 3 (bug #16). 54 | if PATCH_DJANGO_NOSE: 55 | patch_django_nose() 56 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import unicode_literals 4 | import os 5 | import sys 6 | 7 | from django.core.management import execute_from_command_line 8 | 9 | 10 | if __name__ == '__main__': 11 | root = os.path.join(os.path.dirname(__file__), '..') 12 | sys.path.append(os.path.abspath(root)) 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /tests/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/tests/project/__init__.py -------------------------------------------------------------------------------- /tests/project/context_processors.py: -------------------------------------------------------------------------------- 1 | """Navigation bar context processor.""" 2 | 3 | from __future__ import unicode_literals 4 | import platform 5 | 6 | import django 7 | from django.core.urlresolvers import reverse 8 | 9 | import endless_pagination 10 | 11 | 12 | VOICES = ( 13 | # Name and label pairs. 14 | ('complete', 'Complete example'), 15 | ('digg', 'Digg-style'), 16 | ('twitter', 'Twitter-style'), 17 | ('onscroll', 'On scroll'), 18 | ('multiple', 'Multiple'), 19 | ('callbacks', 'Callbacks'), 20 | ('chunks', 'On scroll/chunks'), 21 | ) 22 | 23 | 24 | def navbar(request): 25 | """Generate a list of voices for the navigation bar.""" 26 | voice_list = [] 27 | current_path = request.path 28 | for name, label in VOICES: 29 | path = reverse(name) 30 | voice_list.append({ 31 | 'label': label, 32 | 'path': path, 33 | 'is_active': path == current_path, 34 | }) 35 | return {'navbar': voice_list} 36 | 37 | 38 | def versions(request): 39 | """Add to context the version numbers of relevant apps.""" 40 | values = ( 41 | ('Python', platform.python_version()), 42 | ('Django', django.get_version()), 43 | ('Endless Pagination', endless_pagination.get_version()), 44 | ) 45 | return {'versions': values} 46 | -------------------------------------------------------------------------------- /tests/project/static/endless_pagination/js/module.endless.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var App = angular.module('EndlessPagination', []); 3 | 4 | /*Closest*/ 5 | (function (ELEMENT) { 6 | ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector; 7 | ELEMENT.closest = ELEMENT.closest || function closest(selector) { 8 | var element = this; 9 | while (element) { 10 | if (element.matches(selector)) { 11 | break; 12 | } 13 | element = element.parentElement; 14 | } 15 | return element; 16 | }; 17 | }(Element.prototype)); 18 | 19 | /* polyfill for Element.prototype.matches */ 20 | if ( !Element.prototype.matches ) { 21 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector; 22 | if ( !Element.prototype.matches ) { 23 | Element.prototype.matches = function matches( selector ) { 24 | var element = this; 25 | var matches = ( element.document || element.ownerDocument ).querySelectorAll( selector ); 26 | var i = 0; 27 | while ( matches[i] && matches[i] !== element ) { 28 | i++; 29 | } 30 | return matches[i] ? true : false; 31 | } 32 | } 33 | } 34 | 35 | /* addDelegatedEventListener */ 36 | Element.prototype.addDelegatedEventListener = function addDelegatedEventListener( type, selector, listener, useCapture, wantsUntrusted ) { 37 | this.addEventListener( type, function ( evt ) { 38 | var element = evt.target; 39 | do { 40 | if ( !element.matches || !element.matches( selector ) ) continue; 41 | listener.apply( element, arguments ); 42 | return; 43 | } while ( ( element = element.parentNode ) ); 44 | }, useCapture, wantsUntrusted ); 45 | } 46 | 47 | 48 | 49 | App.directive('endlessPagination', function($http, $window, $document) { 50 | return function (scope, element, attrs) { 51 | 52 | var defaults = { 53 | // Twitter-style pagination container selector. 54 | containerSelector: '.endless_container', 55 | // Twitter-style pagination loading selector. 56 | loadingSelector: '.endless_loading', 57 | // Twitter-style pagination link selector. 58 | moreSelector: 'a.endless_more', 59 | // Digg-style pagination page template selector. 60 | pageSelector: '.endless_page_template', 61 | // Digg-style pagination link selector. 62 | pagesSelector: 'a.endless_page_link', 63 | // Callback called when the user clicks to get another page. 64 | onClick: function() {}, 65 | // Callback called when the new page is correctly displayed. 66 | onCompleted: function() {}, 67 | // Set this to true to use the paginate-on-scroll feature. 68 | paginateOnScroll: false, 69 | // If paginate-on-scroll is on, this margin will be used. 70 | paginateOnScrollMargin : 1, 71 | // If paginate-on-scroll is on, it is possible to define chunks. 72 | paginateOnScrollChunkSize: 0 73 | }, 74 | 75 | settings = angular.extend(defaults, (attrs.endlessPagination ? eval('(' + attrs.endlessPagination + ')') : "")); 76 | 77 | var getContext = function(link) { 78 | return { 79 | key: link.attr('rel').split(' ')[0], 80 | url: link.attr('href') 81 | }; 82 | }; 83 | 84 | return angular.forEach(element, function() { 85 | var loadedPages = 1; 86 | // Twitter-style pagination. 87 | element[0].addDelegatedEventListener('click', settings.moreSelector, function ($event) { 88 | var link = angular.element(this); 89 | var html_link = link[0]; 90 | var container = angular.element(html_link.closest(settings.containerSelector)); 91 | var loading = container.children(settings.loadingSelector); 92 | // Avoid multiple Ajax calls. 93 | if (loading.offsetWidth > 0 && loading.offsetHeight > 0) { 94 | $event.preventDefault(); 95 | } 96 | link[0].style.display="none"; 97 | loading[0].style.display="block"; 98 | var context = getContext(link); 99 | //For get function onClick 100 | if(typeof settings.onClick == 'string'){ 101 | var onClick = scope.$eval(settings.onClick); 102 | }else{ 103 | var onClick = settings.onClick; 104 | } 105 | //For get function onComplete 106 | if(typeof settings.onCompleted == 'string'){ 107 | var onCompleted = scope.$eval(settings.onCompleted); 108 | }else{ 109 | var onCompleted = settings.onCompleted; 110 | } 111 | // Fire onClick callback. 112 | if (onClick.apply(html_link, [context]) !== false) { 113 | // Send the Ajax request. 114 | $http({ 115 | method: "GET", 116 | url: context.url, 117 | params: {querystring_key: context.key}, 118 | headers: { 119 | 'X-Requested-With': 'XMLHttpRequest' 120 | } 121 | }).success(function(fragment){ 122 | container.parent().append(fragment); 123 | container.remove(); 124 | // Increase the number of loaded pages. 125 | loadedPages += 1; 126 | // Fire onCompleted callback. 127 | onCompleted.apply(html_link, [context, fragment.trim()]); 128 | }); 129 | } 130 | $event.preventDefault(); 131 | }); 132 | 133 | // On scroll pagination. 134 | if (settings.paginateOnScroll) { 135 | angular.element($window).bind("scroll", function() { 136 | if ($document[0].body.offsetHeight - $window.innerHeight - 137 | $window.pageYOffset <= settings.paginateOnScrollMargin) { 138 | // Do not paginate on scroll if chunks are used and 139 | // the current chunk is complete. 140 | var chunckSize = settings.paginateOnScrollChunkSize; 141 | if (!chunckSize || loadedPages % chunckSize) { 142 | if(document.querySelector(settings.moreSelector) != null){ 143 | document.querySelector(settings.moreSelector).click(); 144 | } 145 | } 146 | } 147 | }); 148 | } 149 | 150 | // Digg-style pagination. 151 | element[0].addDelegatedEventListener('click', settings.pagesSelector, function ($event) { 152 | var link = angular.element(this), 153 | html_link = link[0], 154 | context = getContext(link); 155 | //For get function onClick 156 | if(typeof settings.onClick == 'string'){ 157 | var onClick = scope.$eval(settings.onClick); 158 | }else{ 159 | var onClick = settings.onClick; 160 | } 161 | //For get function onComplete 162 | if(typeof settings.onCompleted == 'string'){ 163 | var onCompleted = scope.$eval(settings.onCompleted); 164 | }else{ 165 | var onCompleted = settings.onCompleted; 166 | } 167 | // Fire onClick callback. 168 | if (onClick.apply(html_link, [context]) !== false) { 169 | var page_template = angular.element(html_link.closest(settings.pageSelector)); 170 | $http({ 171 | method: "GET", 172 | url: context.url, 173 | params: {querystring_key: context.key}, 174 | headers: { 175 | 'X-Requested-With': 'XMLHttpRequest' 176 | } 177 | }).success(function(fragment){ 178 | page_template.html(fragment); 179 | onCompleted.apply(html_link, [context, fragment.trim()]); 180 | }); 181 | } 182 | $event.preventDefault(); 183 | }); 184 | }); 185 | }; 186 | }); 187 | -------------------------------------------------------------------------------- /tests/project/static/endless_pagination/js/module.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var App = angular.module('TestApp', ['EndlessPagination']); 3 | 4 | App.controller("TestController", function($scope){ 5 | 6 | // Get a reference of the notifications element. 7 | var notifications = angular.element(document.querySelector('#notifications')); 8 | 9 | // Add a notification: the element containing the value will have *id*. 10 | var notify = function(id, key, value) { 11 | var key_element = angular.element('').html(key + ': '); 12 | var value_element = angular.element('').attr('id', id).html(value); 13 | var notification = angular.element('

').append(key_element).append(value_element); 14 | notifications.append(notification); 15 | } 16 | 17 | $scope.callbacks_click = function(context){ 18 | // Paginate! 19 | notifications.html(''); 20 | notify('onclick', 'First object on click', angular.element(document.querySelector('#endless h4')).html()); 21 | notify('onclick-label', 'Clicked label', angular.element(this).text()); 22 | notify('onclick-url', 'URL', context.url); 23 | notify('onclick-key', 'Querystring key', context.key); 24 | } 25 | 26 | $scope.callbacks_completed = function(context, fragment){ 27 | // Complete 28 | notify('oncompleted', 'First object on completed', angular.element(document.querySelector('#endless h4')).html()); 29 | notify('oncompleted-label', 'Clicked label', angular.element(this).text()); 30 | notify('oncompleted-url', 'URL', context.url); 31 | notify('oncompleted-key', 'Querystring key', context.key); 32 | } 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /tests/project/static/pagination.css: -------------------------------------------------------------------------------- 1 | /* Customized css for the pagination elements. */ 2 | 3 | .pagination div, 4 | .endless_container { 5 | display: inline-block; 6 | *display: inline; 7 | margin-bottom: 0; 8 | margin-left: 0; 9 | -webkit-border-radius: 3px; 10 | -moz-border-radius: 3px; 11 | border-radius: 3px; 12 | *zoom: 1; 13 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 14 | -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 15 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 16 | } 17 | 18 | .pagination div > a, 19 | .pagination span, 20 | .endless_container a { 21 | float: left; 22 | padding: 0 14px; 23 | line-height: 38px; 24 | background-color: #ffffff; 25 | border: 1px solid #dddddd; 26 | border-left-width: 0; 27 | } 28 | 29 | .pagination div > a:hover, 30 | .pagination .endless_page_current, 31 | .endless_container a:hover { 32 | background-color: #eeeeee; 33 | } 34 | 35 | .pagination div > a:first-child, 36 | .pagination div > span:first-child, 37 | .endless_container a { 38 | border-left-width: 1px; 39 | -webkit-border-radius: 3px 0 0 3px; 40 | -moz-border-radius: 3px 0 0 3px; 41 | border-radius: 3px 0 0 3px; 42 | } 43 | 44 | .endless_container { 45 | width: 50%; 46 | } 47 | 48 | .endless_container a { 49 | text-align: center; 50 | width: inherit; 51 | text-decoration: none; 52 | } 53 | -------------------------------------------------------------------------------- /tests/project/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/tests/project/templates/404.html -------------------------------------------------------------------------------- /tests/project/templates/500.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapeveri/django-endless-pagination-angular/5b3259dd27e5b8f7e763c7ea14a20de8d77bfd42/tests/project/templates/500.html -------------------------------------------------------------------------------- /tests/project/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Testing project{% endblock %} - Django Endless Pagination Angular 6 | 7 | 8 | 9 | 10 |

11 |
12 | {% for name, value in versions %} 13 |
14 | {{ name }} 15 |
16 |
v{{ value }}
17 | {% endfor %} 18 |
19 | 22 | 34 |
35 | {% block content %}{% endblock %} 36 |
37 |
38 | {% block js %} 39 | 40 | 41 | {% endblock %} 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/project/templates/base_callbacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Testing project{% endblock %} - Django Endless Pagination Angular 6 | 7 | 8 | 9 | 10 |
11 |
12 | {% for name, value in versions %} 13 |
14 | {{ name }} 15 |
16 |
v{{ value }}
17 | {% endfor %} 18 |
19 | 22 | 35 |
36 | {% block content %}{% endblock %} 37 |
38 |
39 | {% block js %} 40 | 41 | 42 | 43 | {% endblock %} 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/project/templates/callbacks/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_callbacks.html" %} 2 | 3 | {% block content %} 4 |
5 | {% include page_template %} 6 |
7 |
8 |
9 |

Notifications

10 |
None so far...
11 |
12 |
13 | 14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /tests/project/templates/callbacks/page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 3 objects %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | {% get_pages %} 11 |
12 | Objects {{ pages.current_start_index }} ... 13 | {{ pages.current_end_index }} 14 | of {{ pages.total_count }} 15 |
16 | 19 | -------------------------------------------------------------------------------- /tests/project/templates/chunks/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% include "chunks/objects_page.html" %} 6 |
7 |
8 | {% include "chunks/items_page.html" %} 9 |
10 |
11 | Objects are paginated on scroll using 3 pages chunks. 12 |
13 | Items are paginated on scroll using 4 pages chunks. 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tests/project/templates/chunks/items_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 5 items using "items-page" %} 4 | {% for item in items %} 5 |
6 |

{{ item.title }}

7 | {{ item.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/chunks/objects_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 5 objects %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | 12 | -------------------------------------------------------------------------------- /tests/project/templates/complete/articles_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% lazy_paginate 3 articles using "articles-page" %} 4 | {% for article in articles %} 5 |
6 |

{{ article.title }}

7 | {{ article.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/complete/entries_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% lazy_paginate 1,3 entries using "entries-page" %} 4 | {% for entry in entries %} 5 |
6 |

{{ entry.title }}

7 | {{ entry.contents }} 8 |
9 | {% endfor %} 10 | {% show_more %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/complete/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% include "complete/objects_page.html" %} 6 |
7 |
8 | {% include "complete/items_page.html" %} 9 |
10 |
11 | This complete example shows several pagination styles. 12 |
13 | Objects are paginated using Digg-style, defaulting to the 14 | last page, with no Ajax involved. 15 |
16 | Items are paginated using Digg-style with Ajax support. 17 |
18 |
19 | {% include "complete/entries_page.html" %} 20 |
21 |
22 | {% include "complete/articles_page.html" %} 23 |
24 |
25 | Entries are paginated using Twitter-style with Ajax 26 | enabled. The first page contains just one entry. Subsequent pages 27 | contain three entries. 28 |
29 | Articles are paginated using Twitter-style. The subsequent 30 | pages are loaded on scroll. 31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /tests/project/templates/complete/items_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 3 items using "items-page" %} 4 | {% for item in items %} 5 |
6 |

{{ item.title }}

7 | {{ item.contents }} 8 |
9 | {% endfor %} 10 | 13 | -------------------------------------------------------------------------------- /tests/project/templates/complete/objects_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 3 objects starting from page -1 using "objects-page" %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | 13 | -------------------------------------------------------------------------------- /tests/project/templates/digg/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% include page_template %} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tests/project/templates/digg/page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 5 objects %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | 13 | -------------------------------------------------------------------------------- /tests/project/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

6 | This project is intended to be used as a testing environment for 7 | Django Endless Pagination Angular. 8 |

9 |

10 | This project also contains a basic collection 11 | of examples on how to use this application, providing both the ability 12 | to manually test Django Endless Pagination Angular in the browser, and a demo 13 | showing some of the application features in action. 14 |

15 |

16 | The documentation is 17 | avaliable online 18 | or in the docs directory of the project.
19 | The source code for this app is hosted at 20 | Github. 21 |

22 |

23 | The template for this project is realized using the 24 | Bootstrap framework. 26 | However, Bootstrap is not required in order to use 27 | Django Endless Pagination Angular. 28 |

29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /tests/project/templates/multiple/entries_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% lazy_paginate 1,3 entries using "entries-page" %} 4 | {% for entry in entries %} 5 |
6 |

{{ entry.title }}

7 | {{ entry.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/multiple/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | {% include "multiple/objects_page.html" %} 7 |
8 |
9 | {% include "multiple/items_page.html" %} 10 |
11 |
12 | {% include "multiple/entries_page.html" %} 13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tests/project/templates/multiple/items_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 3 items using "items-page" %} 4 | {% for item in items %} 5 |
6 |

{{ item.title }}

7 | {{ item.contents }} 8 |
9 | {% endfor %} 10 | 13 | -------------------------------------------------------------------------------- /tests/project/templates/multiple/objects_page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 3 objects using "objects-page" %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | 14 | -------------------------------------------------------------------------------- /tests/project/templates/onscroll/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% include page_template %} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tests/project/templates/onscroll/page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 10 objects %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/twitter/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |
6 | {% include page_template %} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tests/project/templates/twitter/page.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 5 objects %} 4 | {% for object in objects %} 5 |
6 |

{{ object.title }}

7 | {{ object.contents }} 8 |
9 | {% endfor %} 10 | {% show_more "More results" %} 11 | -------------------------------------------------------------------------------- /tests/project/templates/twitter/table.html: -------------------------------------------------------------------------------- 1 | {% load endless %} 2 | 3 | {% paginate 5 objects %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for object in objects %} 13 | 14 | 15 | 16 | 17 | {% endfor %} 18 | {% show_more_table "More results" %} 19 | 20 |
TitleContent
{{ object.title }}{{ object.contents }}
-------------------------------------------------------------------------------- /tests/project/urls.py: -------------------------------------------------------------------------------- 1 | """Test project URL patterns.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.conf.urls import url 6 | from django.views.generic import TemplateView 7 | 8 | from endless_pagination.decorators import ( 9 | page_template, 10 | page_templates, 11 | ) 12 | 13 | from project.views import generic 14 | 15 | 16 | # Avoid lint errors for the following Django idiom: flake8: noqa. 17 | urlpatterns = [ 18 | url(r'^$', 19 | TemplateView.as_view(template_name="home.html"), 20 | name='home'), 21 | url(r'^complete/$', 22 | page_templates({ 23 | 'complete/objects_page.html': 'objects-page', 24 | 'complete/items_page.html': 'items-page', 25 | 'complete/entries_page.html': 'entries-page', 26 | 'complete/articles_page.html': 'articles-page', 27 | })(generic), 28 | {'template': 'complete/index.html', 'number': 21}, 29 | name='complete'), 30 | url(r'^digg/$', 31 | page_template('digg/page.html')(generic), 32 | {'template': 'digg/index.html'}, 33 | name='digg'), 34 | url(r'^twitter/$', 35 | page_template('twitter/page.html')(generic), 36 | {'template': 'twitter/index.html'}, 37 | name='twitter'), 38 | url(r'^onscroll/$', 39 | page_template('onscroll/page.html')(generic), 40 | {'template': 'onscroll/index.html'}, 41 | name='onscroll'), 42 | url(r'^chunks/$', 43 | page_templates({ 44 | 'chunks/objects_page.html': None, 45 | 'chunks/items_page.html': 'items-page', 46 | })(generic), 47 | {'template': 'chunks/index.html', 'number': 50}, 48 | name='chunks'), 49 | url(r'^multiple/$', 50 | page_templates({ 51 | 'multiple/objects_page.html': 'objects-page', 52 | 'multiple/items_page.html': 'items-page', 53 | 'multiple/entries_page.html': 'entries-page', 54 | })(generic), 55 | {'template': 'multiple/index.html', 'number': 21}, 56 | name='multiple'), 57 | url(r'^callbacks/$', 58 | page_template('callbacks/page.html')(generic), 59 | {'template': 'callbacks/index.html'}, 60 | name='callbacks'), 61 | ] 62 | -------------------------------------------------------------------------------- /tests/project/views.py: -------------------------------------------------------------------------------- 1 | """Test project views.""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.shortcuts import render 6 | 7 | 8 | LOREM = """Lorem ipsum dolor sit amet, consectetur adipisicing elit, 9 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris 11 | nisi ut aliquip ex ea commodo consequat. 12 | """ 13 | 14 | 15 | def _make(title, number): 16 | """Make a *number* of items.""" 17 | return [ 18 | {'title': '{0} {1}'.format(title, i + 1), 'contents': LOREM} 19 | for i in range(number) 20 | ] 21 | 22 | 23 | def generic(request, extra_context=None, template=None, number=50): 24 | context = { 25 | 'objects': _make('Object', number), 26 | 'items': _make('Item', number), 27 | 'entries': _make('Entry', number), 28 | 'articles': _make('Article', number), 29 | } 30 | if extra_context is not None: 31 | context.update(extra_context) 32 | return render(request, template, context) 33 | -------------------------------------------------------------------------------- /tests/requirements.pip: -------------------------------------------------------------------------------- 1 | # Django Endless Pagination Angular test requirements. 2 | # Dependencies are installed by the ``make`` command. 3 | 4 | coverage==3.6 5 | django 6 | django-nose==1.4.4 7 | flake8==2.0 8 | ipdb==0.7 9 | nose==1.2.1 10 | selenium==2.31.0 11 | sphinx==1.1.3 12 | xvfbwrapper==0.1.3 13 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Settings file for the Django project used for tests.""" 2 | 3 | import os 4 | 5 | 6 | PROJECT_NAME = 'project' 7 | 8 | # Base paths. 9 | ROOT = os.path.abspath(os.path.dirname(__file__)) 10 | PROJECT = os.path.join(ROOT, PROJECT_NAME) 11 | 12 | # Django configuration. 13 | DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}} 14 | DEBUG = TEMPLATE_DEBUG = True 15 | INSTALLED_APPS = ( 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | 'endless_pagination', 23 | PROJECT_NAME, 24 | ) 25 | LANGUAGE_CODE = os.getenv('ENDLESS_PAGINATION_LANGUAGE_CODE', 'en-us') 26 | ROOT_URLCONF = PROJECT_NAME + '.urls' 27 | SECRET_KEY = os.getenv('ENDLESS_PAGINATION_SECRET_KEY', 'secret') 28 | SITE_ID = 1 29 | STATIC_ROOT = os.path.join(PROJECT, 'static') 30 | STATIC_URL = '/static/' 31 | 32 | TEMPLATES = [ 33 | { 34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 35 | 'DIRS': [os.path.join(PROJECT, 'templates')], 36 | 'APP_DIRS': True, 37 | 'OPTIONS': { 38 | 'context_processors': [ 39 | 'django.template.context_processors.debug', 40 | 'django.template.context_processors.request', 41 | 'django.contrib.auth.context_processors.auth', 42 | 'django.contrib.messages.context_processors.messages', 43 | 'django.template.context_processors.media', 44 | 'django.template.context_processors.static', 45 | 'django.template.context_processors.tz', 46 | ], 47 | }, 48 | }, 49 | ] 50 | 51 | # Testing. 52 | NOSE_ARGS = ( 53 | '--verbosity=2', 54 | '--with-coverage', 55 | '--cover-package=endless_pagination', 56 | ) 57 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 58 | -------------------------------------------------------------------------------- /tests/with_venv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TESTS=`dirname $0` 4 | VENV=$TESTS/../$1 5 | shift 6 | . $VENV/bin/activate && $@ 7 | --------------------------------------------------------------------------------