Repository: shtalinberg/django-el-pagination Branch: develop Commit: f795d6120ae2 Files: 134 Total size: 318.4 KB Directory structure: gitextract_8g56kzu8/ ├── .coveragerc ├── .github/ │ └── workflows/ │ └── tox.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yaml ├── .vscode/ │ ├── launch.json │ └── settings.json ├── AUTHORS ├── HACKING ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── Makefile ├── PKG-INFO ├── 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 │ ├── requirements.txt │ ├── start.rst │ ├── templatetags_reference.rst │ ├── thanks.rst │ └── twitter_pagination.rst ├── el_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 │ │ ├── pt_BR/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ └── zh_CN/ │ │ └── LC_MESSAGES/ │ │ ├── django.mo │ │ └── django.po │ ├── models.py │ ├── paginators.py │ ├── settings.py │ ├── static/ │ │ └── el-pagination/ │ │ └── js/ │ │ └── el-pagination.js │ ├── templates/ │ │ └── el_pagination/ │ │ ├── current_link.html │ │ ├── next_link.html │ │ ├── page_link.html │ │ ├── previous_link.html │ │ ├── show_more.html │ │ ├── show_more_table.html │ │ └── show_pages.html │ ├── templatetags/ │ │ ├── __init__.py │ │ └── el_pagination_tags.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── integration/ │ │ │ ├── __init__.py │ │ │ ├── test_callbacks.py │ │ │ ├── test_chunks.py │ │ │ ├── test_digg.py │ │ │ ├── test_feed_wrapper.py │ │ │ ├── test_multiple.py │ │ │ ├── test_onscroll.py │ │ │ └── test_twitter.py │ │ ├── templatetags/ │ │ │ ├── __init__.py │ │ │ └── test_el_pagination_tags.py │ │ ├── test_decorators.py │ │ ├── test_loaders.py │ │ ├── test_models.py │ │ ├── test_paginators.py │ │ ├── test_utils.py │ │ └── test_views.py │ ├── utils.py │ └── views.py ├── pyproject.toml ├── release-requirements.txt ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── develop.py │ ├── manage.py │ ├── project/ │ │ ├── __init__.py │ │ ├── context_processors.py │ │ ├── models.py │ │ ├── settings.py │ │ ├── static/ │ │ │ └── pagination.css │ │ ├── templates/ │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── base.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 │ │ │ │ └── objects_simple_page.html │ │ │ ├── digg/ │ │ │ │ ├── index.html │ │ │ │ ├── page.html │ │ │ │ └── table/ │ │ │ │ ├── index.html │ │ │ │ └── page.html │ │ │ ├── feed_wrapper/ │ │ │ │ ├── index.html │ │ │ │ └── page.html │ │ │ ├── home.html │ │ │ ├── multiple/ │ │ │ │ ├── entries_page.html │ │ │ │ ├── index.html │ │ │ │ ├── items_page.html │ │ │ │ └── objects_page.html │ │ │ ├── onscroll/ │ │ │ │ ├── index.html │ │ │ │ ├── page.html │ │ │ │ └── table/ │ │ │ │ ├── index.html │ │ │ │ └── page.html │ │ │ └── twitter/ │ │ │ ├── index.html │ │ │ ├── page.html │ │ │ └── table/ │ │ │ ├── index.html │ │ │ └── page.html │ │ ├── urls.py │ │ └── views.py │ ├── requirements.pip │ └── with_venv.sh └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] source = el_pagination branch = 1 [report] omit = *tests*,*migrations* ================================================ FILE: .github/workflows/tox.yml ================================================ name: Tox on: [push, pull_request] jobs: tox-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install --upgrade pip - run: pip install tox - run: tox -e lint || true # Fix error and remove "|| true"! tox-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install --upgrade pip - run: pip install tox - run: tox -e docs || true # Fix error and remove "|| true"! tox-docs-linkcheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.x - run: pip install --upgrade pip - run: pip install tox - run: tox -e docs-linkcheck || true # Fix error and remove "|| true"! build: strategy: fail-fast: false max-parallel: 5 matrix: os: [ubuntu-latest] # [macos-latest, ubuntu-latest, windows-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] django-version: ["==3.2.*", "==4.1.*", "==4.2.*", "==5.0.*", "==5.1.*", "==5.2.*"] exclude: # https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django - python-version: 3.11 django-version: "==3.2.*" - python-version: 3.12 django-version: "==3.2.*" - python-version: 3.12 django-version: "==4.1.*" - python-version: 3.8 django-version: "==5.0.*" - python-version: 3.8 django-version: "==5.1.*" - python-version: 3.8 django-version: "==5.2.*" - python-version: 3.9 django-version: "==5.0.*" - python-version: 3.9 django-version: "==5.1.*" - python-version: 3.9 django-version: "==5.2.*" # # Django 4.0 no longer supports python 3.7 # - python-version: 3.7 # django-version: "==4.0.*" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Get pip cache dir id: pip-cache run: | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: -${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} restore-keys: | -${{ matrix.python-version }}-v1- - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox tox-gh-actions - name: Tox tests run: | tox -v - name: Upload coverage uses: codecov/codecov-action@v3 with: name: Python ${{ matrix.python-version }} ================================================ FILE: .gitignore ================================================ # Python *.py[cod] __pycache__/ *.so .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # Sphinx documentation _static/TRACKME .doctrees/ doc/_build doc/.doctrees *.sublime-* .idea* /help-man ~$ .venv/ .coverage .ropeproject .DS_Store MANIFEST tests/settings_local.py *.log .tox/ ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort - repo: local hooks: - id: black name: black language: system entry: sh -c 'make black_diff' types: [python] exclude: (migrations/|\.venv|\.git) - repo: local hooks: - id: pylint name: pylint language: system entry: sh -c 'make pylint' types: [python] exclude: (migrations/|\.venv|\.git) ================================================ FILE: .pylintrc ================================================ [MAIN] # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Load and enable all available extensions. Use --list-extensions to see a list # all available extensions. #enable-all-extensions= # In error mode, messages with a category besides ERROR or FATAL are # suppressed, and no reports are done by default. Error mode is compatible with # disabling specific errors. #errors-only= # Always return a 0 (non-error) status code, even if lint errors are found. # This is primarily useful in continuous integration scripts. #exit-zero= # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-allow-list= # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. (This is an alternative name to extension-pkg-allow-list # for backward compatibility.) extension-pkg-whitelist= # Return non-zero exit code if any of these messages/categories are detected, # even if score is above --fail-under value. Syntax same as enable. Messages # specified are enabled, while categories only check already-enabled messages. fail-on= # Specify a score threshold to be exceeded before program exits with error. fail-under=10 # Interpret the stdin as a python script, whose filename needs to be passed as # the module_or_package argument. #from-stdin= # Files or directories to be skipped. They should be base names, not paths. ignore=tests # Add files or directories matching the regex patterns to the ignore-list. The # regex matches against paths and can be in Posix or Windows format. ignore-paths=logs, docs, help-man # Files or directories matching the regex patterns are skipped. The regex # matches against base names, not paths. The default value ignores Emacs file # locks ignore-patterns=conftest.py,tests.py,test_*, test* # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook='import sys; sys.path = ["src"] + sys.path' init-hook='import os, sys; sys.path.append("/el_pagination"); sys.path.append("/tests"); sys.path.append(".")' # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use, and will cap the count on Windows to # avoid hangs. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins=pylint_django # Pickle collected data for later comparisons. persistent=no # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. py-version=3.10 # Discover python modules and packages in the file system subtree. recursive=no # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # In verbose mode, extra non-checker-related info will be displayed. #verbose= [REPORTS] # Python expression which should return a score less than or equal to 10. You # have access to the variables 'fatal', 'error', 'warning', 'refactor', # 'convention', and 'info' which contain the number of messages in each # category, as well as 'statement' which is the total number of statements # analyzed. This score is used by the global evaluation report (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=colorized # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, # UNDEFINED. confidence=HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then re-enable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=django-settings-module-not-found, missing-module-docstring, missing-class-docstring, missing-function-docstring, too-few-public-methods, unused-argument, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [SIMILARITIES] # Comments are removed from the similarity computation ignore-comments=yes # Docstrings are removed from the similarity computation ignore-docstrings=yes # Imports are removed from the similarity computation ignore-imports=yes # Signatures are removed from the similarity computation ignore-signatures=yes # Minimum lines number of a similarity. min-similarity-lines=4 [IMPORTS] # List of modules that can be imported at any level, not just the top level # one. allow-any-import-level= # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Output a graph (.gv or any supported image format) of external dependencies # to the given file (report RP0402 must not be disabled). ext-import-graph= # Output a graph (.gv or any supported image format) of all (i.e. internal and # external) dependencies to the given file (report RP0402 must not be # disabled). import-graph= # Output a graph (.gv or any supported image format) of internal dependencies # to the given file (report RP0402 must not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant # Couples of modules and preferred modules, separated by a comma. preferred-modules= [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members=cv2.* # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of symbolic message names to ignore for Mixin members. ignored-checks-for-mixins=no-member, not-async-context-manager, not-context-manager, attribute-defined-outside-init # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 # Regex pattern to define which classes are considered mixins. mixin-class-rgx=.*[Mm]ixin # List of decorators that change the signature of a decorated function. signature-mutators= [LOGGING] # The type of string formatting that logging methods do. `old` means using % # formatting, `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of names allowed to shadow builtins allowed-redefined-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [STRING] # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. check-quote-consistency=no # This flag controls whether the implicit-str-concat should generate a warning # on implicit string concatenation in sequences defined over several lines. check-str-concat-over-line-jumps=no [DESIGN] # List of regular expressions of class ancestor names to ignore when counting # public methods (see R0903) exclude-too-few-public-methods= # List of qualified class names to ignore when counting class parents (see # R0901) ignored-parents= # Maximum number of arguments for function / method. max-args=10 # Maximum number of attributes for a class (see R0902). max-attributes=13 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=15 # Maximum number of locals for function / method body. max-locals=20 # Maximum number of parents for a class (see R0901). max-parents=10 # Maximum number of positional arguments for function / method. max-positional-arguments=10 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. If left empty, argument names will be checked with the set # naming style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. If left empty, attribute names will be checked with the set naming # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Bad variable names regexes, separated by a comma. If names match any regex, # they will always be refused bad-names-rgxs= # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. If left empty, class attribute names will be checked # with the set naming style. #class-attribute-rgx= # Naming style matching correct class constant names. class-const-naming-style=UPPER_CASE # Regular expression matching correct class constant names. Overrides class- # const-naming-style. If left empty, class constant names will be checked with # the set naming style. #class-const-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. If left empty, class names will be checked with the set naming style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. If left empty, constant names will be checked with the set naming # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. If left empty, function names will be checked with the set # naming style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted good-names-rgxs= # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. If left empty, inline iteration names will be checked # with the set naming style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. If left empty, method names will be checked with the set naming style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. If left empty, module names will be checked with the set naming style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Regular expression matching correct type variable names. If left empty, type # variable names will be checked with the set naming style. #typevar-rgx= # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. If left empty, variable names will be checked with the set # naming style. variable-rgx=(([a-z_][a-z0-9_]+)|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$ [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO # Regular expression of note tags to take in consideration. notes-rgx= [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=119 # Maximum number of lines in a module. max-module-lines=1200 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it work, # install the 'python-enchant' package. spelling-dict= # List of comma separated words that should be considered directives if they # appear at the beginning of a comment and should not be checked. spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains the private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to the private dictionary (see the # --spelling-private-dict-file option) instead of raising a message. spelling-store-unknown-words=no [CLASSES] # Warn about protected attribute access inside special methods check-protected-access-in-special-methods=no # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [EXCEPTIONS] # Exceptions that will emit a warning when caught. overgeneral-exceptions=builtins.BaseException, builtins.Exception [DJANGO FOREIGN KEYS REFERENCED BY STRINGS] # A module containing Django settings to be used while linting. django-settings-module=project.settings ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.10" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: doc/conf.py fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub formats: - pdf # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - doc - requirements: doc/requirements.txt # Don't install the package in editable mode # This ensures we're testing the actual installation python: install: - method: pip path: . - requirements: doc/requirements.txt ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Python Demo: Django", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/tests/manage.py", "console": "internalConsole", "args": [ "runserver", "127.0.0.1:8000", "--noreload", "--settings=project.settings" ], "django": true, "autoStartBrowser": false }, { "type": "chrome", "request": "attach", "name": "Attach to Chrome", "port": 9222, "urlFilter": "http://127.0.0.1:8000/*", "webRoot": "${workspaceFolder}" }, ], "compounds": [ { "name": "Django", "configurations": [ "Python Demo: Django", ] }, ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.trimAutoWhitespace": true, "editor.useTabStops": true, "files.trimTrailingWhitespace": true, "html.format.enable": false, "html.format.templating": true, "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": false, "javascript.format.insertSpaceAfterCommaDelimiter": false, "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": false, "javascript.format.insertSpaceAfterSemicolonInForStatements": false, "[django-html]": { "editor.defaultFormatter": "monosans.djlint" }, "emmet.includeLanguages": { "django-html": "html", }, "files.associations": { "**/templates{/**,*}.html": "django-html", "**/templates{/**,*}.htm": "django-html", "**/templates{/**,*}.txt": "django-txt", "**/requirements{/**,*}.{txt,pip}": "pip-requirements", "**/*.html": "html" }, "[python]": { "editor.formatOnType": true, "editor.defaultFormatter": "ms-python.black-formatter" }, "python.analysis.extraPaths": [ "./src/apps", "./src/compat" ], "python.testing.pytestArgs": [ "src" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, } ================================================ FILE: AUTHORS ================================================ Oleksandr Shtalinberg Francesco Banconi Christian Clauss ================================================ FILE: HACKING ================================================ Hacking Django EL(Endless) Pagination ================================= Here are the steps needed to set up a development and testing environment. Creating a development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The development environment is created in a venv. The environment creation requires the *make* program to be installed. To install *make* under Debian/Ubuntu:: $ sudo apt-get install build-essential Under Mac OS/X, *make* is available as part of XCode. At this point, from the root of this branch, run the command:: $ make This command will create a ``.venv`` directory in the branch root, ignored by DVCSes, containing the development virtual environment with all the dependencies. Testing the application ~~~~~~~~~~~~~~~~~~~~~~~ Run the tests:: $ make test The command above also runs all tests except the available integration. They use Selenium and require Firefox to be installed. To include executing integration tests, define the environment variable USE_SELENIUM, e.g.:: $ make test USE_SELENIUM=1 Integration tests are excluded by default when using Python 3. The test suite requires Python >= 3.8.0. Run the tests and lint/pep8 checks:: $ make check Again, to exclude integration tests:: $ make check USE_SELENIUM=1 Debugging ~~~~~~~~~ Run the Django shell (Python interpreter):: $ make shell Run the Django development server for manual testing:: $ make server After executing the command above, it is possible to navigate the testing project going to . See all the available make targets, including info on how to create a Python 3 development environment:: $ make help Thanks for contributing, and have fun! Pipy testing before bump new vervion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before register our package with PyPI, it is important to test if the package actually works. pip install -e git+https://shtalinberg@github.com/shtalinberg/django-el-pagination#egg=django-el-pagination pip install -e git+https://shtalinberg@github.com/shtalinberg/django-el-pagination@develop#egg=django-el-pagination ================================================ FILE: INSTALL ================================================ To install django-el-pagination, run the following command inside this directory: make install Or if you'd prefer you can simply place the included ``el_pagination`` package on your Python path. ================================================ FILE: LICENSE ================================================ Copyright (c) 2009-2013 Francesco Banconi Copyright (c) 2015-2024 Oleksandr Shtalinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include AUTHORS include HACKING include INSTALL include LICENSE include Makefile include MANIFEST.in include README.rst # Documentation recursive-include doc * # Test files recursive-include tests * # Package resources recursive-include el_pagination/static * recursive-include el_pagination/locale * recursive-include el_pagination/templates * recursive-include el_pagination/templatetags *.py # Exclude cache files and compiled python files recursive-exclude * __pycache__ recursive-exclude * *.py[cod] recursive-exclude * *.so recursive-exclude * .*.swp recursive-exclude * .DS_Store # Exclude version control prune .git prune .github prune .gitignore ================================================ FILE: Makefile ================================================ # Django Endless Pagination Makefile. # Define these variables based on the system Python versions. PYTHON ?= python3 VENV = .venv WITH_VENV = ./tests/with_venv.sh $(VENV) MANAGE = $(PYTHON) ./tests/manage.py LINTER = flake8 --show-source el_pagination/ tests/ DOC_INDEX = doc/_build/html/index.html .PHONY: all clean cleanall check develop help install lint doc opendoc release server shell source test all: develop # Virtual environment $(VENV)/bin/activate: tests/develop.py tests/requirements.pip @$(PYTHON) tests/develop.py @touch $(VENV)/bin/activate $(VENV)/bin/pip install --upgrade pip setuptools wheel $(VENV)/bin/pip install -r tests/requirements.pip develop: $(VENV)/bin/activate $(VENV)/bin/pip install -e . # Documentation $(DOC_INDEX): $(wildcard doc/*.rst) @$(WITH_VENV) make -C doc html doc: develop $(DOC_INDEX) clean: pip uninstall django-el-pagination -y || true rm -rf .coverage build/ dist/ doc/_build MANIFEST *.egg-info find . -name '*.pyc' -delete find . -name '__pycache__' -type d -delete cleanall: clean rm -rf $(VENV) check: test lint install: pip install --force-reinstall -e . lint: develop @$(WITH_VENV) $(LINTER) black: develop @echo "*** Black - Reformat pycode ***" @echo "" @$(WITH_VENV) black el_pagination/ tests/ black_diff: develop @echo "*** Black - Show pycode diff ***" @echo "" black --diff --check --color el_pagination/ tests/ pylint: develop @echo "Running pylint" pylint --rcfile=.pylintrc el_pagination/ tests/ @echo "Finish pylint" opendoc: doc @firefox $(DOC_INDEX) server: develop @$(WITH_VENV) $(MANAGE) runserver 0.0.0.0:8000 shell: develop @$(WITH_VENV) $(MANAGE) shell source: $(PYTHON) setup.py sdist test: develop @$(WITH_VENV) $(MANAGE) test build-dist: clean develop @echo "Installing build dependencies..." $(VENV)/bin/pip install build twine @echo "Building distribution..." $(VENV)/bin/python -m build check-dist: build-dist @echo "Checking distribution..." $(VENV)/bin/twine check dist/* upload-dist: check-dist @echo "Uploading to PyPI..." $(VENV)/bin/twine upload dist/* release: clean develop @echo "Starting release process..." @if [ -z "$$SKIP_CONFIRMATION" ]; then \ read -p "Are you sure you want to release to PyPI? [y/N] " confirm; \ if [ "$$confirm" != "y" ]; then \ echo "Release cancelled."; \ exit 1; \ fi \ fi $(MAKE) build-dist $(MAKE) check-dist @echo "Ready to upload to PyPI..." @if [ -z "$$SKIP_CONFIRMATION" ]; then \ read -p "Proceed with upload? [y/N] " confirm; \ if [ "$$confirm" != "y" ]; then \ echo "Upload cancelled."; \ exit 1; \ fi \ fi $(MAKE) upload-dist @echo "Release completed successfully!" help: @echo 'Django Endless Pagination - Available commands:' @echo @echo 'Development:' @echo ' make - Set up development environment' @echo ' make install - Install package locally' @echo ' make server - Run development server' @echo ' make shell - Enter Django shell' @echo @echo 'Testing:' @echo ' make test - Run tests' @echo ' make lint - Run code linting' @echo ' make check - Run tests and linting' @echo @echo 'Documentation:' @echo ' make doc - Build documentation' @echo ' make opendoc - Build and open documentation' @echo @echo 'Cleaning:' @echo ' make clean - Remove build artifacts' @echo ' make cleanall - Remove all generated files including venv' @echo @echo 'Distribution:' @echo ' make source - Create source package' @echo ' make release - Upload to PyPI' @echo @echo 'Environment Variables:' @echo ' USE_SELENIUM=1 - Include integration tests' @echo ' SHOW_BROWSER=1 - Show browser during Selenium tests' @echo ================================================ FILE: PKG-INFO ================================================ Metadata-Version: 1.2 Name: django-el-pagination Version: 4.2.0 Summary: Django pagination tools supporting Ajax, multiple and lazy pagination, Twitter-style and Digg-style pagination. Home-page: https://github.com/shtalinberg/django-el-pagination Author: Oleksandr Shtalinberg Author-email: O.Shtalinberg@gmail.com License: MIT Description: Django EL(Endless) Pagination can be used to provide Twitter-style or Digg-style pagination, with optional Ajax support and other features like multiple or lazy pagination. The initial idea, which has guided the development of this application, is to allow pagination of web contents in `very few steps `_. **Documentation** is `avaliable online `_, or in the docs directory of the project. To file **bugs and requests**, please use https://github.com/shtalinberg/django-el-pagination/issues. The **source code** for this app is hosted at https://github.com/shtalinberg/django-el-pagination. Keywords: django pagination ajax Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Utilities ================================================ FILE: README.rst ================================================ ============================= Django EL(Endless) Pagination ============================= | |pypi-pkg-version| |python-versions| |django-versions| |pypi-status| |docs| | |build-ci-status| |tox-ci-status| |codecov| Django EL(Endless) Pagination can be used to provide Twitter-style or Digg-style pagination, with optional Ajax support and other features like multiple or lazy pagination. This app **django-el-pagination** forked from django-endless-pagination==2.0 (https://github.com/frankban/django-endless-pagination) From version 4.0.0 drop support Django<3.2. For support Django<3.2 use django-endless-pagination<4.0.x From version 4.1.2 added support Django 5.0 and python 3.12 The initial idea, which has guided the development of this application, is to allow pagination of web contents in `very few steps `_. **Documentation** is `available online `_, or in the doc directory of the project. To file **bugs and requests**, please use https://github.com/shtalinberg/django-el-pagination/issues. The **source code** for this app is hosted at https://github.com/shtalinberg/django-el-pagination. Pull requests are welcome. See `Contributing Guide `_. .. |build-ci-status| image:: https://github.com/shtalinberg/django-el-pagination/actions/workflows/tox.yml/badge.svg?branch=master :target: https://github.com/shtalinberg/django-el-pagination/actions/workflows/tox.yml :alt: Build release status .. |docs| image:: https://readthedocs.org/projects/django-el-pagination/badge/?version=latest :target: https://django-el-pagination.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |pypi-pkg-version| image:: https://img.shields.io/pypi/v/django-el-pagination.svg :target: https://pypi.python.org/pypi/django-el-pagination/ .. |pypi-status| image:: https://img.shields.io/pypi/status/coverage.svg :target: https://pypi.python.org/pypi/django-el-pagination/ .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-el-pagination.svg .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-el-pagination.svg .. |codecov| image:: https://codecov.io/gh/shtalinberg/django-el-pagination/branch/master/graph/badge.svg :target: https://codecov.io/gh/shtalinberg/django-el-pagination :alt: Code coverage .. |tox-ci-status| image:: https://github.com/shtalinberg/django-el-pagination/actions/workflows/tox.yml/badge.svg?branch=develop :target: https://github.com/shtalinberg/django-el-pagination/actions/workflows/tox.yml :alt: Tox develop status ================================================ FILE: doc/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoEndlessPagination.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoEndlessPagination.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoEndlessPagination" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoEndlessPagination" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: doc/_static/TRACKME ================================================ Placeholder file to let Hg include this directory. ================================================ FILE: doc/changelog.rst ================================================ Changelog ========= Unreleased ~~~~~~~~~~ **New feature**: Django 5.2.x support. Django EL(Endless) Pagination now supports Django from 4.2.x to 5.2.x Version 4.2.0 ~~~~~~~~~~~~~ **New feature**: Django 5.1.x support. Django EL(Endless) Pagination now supports Django from 4.2.x to 5.1.x supports Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 (only 5.1.x) **Source code validation**: add black formatting rules add pylint checks Version 4.1.2 ~~~~~~~~~~~~~ **Fix**: in 4.1.1 problem with loading template_tag Version 4.1.1 ~~~~~~~~~~~~~ **Fix**: fixed readthedocs documentation Version 4.1.0 ~~~~~~~~~~~~~ **New feature**: Django 4.2.x, 5.x support. Django EL(Endless) Pagination now supports Django 3.2.x, 4.2.x and 5.0 supports Python Django Python versions 3.2 3.8, 3.9, 3.10 (added in 3.2.9) 4.2 3.8, 3.9, 3.10, 3.11, 3.12 (added in 4.2.8) 5.0 3.10, 3.11, 3.12 Version 4.0.0 ~~~~~~~~~~~~~ **New feature**: Django 4.1.x support. Django EL(Endless) Pagination now supports Django from 3.2.x to 4.1.x supports Python 3.8, 3.9, 3.10 Version 3.3.0 ~~~~~~~~~~~~~ **New feature**: Django 3.0.x support. Django EL(Endless) Pagination now supports Django from 1.11.x to 3.0.x Dropped support for Python 2.x Version 3.2.4 ~~~~~~~~~~~~~ **Fix**: compatible with jQuery 3.x Version 3.2.3 ~~~~~~~~~~~~~ Bug-fix release **Fix**: cycle in show_pages with django 2.0 fix tests for PageList.get_rendered() Version 3.2.2 ~~~~~~~~~~~~~ Bug-fix release **Fix**: fix UnicodeEncodeError with translate in templates Version 3.2.0 ~~~~~~~~~~~~~ **New feature**: Django 2.0.x support. Django EL(Endless) Pagination now supports Django from 1.8.x to 2.0.x **New feature**: settings.USE_NEXT_PREVIOUS_LINKS: default=False if True: Add `is_previous` & `is_next` flags for `previous` and `next` pages Add `next_link.html` & `previous_link.html` templates **New feature**: `__unicode__` is removed from class ELPage It's Fix Causes Fatal Python error with django-debug-toolbar In templates: - {{ page }} now use as {{ page.render_link }} - {{ pages }} now use as {{ pages.get_rendered }} **Template changes**: show_pages.html: `page|default_if_none` replaced `page.render_link|default` ---- **Cleanup**: utils.UnicodeMixin utils.text Version 3.1.0 ~~~~~~~~~~~~~ **Template changes**: link attribute rel="{{ querystring_key }}" replaced by data-el-querystring-key="{{ querystring_key }}" **New feature**: Django 1.11 support. **New feature**: added view for maintaining original functionality on page index out of range, but setting response code to 404 ``PAGE_OUT_OF_RANGE_404`` default *False* If True on page out of range, throw a 404 exception, otherwise display the first page **Documentation**: render_to_response deprecated in django 1.10 replaced to ``return render(request, template, context)`` Version 3.0.0 ~~~~~~~~~~~~~ **New feature**: Django 1.10 support. New app Django EL(Endless) Pagination now supports Django from 1.8.x to 1.10 ---- **New feature**: Travic CI support add tox and Travic CI config ---- **Documentation**: general clean up. Version 2.1.1 ~~~~~~~~~~~~~ Bug-fix release **Fix**: page_template decorator doesn't change template of ajax call ---- **Fix**: Fix syntax error in declaring variable in javascript Version 2.1.0 ~~~~~~~~~~~~~ New name app: django-el-pagination **New feature**: Django 1.8 and 1.9 support. New app Django EL(Endless) Pagination now supports Django from 1.4.x to 1.9 new jQuery plugin that can be found in ``static/el-pagination/js/el-pagination.js``. Support get the numbers of objects are normally display in per page Usage: .. code-block:: html+django {{ pages.per_page_number }} add a class on chunk complete Each time a chunk size is complete, the class ``endless_chunk_complete`` is added to the *show more* link, Version 2.0 ~~~~~~~~~~~ **New feature**: Python 3 support. Django Endless Pagination now supports both Python 2 and **Python 3**. Dropped support for Python 2.5. See :doc:`start` for the new list of requirements. ---- **New feature**: the **JavaScript refactoring**. This version introduces a re-designed Ajax support for pagination. Ajax can now be enabled using a brand new jQuery plugin that can be found in ``static/el-pagination/js/el-pagination.js``. Usage: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} The last line in the block above enables Ajax requests to retrieve new pages for each pagination in the page. That's basically the same as the old approach of loading the file ``endless.js``. The new approach, however, is more jQuery-idiomatic, increases the flexibility of how objects can be paginated, implements some :doc:`new features ` and also contains some bug fixes. For backward compatibility, the application still includes the two JavaScript ``endless.js`` and ``endless_on_scroll.js`` files. However, please consider :ref:`migrating` as soon as possible: the old JavaScript files are deprecated, are no longer maintained, and don't provide the new JavaScript features. Also note that the old Javascript files will not work if jQuery >= 1.9 is used. New features include ability to **paginate different objects with different options**, precisely **selecting what to bind**, ability to **register callbacks**, support for **pagination in chunks** and much more. Please refer to the :doc:`javascript` for a detailed overview of the new features and for instructions on :ref:`how to migrate` from the old JavaScript files to the new one. ---- **New feature**: the :ref:`page_templates` decorator also accepts a sequence of ``(template, key)`` pairs, functioning as a dict mapping templates and keys (still present), e.g.:: from endless_pagination.decorators import page_templates @page_templates(( ('myapp/entries_page.html', None), ('myapp/other_entries_page.html', 'other_entries_page'), )) def entry_index(): ... This also supports serving different paginated objects with the same template. ---- **New feature**: ability to provide nested context variables in the :ref:`templatetags-paginate` and :ref:`templatetags-lazy-paginate` template tags, e.g.: .. code-block:: html+django {% paginate entries.all as myentries %} The code above is basically equivalent to: .. code-block:: html+django {% with entries.all as myentries %} {% paginate myentries %} {% endwith %} In this case, and only in this case, the `as` argument is mandatory, and a *TemplateSyntaxError* will be raised if the variable name is missing. ---- **New feature**: the page list object returned by the :ref:`templatetags-get-pages` template tag has been improved adding the following new methods: .. code-block:: html+django {# whether the page list contains more than one page #} {{ pages.paginated }} {# the 1-based index of the first item on the current page #} {{ pages.current_start_index }} {# the 1-based index of the last item on the current page #} {{ pages.current_end_index }} {# the total number of objects, across all pages #} {{ pages.total_count }} {# the first page represented as an arrow #} {{ pages.first_as_arrow }} {# the last page represented as an arrow #} {{ pages.last_as_arrow }} In the *arrow* representation, the page label defaults to ``<<`` for the first page and to ``>>`` for the last one. As a consequence, the labels of the previous and next pages are now single brackets, respectively ``<`` and ``>``. First and last pages' labels can be customized using ``settings.ENDLESS_PAGINATION_FIRST_LABEL`` and ``settings.ENDLESS_PAGINATION_LAST_LABEL``: see :doc:`customization`. ---- **New feature**: The sequence returned by the callable ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` can now contain two new values: - *'first'*: will display the first page as an arrow; - *'last'*: will display the last page as an arrow. The :ref:`templatetags-show-pages` template tag documentation describes how to customize Digg-style pagination defining your own page list callable. When using the default Digg-style pagination (i.e. when ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` is set to *None*), it is possible to enable first / last page arrows by setting the new flag ``settings.ENDLESS_PAGINATION_DEFAULT_CALLABLE_ARROWS`` to *True*. ---- **New feature**: ``settings.ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` can now be either a callable or a **dotted path** to a callable, e.g.:: ENDLESS_PAGINATION_PAGE_LIST_CALLABLE = 'path.to.callable' In addition to the default, ``endless_pagination.utils.get_page_numbers``, an alternative implementation is now available: ``endless_pagination.utils.get_elastic_page_numbers``. It adapts its output to the number of pages, making it arguably more usable when there are many of them. To enable it, add the following line to your ``settings.py``:: ENDLESS_PAGINATION_PAGE_LIST_CALLABLE = ( 'endless_pagination.utils.get_elastic_page_numbers') ---- **New feature**: ability to create a development and testing environment (see :doc:`contributing`). ---- **New feature**: in addition to the ability to provide a customized pagination URL as a context variable, the :ref:`templatetags-paginate` and :ref:`templatetags-lazy-paginate` tags now support hardcoded pagination URL endpoints, e.g.: .. code-block:: html+django {% paginate 20 entries with "/mypage/" %} ---- **New feature**: ability to specify negative indexes as values for the ``starting from page`` argument of the :ref:`templatetags-paginate` template tag. When changing the default page, it is now possible to reference the last page (or the second last page, and so on) by using negative indexes, e.g: .. code-block:: html+django {% paginate entries starting from page -1 %} See :doc:`templatetags_reference`. ---- **Documentation**: general clean up. ---- **Documentation**: added a :doc:`contributing` page. Have a look! ---- **Documentation**: included a comprehensive :doc:`javascript`. ---- **Fix**: ``endless_pagination.views.AjaxListView`` no longer subclasses ``django.views.generic.list.ListView``. Instead, the base objects and mixins composing the final view are now defined by this app. This change eliminates the ambiguity of having two separate pagination machineries in place: the Django Endless Pagination one and the built-in Django ``ListView`` one. ---- **Fix**: the *using* argument of :ref:`templatetags-paginate` and :ref:`templatetags-lazy-paginate` template tags now correctly handles querystring keys containing dashes, e.g.: .. code-block:: html+django {% lazy_paginate entries using "entries-page" %} ---- **Fix**: replaced namespace ``endless_pagination.paginator`` with ``endless_pagination.paginators``: the module contains more than one paginator classes. ---- **Fix**: in some corner cases, loading ``endless_pagination.models`` raised an *ImproperlyConfigured* error while trying to pre-load the templates. ---- **Fix**: replaced doctests with proper unittests. Improved the code coverage as a consequence. Also introduced integration tests exercising JavaScript, based on Selenium. ---- **Fix**: overall code lint and clean up. Version 1.1 ~~~~~~~~~~~ **New feature**: now it is possible to set the bottom margin used for pagination on scroll (default is 1 pixel). For example, if you want the pagination on scroll to be activated when 20 pixels remain until the end of the page: .. code-block:: html+django {# add the lines below #} ---- **New feature**: added ability to avoid Ajax requests when multiple pagination is used. A template for multiple pagination with Ajax support may look like this (see :doc:`multiple_pagination`): .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %}

Entries:

{% include "myapp/entries_page.html" %}

Other entries:

{% include "myapp/other_entries_page.html" %}
But what if you need Ajax pagination for *entries* but not for *other entries*? You will only have to add a class named ``endless_page_skip`` to the page container element, e.g.: .. code-block:: html+django

Other entries:

{% include "myapp/other_entries_page.html" %}
---- **New feature**: implemented a class-based generic view allowing Ajax pagination of a list of objects (usually a queryset). Intended as a substitution of *django.views.generic.ListView*, it recreates the behaviour of the *page_template* decorator. For a complete explanation, see :doc:`generic_views`. ---- **Fix**: the ``page_template`` and ``page_templates`` decorators no longer hide the original view name and docstring (*update_wrapper*). ---- **Fix**: pagination on scroll now works on Firefox >= 4. ---- **Fix**: tests are now compatible with Django 1.3. ================================================ FILE: doc/conf.py ================================================ """Django EL(Endless) Pagination documentation build configuration file.""" import os import sys sys.path.insert(0, os.path.abspath('..')) AUTHOR = 'Francesco Banconi, Oleksandr Shtalinberg' APP = 'Django EL(Endless) Pagination' TITLE = APP + ' Documentation' VERSION = '4.2.0' # General information about the project. project = APP copyright = '2009-2024, ' + AUTHOR author = 'Oleksandr Shtalinberg' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] exclude_patterns = [ '_build', 'Thumbs.db', '.DS_Store', '_static/TRACKME', # Exclude the TRACKME file from processing ] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The short X.Y version. version = release = VERSION # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' html_theme_options = { 'navigation_depth': 4, 'collapse_navigation': False, 'sticky_navigation': True, 'includehidden': True, 'titles_only': False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] epub_exclude_files = [ '_static/TRACKME', 'search.html', '_static/websupport.js', ] # -- Epub options --------------------------------------------------------- epub_show_urls = 'footnote' epub_tocdepth = 3 epub_tocdup = True epub_guide = (('toc', 'index.html', 'Table of Contents'),) # Output file base name for HTML help builder. htmlhelp_basename = 'DjangoELPaginationdoc' # Grouping the document tree into LaTeX files. List of tuples (source start # file, target name, title, author, documentclass [howto/manual]). latex_documents = [( 'index', 'DjangoELPagination.tex', TITLE, AUTHOR, 'manual')] # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [('index', 'djangoelpagination', TITLE, [AUTHOR], 1)] # Intersphinx configuration intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'django': ( 'https://docs.djangoproject.com/en/stable/', 'https://docs.djangoproject.com/en/stable/_objects/', ), } ================================================ FILE: doc/contacts.rst ================================================ Source code and contacts ======================== Repository and bugs ~~~~~~~~~~~~~~~~~~~ The **source code** for this app is hosted on https://github.com/shtalinberg/django-el-pagination. To file **bugs and requests**, please use https://github.com/shtalinberg/django-el-pagination/issues. Contacts ~~~~~~~~ Oleksandr Shtalinberg - Email: ``o.shtalinberg at gmail.com`` Francesco Banconi - Email: ``frankban at gmail.com`` - IRC: ``frankban@freenode`` ================================================ FILE: doc/contributing.rst ================================================ Contributing ============ Here are the steps needed to set up a development and testing environment. **WARNING** This app use *git flow* for branching strategy and release management. Please, change code and submit all pull requests into branch `develop` Creating a development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The development environment is created in a venv. The environment creation requires the *make* program to be installed. To install *make* under Debian/Ubuntu:: $ sudo apt-get install build-essential Under Mac OS/X, *make* is available as part of XCode. At this point, from the root of this branch, run the command:: $ make This command will create a ``.venv`` directory in the branch root, ignored by DVCSes, containing the development virtual environment with all the dependencies. Testing the application ~~~~~~~~~~~~~~~~~~~~~~~ To install *xvfb* (for integration tests) under Debian/Ubuntu:: $ sudo apt-get install xvfb If you are on CentOS and using yum, it's:: $ yum install xorg-X11-server-Xvfb Run the tests:: $ make test The command above also runs all tests except the available integration. They use Selenium and require Firefox to be installed. To include executing integration tests, define the environment variable USE_SELENIUM, e.g.:: $ make test USE_SELENIUM=1 Integration tests are excluded by default when using Python 3. The test suite requires Python >= 3.8.x. Run the tests and lint/pep8 checks:: $ make check Again, to exclude integration tests:: $ make check USE_SELENIUM=1 Debugging ~~~~~~~~~ Run the Django shell (Python interpreter):: $ make shell Run the Django development server for manual testing:: $ make server After executing the command above, it is possible to navigate the testing project going to . See all the available make targets, including info on how to create a Python 3 development environment:: $ make help Thanks for contributing, and have fun! ================================================ FILE: doc/current_page_number.rst ================================================ Getting the current page number =============================== In the template ~~~~~~~~~~~~~~~ You can get and display the current page number in the template using the :ref:`templatetags-show-current-number` template tag, e.g.: .. code-block:: html+django {% show_current_number %} This call will display the current page number, but you can also insert the value in the context as a template variable: .. code-block:: html+django {% show_current_number as page_number %} {{ page_number }} See the :ref:`templatetags-show-current-number` refrence for more information on accepted arguments. In the view ~~~~~~~~~~~ If you need to get the current page number in the view, you can use an utility function called ``get_page_number_from_request``, e.g.:: from el_pagination import utils page = utils.get_page_number_from_request(request) If you are using :doc:`multiple pagination`, or you have changed the default querystring for pagination, you can pass the querystring key as an optional argument:: page = utils.get_page_number_from_request(request, querystring_key=mykey) If the page number is not present in the request, by default *1* is returned. You can change this behaviour using:: page = utils.get_page_number_from_request(request, default=3) ================================================ FILE: doc/customization.rst ================================================ Customization ============= Settings ~~~~~~~~ You can customize the application using ``settings.py``. ================================================= =========== ============================================== Name Default Description ================================================= =========== ============================================== ``EL_PAGINATION_PER_PAGE`` 10 How many objects are normally displayed in a page (overwriteable by templatetag). ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_PAGE_LABEL`` 'page' The querystring key of the page number (e.g. ``http://example.com?page=2``). ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_ORPHANS`` 0 See Django *Paginator* definition of orphans. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_LOADING`` 'loading' If you use the default ``show_more`` template, here you can customize the content of the loader hidden element. HTML is safe here, e.g. you can show your pretty animated GIF ``EL_PAGINATION_LOADING = """loading"""``. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_PREVIOUS_LABEL`` '<' Default label for the *previous* page link. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_NEXT_LABEL`` '>' Default label for the *next* page link. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_FIRST_LABEL`` '<<' Default label for the *first* page link. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_LAST_LABEL`` '>>' Default label for the *last* page link. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_ADD_NOFOLLOW`` *False* Set to *True* if your SEO alchemist wants search engines not to follow pagination links. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_PAGE_LIST_CALLABLE`` *None* Callable (or dotted path to a callable) that returns pages to be displayed. If *None*, a default callable is used; that produces :doc:`digg_pagination`. The applicationt provides also a callable producing elastic pagination: ``EL_pagination.utils.get_elastic_page_numbers``. It adapts its output to the number of pages, making it arguably more usable when there are many of them. See :doc:`templatetags_reference` for information about writing custom callables. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_DEFAULT_CALLABLE_EXTREMES`` 3 Default number of *extremes* displayed when :doc:`digg_pagination` is used with the default callable. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_DEFAULT_CALLABLE_AROUNDS`` 2 Default number of *arounds* displayed when :doc:`digg_pagination` is used with the default callable. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_DEFAULT_CALLABLE_ARROWS`` *False* Whether or not the first and last pages arrows are displayed when :doc:`digg_pagination` is used with the default callable. ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_TEMPLATE_VARNAME`` 'template' Template variable name used by the ``page_template`` decorator. You can change this value if you are going to decorate generic views using a different variable name for the template (e.g. ``template_name``). ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_PAGE_OUT_OF_RANGE_404`` *False* If True on page out of range, throw a 404 exception, otherwise display the first page. There is a view that maintains the original functionality but sets the 404 status code found in el_pagination\\views.py ------------------------------------------------- ----------- ---------------------------------------------- ``EL_PAGINATION_USE_NEXT_PREVIOUS_LINKS`` *False* Add `is_previous` & `is_next` flags for `previous` and `next` pages ================================================= =========== ============================================== Templates and CSS ~~~~~~~~~~~~~~~~~ You can override the default template for ``show_more`` templatetag following some rules: - *more* link is shown only if the variable ``querystring`` is not False; - the container (most external html element) class is *endless_container*; - the *more* link and the loader hidden element live inside the container; - the *more* link class is *endless_more*; - the *more* link data-el-querystring-key attribute is ``{{ querystring_key }}``; - the loader hidden element class is *endless_loading*. ================================================ FILE: doc/different_first_page.rst ================================================ Different number of items on the first page =========================================== Sometimes you might want to show on the first page a different number of items than on subsequent pages (e.g. in a movie detail page you want to show 4 images of the movie as a reminder, making the user click to see the next 20 images). To achieve this, use the :ref:`templatetags-paginate` or :ref:`templatetags-lazy-paginate` tags with comma separated *first page* and *per page* arguments, e.g.: .. code-block:: html+django {% load el_pagination_tags %} {% lazy_paginate 4,20 entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_more %} This code will display 4 entries on the first page and 20 entries on the other pages. Of course the *first page* and *per page* arguments can be passed as template variables, e.g.: .. code-block:: html+django {% lazy_paginate first_page,per_page entries %} ================================================ FILE: doc/digg_pagination.rst ================================================ Digg-style pagination ===================== Digg-style pagination of queryset objects is really easy to implement. If Ajax pagination is not needed, all you have to do is modifying the template, e.g.: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} That's it! As seen, the :ref:`templatetags-paginate` template tag takes care of customizing the given queryset and the current template context. The :ref:`templatetags-show-pages` one displays the page links allowing for navigation to other pages. Page by page ~~~~~~~~~~~~ If you only want to display previous and next links (in a page-by-page pagination) you have to use the lower level :ref:`templatetags-get-pages` template tag, e.g.: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% get_pages %} {{ pages.previous }} {{ pages.next }} :doc:`customization` explains how to customize the arrows that go to previous and next pages. Showing indexes ~~~~~~~~~~~~~~~ The :ref:`templatetags-get-pages` template tag adds to the current template context a ``pages`` variable containing several methods that can be used to fully customize how the page links are displayed. For example, assume you want to show the indexes of the entries in the current page, followed by the total number of entries: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %}{% get_pages %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} Showing entries {{ pages.current_start_index }}-{{ pages.current_end_index }} of {{ pages.total_count }}. {# Just print pages to render the Digg-style pagination. #} {{ pages.get_rendered }} Number of pages ~~~~~~~~~~~~~~~ You can use ``{{ pages|length }}`` to retrieve and display the pages count. A common use case is to change the layout or display additional info based on whether the page list contains more than one page. This can be achieved checking ``{% if pages|length > 1 %}``, or, in a more convenient way, using ``{{ pages.paginated }}``. For example, assume you want to change the layout, or display some info, only if the page list contains more than one page, i.e. the results are actually paginated: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% get_pages %} {% if pages.paginated %} Some info/layout to display only if the available objects span multiple pages... {{ pages.get_rendered }} {% endif %} Again, for a full overview of the :ref:`templatetags-get-pages` and all the other template tags, see the :doc:`templatetags_reference`. .. _digg-ajax: Adding Ajax ~~~~~~~~~~~ The view is exactly the same as the one used in :ref:`Twitter-style Pagination`:: from el_pagination.decorators import page_template @page_template('myapp/entry_index_page.html') # just add this decorator def entry_index( request, template='myapp/entry_index.html', extra_context=None): context = { 'entries': Entry.objects.all(), } if extra_context is not None: context.update(extra_context) return render(request, template, context) As seen before in :doc:`twitter_pagination`, you have to :ref:`split the templates`, separating the main one from the fragment representing the single page. However, this time a container for the page template is also required and, by default, must be an element having a class named *endless_page_template*. *myapp/entry_index.html* becomes: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} *myapp/entry_index_page.html* becomes: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} Done. It is possible to manually :ref:`override the container selector` used by *$.endlessPaginate()* to update the page contents. This can be easily achieved by customizing the *pageSelector* option of *$.endlessPaginate()*, e.g.: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} See the :doc:`javascript` for a detailed explanation of how to integrate JavaScript and Ajax features in Django Endless Pagination. ================================================ FILE: doc/generic_views.rst ================================================ Generic views ============= This application provides a customized class-based view, similar to *django.views.generic.ListView*, that allows Ajax pagination of a list of objects (usually a queryset). AjaxListView reference ~~~~~~~~~~~~~~~~~~~~~~ .. py:module:: el_pagination.views .. py:class:: AjaxListView(django.views.generic.ListView) A class based view, similar to *django.views.generic.ListView*, that allows Ajax pagination of a list of objects. You can use this class based view in place of *ListView* in order to recreate the behaviour of the *page_template* decorator. For instance, assume you have this code (taken from Django docs):: from django.conf.urls import url from django.views.generic import ListView from books.models import Publisher urlpatterns = [ url(r'^publishers/$', ListView.as_view(model=Publisher)), ] You want to Ajax paginate publishers, so, as seen, you need to switch the template if the request is Ajax and put the page template into the context as a variable named *page_template*. This is straightforward, you only need to replace the view class, e.g.:: from django.conf.urls import * from books.models import Publisher from el_pagination.views import AjaxListView urlpatterns = [ url(r'^publishers/$', AjaxListView.as_view(model=Publisher)), ] .. py:attribute:: key the querystring key used for the current pagination (default: *settings.EL_PAGINATION_PAGE_LABEL*) .. py:attribute:: page_template the template used for the paginated objects .. py:attribute:: page_template_suffix the template suffix used for autogenerated page_template name (when not given, default='_page') .. py:method:: get_context_data(self, **kwargs) Adds the *page_template* variable in the context. If the *page_template* is not given as a kwarg of the *as_view* method then it is invented using app label, model name (obviously if the list is a queryset), *self.template_name_suffix* and *self.page_template_suffix*. For instance, if the list is a queryset of *blog.Entry*, the template will be *myapp/publisher_list_page.html*. .. py:method:: get_template_names(self) Switch the templates for Ajax requests. .. py:method:: get_page_template(self, **kwargs) Only called if *page_template* is not given as a kwarg of *self.as_view*. Generic view example ~~~~~~~~~~~~~~~~~~~~ If the developer wants pagination of publishers, in *views.py* we have code class-based:: from django.views.generic import ListView class EntryListView(ListView) model = Publisher template_name = "myapp/publisher_list.html" context_object_name = "publisher_list" or function-based:: def entry_index(request, template='myapp/publisher_list.html'): context = { 'publisher_list': Entry.objects.all(), } return render(request, template, context) In *myapp/publisher_list.html*: .. code-block:: html+django

Entries:

{% for entry in publisher_list %} {# your code to show the entry #} {% endfor %} This is just a basic example. To continue exploring more AjaxListView examples, have a look at :doc:`twitter_pagination` ================================================ FILE: doc/index.rst ================================================ ============================= Django EL(Endless) Pagination ============================= This application provides Twitter- and Digg-style pagination, with multiple and lazy pagination and optional Ajax support. It is devoted to implementing web pagination in very few steps. The **source code** for this app is hosted at https://github.com/shtalinberg/django-el-pagination. :doc:`start` is easy! Contents: .. toctree:: :maxdepth: 2 changelog start twitter_pagination digg_pagination multiple_pagination lazy_pagination different_first_page current_page_number templatetags_reference javascript generic_views customization contributing contacts thanks ================================================ FILE: doc/javascript.rst ================================================ JavaScript reference ==================== For each type of pagination it is possible to enable Ajax so that the requested page is loaded using an asynchronous request to the server. This is especially important for :doc:`twitter_pagination` and :ref:`endless pagination on scroll`, but :doc:`digg_pagination` can also take advantage of this technique. Activating Ajax support ~~~~~~~~~~~~~~~~~~~~~~~ Ajax support is activated linking jQuery and the ``el-pagination.js`` file included in this app. It is then possible to use the *$.endlessPaginate()* jQuery plugin to enable Ajax pagination, e.g.: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} This example assumes that you :ref:`separated the fragment` containing the single page (*page_tempate*) from the main template (the code snipper above). More on this in :doc:`twitter_pagination` and :doc:`digg_pagination`. The *$.endlessPaginate()* call activates Ajax for each pagination present in the page. .. _javascript-pagination-on-scroll: Pagination on scroll ~~~~~~~~~~~~~~~~~~~~ If you want new items to load when the user scrolls down the browser page, you can use the **pagination on scroll** feature: just set the *paginateOnScroll* option of *$.endlessPaginate()* to *true*, e.g.: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} That's all. See the :doc:`templatetags_reference` page to improve usage of the included templatetags. It is possible to set the **bottom margin** used for pagination on scroll (default is 1 pixel). For example, if you want the pagination on scroll to be activated when 20 pixels remain to the end of the page: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} Attaching callbacks ~~~~~~~~~~~~~~~~~~~ It is possible to customize the behavior of JavaScript pagination by attaching callbacks to *$.endlessPaginate()*, called when the following events are fired: - *onClick*: the user clicks on a page link; - *onCompleted*: the new page is fully loaded and inserted in the DOM. The context of both callbacks is the clicked link fragment: in other words, inside the callbacks, *this* will be the HTML fragment representing the clicked link, e.g.: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} Both callbacks also receive a *context* argument containing information about the requested page: - *context.url*: the requested URL; - *context.key*: the querystring key used to retrieve the requested contents. If the *onClick* callback returns *false*, the pagination process is stopped, the Ajax request is not performed and the *onCompleted* callback never called. The *onCompleted* callbacks also receives a second argument: the data returned by the server. Basically this is the HTML fragment representing the new requested page. To wrap it up, here is an example showing the callbacks' signatures: .. code-block:: html+django

Entries:

{% include page_template %}
{% block js %} {{ block.super }} {% endblock %} Manually selecting what to bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As seen above, *$.endlessPaginate()* enables Ajax support for each pagination in the page. But assuming you are using :doc:`multiple_pagination`, e.g.: .. code-block:: html+django

Entries:

{% include "myapp/entries_page.html" %}

Other entries:

{% include "myapp/other_entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} What if you need Ajax pagination only for *entries* and not for *other entries*? You can do this in a straightforward way using jQuery selectors, e.g.: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} The call to *$('#entries').endlessPaginate()* applies Ajax pagination starting from the DOM node with id *entries* and to all sub-nodes. This means that *other entries* are left intact. Of course you can use any selector supported by jQuery. At this point, you might have already guessed that *$.endlessPaginate()* is just an alias for *$('body').endlessPaginate()*. Customize each pagination ~~~~~~~~~~~~~~~~~~~~~~~~~ You can also call *$.endlessPaginate()* multiple times if you want to customize the behavior of each pagination. E.g. if you need to register a callback for *entries* but not for *other entries*: .. code-block:: html+django

Entries:

{% include "myapp/entries_page.html" %}

Other entries:

{% include "myapp/other_entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} .. _javascript-selectors: Selectors ~~~~~~~~~ Each time *$.endlessPaginate()* is used, several JavaScript selectors are used to select DOM nodes. Here is a list of them all: - containerSelector: '.endless_container' (Twitter-style pagination container selector); - loadingSelector: '.endless_loading' - (Twitter-style pagination loading selector); - moreSelector: 'a.endless_more' - (Twitter-style pagination link selector); - contentSelector: null - (Twitter-style pagination content wrapper); - pageSelector: '.endless_page_template' (Digg-style pagination page template selector); - pagesSelector: 'a.endless_page_link' (Digg-style pagination link selector). An example can better explain the meaning of the selectors above. Assume you have a Digg-style pagination like the following: .. code-block:: html+django

Entries:

{% include "myapp/entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} Here the ``#entries`` node is selected and Digg-style pagination is applied. Digg-style needs to know which DOM node will be updated with new contents, and in this case it's the same node we selected, because we added the *endless_page_template* class to that node, and *.endless_page_template* is the selector used by default. However, the following example is equivalent and does not involve adding another class to the container: .. code-block:: html+django

Entries:

{% include "myapp/entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} .. _javascript-chunks: On scroll pagination using chunks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sometimes, when using on scroll pagination, you may want to still display the *show more* link after each *N* pages. In Django Endless Pagination this is called *chunk size*. For instance, a chunk size of 5 means that a *show more* link is displayed after page 5 is loaded, then after page 10, then after page 15 and so on. Activating this functionality is straightforward, just use the *paginateOnScrollChunkSize* option: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} Each time a chunk size is complete, the class ``endless_chunk_complete`` is added to the *show more* link, so you still have a way to distinguish between the implicit click done by the scroll event and a real click on the button. .. _javascript-migrate: Migrate from version 1.1 to 2.1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django Endless Pagination v2.0 introduces changes in how Ajax pagination is handled by JavaScript. These changes are discussed in this document and in the :doc:`changelog`. The JavaScript code now lives in a file named ``el-pagination.js``. The two JavaScript files ``el-pagination-endless.js`` and ``el-pagination_on_scroll.js`` was removed. However, please consider migrating: the old JavaScript files was removed, are no longer maintained, and don't provide the new JavaScript features. Instructions on how to migrate from the old version to the new one follow. Basic migration --------------- Before: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Now: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Pagination on scroll -------------------- Before: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Now: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Pagination on scroll with customized bottom margin -------------------------------------------------- Before: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Now: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Avoid enabling Ajax on one or more paginations ---------------------------------------------- Before: .. code-block:: html+django

Other entries:

{% include "myapp/other_entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} Now: .. code-block:: html+django

Other entries:

{% include "myapp/other_entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} In this last example, activating Ajax just where you want might be preferred over excluding nodes. ================================================ FILE: doc/lazy_pagination.rst ================================================ Lazy pagination =============== Usually pagination requires hitting the database to get the total number of items to display. Lazy pagination avoids this *select count* query and results in a faster page load, with a disadvantage: you won't know the total number of pages in advance. For this reason it is better to use lazy pagination in conjunction with :doc:`twitter_pagination` (e.g. using the :ref:`templatetags-show-more` template tag). In order to switch to lazy pagination you have to use the :ref:`templatetags-lazy-paginate` template tag instead of the :ref:`templatetags-paginate` one, e.g.: .. code-block:: html+django {% load el_pagination_tags %} {% lazy_paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_more %} The :ref:`templatetags-lazy-paginate` tag can take all the args of the :ref:`templatetags-paginate` one, with one exception: negative indexes can not be passed to the ``starting from page`` argument. ================================================ FILE: doc/multiple_pagination.rst ================================================ Multiple paginations in the same page ===================================== Sometimes it is necessary to show different types of paginated objects in the same page. In this case we have to associate a different querystring key to every pagination. Normally, the key used is the one specified in ``settings.ENDLESS_PAGINATION_PAGE_LABEL`` (see :doc:`customization`), but in the case of multiple pagination the application provides a simple way to override the settings. If you do not need Ajax, the only file you need to edit is the template. Here is an example with 2 different paginations (*entries* and *other_entries*) in the same page, but there is no limit to the number of different paginations in a page: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} {# "other_entries_page" is the new querystring key #} {% paginate other_entries using "other_entries_page" %} {% for entry in other_entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} The ``using`` argument of the :ref:`templatetags-paginate` template tag allows you to choose the name of the querystring key used to track the page number. If not specified the system falls back to ``settings.EL_PAGINATION_PAGE_LABEL``. In the example above, the url *http://example.com?page=2&other_entries_page=3* requests the second page of *entries* and the third page of *other_entries*. The name of the querystring key can also be dinamically passed in the template context, e.g.: .. code-block:: html+django {# page_variable is not surrounded by quotes #} {% paginate other_entries using page_variable %} You can use any style of pagination: :ref:`templatetags-show-pages`, :ref:`templatetags-get-pages`, :ref:`templatetags-show-more` etc... (see :doc:`templatetags_reference`). Adding Ajax for multiple pagination ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Obviously each pagination needs a template for the page contents. Remember to box each page in a div with a class called *endless_page_template*, or to specify the container selector passing an option to *$.endlessPaginate()* as seen in :ref:`Digg-style pagination and Ajax`. *myapp/entry_index.html*: .. code-block:: html+django

Entries:

{% include "myapp/entries_page.html" %}

Other entries:

{% include "myapp/other_entries_page.html" %}
{% block js %} {{ block.super }} {% endblock %} See the :doc:`javascript` for further details on how to use the included jQuery plugin. *myapp/entries_page.html*: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} *myapp/other_entries_page.html*: .. code-block:: html+django {% load el_pagination_tags %} {% paginate other_entries using other_entries_page %} {% for entry in other_entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} As seen :ref:`before`, the decorator ``page_template`` simplifies the management of Ajax requests in views. You must, however, map different paginations to different page templates. You can chain decorator calls relating a template to the associated querystring key, e.g.:: from endless_pagination.decorators import page_template @page_template('myapp/entries_page.html') @page_template('myapp/other_entries_page.html', key='other_entries_page') def entry_index( request, template='myapp/entry_index.html', extra_context=None): context = { 'entries': Entry.objects.all(), 'other_entries': OtherEntry.objects.all(), } if extra_context is not None: context.update(extra_context) return render_to_response( template, context, context_instance=RequestContext(request)) As seen in previous examples, if you do not specify the *key* kwarg in the decorator, then the page template is associated to the querystring key defined in the settings. .. _multiple-page-templates: You can use the ``page_templates`` (note the trailing *s*) decorator in substitution of a decorator chain when you need multiple Ajax paginations. The previous example can be written as:: from endless_pagination.decorators import page_templates @page_templates({ 'myapp/entries_page.html': None, 'myapp/other_entries_page.html': 'other_entries_page', }) def entry_index(): ... As seen, a dict object is passed to the ``page_templates`` decorator, mapping templates to querystring keys. Alternatively, you can also pass a sequence of ``(template, key)`` pairs, e.g.:: from endless_pagination.decorators import page_templates @page_templates(( ('myapp/entries_page.html', None), ('myapp/other_entries_page.html', 'other_entries_page'), )) def entry_index(): ... This also supports serving different paginated objects with the same template. Manually selecting what to bind ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ What if you need Ajax pagination only for *entries* and not for *other entries*? You can do this in a straightforward way using jQuery selectors, e.g.: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} The call to *$('#entries').endlessPaginate()* applies Ajax pagination starting from the DOM node with id *entries* and to all sub-nodes. This means that *other entries* are left intact. Of course you can use any selector supported by jQuery. Refer to the :doc:`javascript` for an explanation of other features like calling *$.endlessPaginate()* multiple times in order to customize the behavior of each pagination in a multiple pagination view. ================================================ FILE: doc/requirements.txt ================================================ # Documentation dependencies sphinx>=5.0.0,<6.0.0 sphinx-rtd-theme>=1.0.0 sphinxcontrib-applehelp>=1.0.4 sphinxcontrib-devhelp>=1.0.2 sphinxcontrib-htmlhelp>=2.0.0 sphinxcontrib-serializinghtml>=1.1.5 sphinxcontrib-qthelp>=1.0.3 docutils>=0.17.1 jinja2>=3.0.0 # Project dependencies django>=3.2.0 ================================================ FILE: doc/start.rst ================================================ Getting started =============== Requirements ~~~~~~~~~~~~ ====== ==================== Python >= 3.8 Django >= 3.2 jQuery >= 1.11.1 ====== ==================== Installation ~~~~~~~~~~~~ The Git repository can be cloned with this command:: git clone https://github.com/shtalinberg/django-el-pagination.git The ``el_pagination`` package, included in the distribution, should be placed on the ``PYTHONPATH``. Otherwise you can just ``easy_install -Z django-el-pagination`` or ``pip install django-el-pagination``. Settings ~~~~~~~~ Add the request context processor to your *settings.py*, e.g.: .. code-block:: python from django.conf.global_settings import TEMPLATES TEMPLATES[0]['OPTIONS']['context_processors'].insert(0, 'django.core.context_processors.request') or just adding it to the context_processors manually like so: .. code-block:: python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ '...', '...', '...', '...', 'django.template.context_processors.request', ## For EL-pagination ], }, }, ] Add ``'el_pagination'`` to the ``INSTALLED_APPS`` to your *settings.py*. See the :doc:`customization` section for other settings. Quickstart ~~~~~~~~~~ Given a template like this: .. code-block:: html+django {% for entry in entries %} {# your code to show the entry #} {% endfor %} you can use Digg-style pagination to display objects just by adding: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entries %} {% for entry in entries %} {# your code to show the entry #} {% endfor %} {% show_pages %} Done. This is just a basic example. To continue exploring all the Django Endless Pagination features, have a look at :doc:`twitter_pagination` or :doc:`digg_pagination`. ================================================ FILE: doc/templatetags_reference.rst ================================================ Templatetags reference ====================== .. _templatetags-paginate: paginate ~~~~~~~~ Usage: .. code-block:: html+django {% paginate entries %} After this call, the *entries* variable in the template context is replaced by only the entries of the current page. You can also keep your *entries* original variable (usually a queryset) and add to the context another name that refers to entries of the current page, e.g.: .. code-block:: html+django {% paginate entries as page_entries %} The *as* argument is also useful when a nested context variable is provided as queryset. In this case, and only in this case, the resulting variable name is mandatory, e.g.: .. code-block:: html+django {% paginate entries.all as entries %} The number of paginated entries is taken from settings, but you can override the default locally, e.g.: .. code-block:: html+django {% paginate 20 entries %} Of course you can mix it all: .. code-block:: html+django {% paginate 20 entries as paginated_entries %} By default, the first page is displayed the first time you load the page, but you can change this, e.g.: .. code-block:: html+django {% paginate entries starting from page 3 %} When changing the default page, it is also possible to reference the last page (or the second last page, and so on) by using negative indexes, e.g: .. code-block:: html+django {% paginate entries starting from page -1 %} This can be also achieved using a template variable that was passed to the context, e.g.: .. code-block:: html+django {% paginate entries starting from page page_number %} If the passed page number does not exist, the first page is displayed. Note that negative indexes are specific to the ``{% paginate %}`` tag: this feature cannot be used when contents are lazy paginated (see `lazy_paginate`_ below). If you have multiple paginations in the same page, you can change the querydict key for the single pagination, e.g.: .. code-block:: html+django {% paginate entries using article_page %} In this case *article_page* is intended to be a context variable, but you can hardcode the key using quotes, e.g.: .. code-block:: html+django {% paginate entries using 'articles_at_page' %} Again, you can mix it all (the order of arguments is important): .. code-block:: html+django {% paginate 20 entries starting from page 3 using page_key as paginated_entries %} Additionally you can pass a path to be used for the pagination: .. code-block:: html+django {% paginate 20 entries using page_key with pagination_url as paginated_entries %} This way you can easily create views acting as API endpoints, and point your Ajax calls to that API. In this case *pagination_url* is considered a context variable, but it is also possible to hardcode the URL, e.g.: .. code-block:: html+django {% paginate 20 entries with "/mypage/" %} If you want the first page to contain a different number of items than subsequent pages, you can separate the two values with a comma, e.g. if you want 3 items on the first page and 10 on other pages: .. code-block:: html+django {% paginate 3,10 entries %} You must use this tag before calling the `show_more`_, `get_pages`_ or `show_pages`_ ones. .. _templatetags-lazy-paginate: lazy_paginate ~~~~~~~~~~~~~ Paginate objects without hitting the database with a *select count* query. Usually pagination requires hitting the database to get the total number of items to display. Lazy pagination avoids this *select count* query and results in a faster page load, with a disadvantage: you won't know the total number of pages in advance. Use this in the same way as `paginate`_ tag when you are not interested in the total number of pages. The ``lazy_paginate`` tag can take all the args of the ``paginate`` one, with one exception: negative indexes can not be passed to the ``starting from page`` argument. .. _templatetags-show-more: show_more ~~~~~~~~~ Show the link to get the next page in a :doc:`twitter_pagination`. Usage: .. code-block:: html+django {% show_more %} Alternatively you can override the label passed to the default template: .. code-block:: html+django {% show_more "even more" %} You can override the loading text too: .. code-block:: html+django {% show_more "even more" "working" %} Must be called after `paginate`_ or `lazy_paginate`_. .. _templatetags-show-more-table: show_more_table ~~~~~~~~~~~~~~~ Same as the `show_more`_, but for table pagination. Usage: .. code-block:: html+django {% show_more_table %} If use table in a :doc:`twitter_pagination`: .. code-block:: html+django {% include page_template %}
then page template: .. code-block:: html+django {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %} {{ object.title }} {% endfor %} {% show_more_table "More results" %} For :doc:`digg_pagination` use instead `show_more_table` in page template: .. code-block:: html+django {% show_pages %} .. _templatetags-get-pages: get_pages ~~~~~~~~~ Usage: .. code-block:: html+django {% get_pages %} This is mostly used for :doc:`digg_pagination`. This call inserts in the template context a *pages* variable, as a sequence of page links. You can use *pages* in different ways: - just print *pages* and you will get Digg-style pagination displayed: .. code-block:: html+django {{ pages.get_rendered }} - display pages count: .. code-block:: html+django {{ pages|length }} - display numbers of objects in per page: .. code-block:: html+django {{ pages.per_page_number }} - check if the page list contains more than one page: .. code-block:: html+django {{ pages.paginated }} {# the following is equivalent #} {{ pages|length > 1 }} - get a specific page: .. code-block:: html+django {# the current selected page #} {{ pages.current }} {# the first page #} {{ pages.first }} {# the last page #} {{ pages.last }} {# the previous page (or nothing if you are on first page) #} {{ pages.previous }} {# the next page (or nothing if you are in last page) #} {{ pages.next }} {# the third page #} {{ pages.3 }} {# this means page.1 is the same as page.first #} {# the 1-based index of the first item on the current page #} {{ pages.current_start_index }} {# the 1-based index of the last item on the current page #} {{ pages.current_end_index }} {# the total number of objects, across all pages #} {{ pages.total_count }} {# the first page represented as an arrow #} {{ pages.first_as_arrow }} {# the last page represented as an arrow #} {{ pages.last_as_arrow }} - iterate over *pages* to get all pages: .. code-block:: html+django {% for page in pages %} {# display page link #} {{ page.render_link }} {# the page url (beginning with "?") #} {{ page.url }} {# the page path #} {{ page.path }} {# the page number #} {{ page.number }} {# a string representing the page (commonly the page number) #} {{ page.label }} {# check if the page is the current one #} {{ page.is_current }} {# check if the page is the first one #} {{ page.is_first }} {# check if the page is the last one #} {{ page.is_last }} {### next two example work only with settings.EL_PAGINATION_USE_NEXT_PREVIOUS_LINKS = True ###} {# check if the page is previous #} {{ page.is_previous }} {# check if the page is_next #} {{ page.is_next }} {% endfor %} You can change the variable name, e.g.: .. code-block:: html+django {% get_pages as page_links %} {{ page_links.get_rendered }} {# the current selected page #} {{ page_links.current }} This must be called after `paginate`_ or `lazy_paginate`_. .. _templatetags-show-pages: show_pages ~~~~~~~~~~ Show page links. Usage: .. code-block:: html+django {% show_pages %} It is just a shortcut for: .. code-block:: html+django {% get_pages %} {{ pages.get_rendered }} You can set ``EL_PAGINATION_PAGE_LIST_CALLABLE`` in your *settings.py* to a callable used to customize the pages that are displayed. ``EL_PAGINATION_PAGE_LIST_CALLABLE`` can also be a dotted path representing a callable, e.g.:: EL_PAGINATION_PAGE_LIST_CALLABLE = 'path.to.callable' The callable takes the current page number and the total number of pages, and must return a sequence of page numbers that will be displayed. The sequence can contain other values: - *'previous'*: will display the previous page in that position; - *'next'*: will display the next page in that position; - *'first'*: will display the first page as an arrow; - *'last'*: will display the last page as an arrow; - *None*: a separator will be displayed in that position. Here is an example of a custom callable that displays the previous page, then the first page, then a separator, then the current page, and finally the last page:: def get_page_numbers(current_page, num_pages): return ('previous', 1, None, current_page, 'last') If ``EL_PAGINATION_PAGE_LIST_CALLABLE`` is *None* the internal callable ``endless_pagination.utils.get_page_numbers`` is used, generating a Digg-style pagination. An alternative implementation is available: ``endless_pagination.utils.get_elastic_page_numbers``: it adapts its output to the number of pages, making it arguably more usable when there are many of them. This must be called after `paginate`_ or `lazy_paginate`_. .. _templatetags-show-current-number: show_current_number ~~~~~~~~~~~~~~~~~~~ Show the current page number, or insert it in the context. This tag can for example be useful to change the page title according to the current page number. To just show current page number: .. code-block:: html+django {% show_current_number %} If you use multiple paginations in the same page, you can get the page number for a specific pagination using the querystring key, e.g.: .. code-block:: html+django {% show_current_number using mykey %} The default page when no querystring is specified is 1. If you changed it in the `paginate`_ template tag, you have to call ``show_current_number`` according to your choice, e.g.: .. code-block:: html+django {% show_current_number starting from page 3 %} This can be also achieved using a template variable you passed to the context, e.g.: .. code-block:: html+django {% show_current_number starting from page page_number %} You can of course mix it all (the order of arguments is important): .. code-block:: html+django {% show_current_number starting from page 3 using mykey %} If you want to insert the current page number in the context, without actually displaying it in the template, use the *as* argument, i.e.: .. code-block:: html+django {% show_current_number as page_number %} {% show_current_number starting from page 3 using mykey as page_number %} ================================================ FILE: doc/thanks.rst ================================================ Thanks ====== This application was initially inspired by the excellent tool *django-pagination* (see https://github.com/ericflo/django-pagination). Thanks to Francesco Banconi for improving previous version of this application (django-endless-pagination) Thanks to Jannis Leidel for improving the application with some new features, and for contributing the German translation. And thanks to Nicola 'tekNico' Larosa for reviewing the documentation and for implementing the elastic pagination feature. ================================================ FILE: doc/twitter_pagination.rst ================================================ Twitter-style Pagination ======================== Assuming the developer wants Twitter-style pagination of entries of a blog post, in *views.py* we have class-based:: from el_pagination.views import AjaxListView class EntryListView(AjaxListView): context_object_name = "entry_list" template_name = "myapp/entry_list.html" def get_queryset(self): return Entry.objects.all() or function-based:: def entry_index(request, template='myapp/entry_list.html'): context = { 'entry_list': Entry.objects.all(), } return render(request, template, context) In *myapp/entry_list.html*: .. code-block:: html+django

Entries:

{% for entry in entry_list %} {# your code to show the entry #} {% endfor %} .. _twitter-split-template: Split the template ~~~~~~~~~~~~~~~~~~ The response to an Ajax request should not return the entire template, but only the portion of the page to be updated or added. So it is convenient to extract from the template the part containing the entries, and use it to render the context if the request is Ajax. The main template will include the extracted part, so it is convenient to put the page template name in the context. *views.py* class-based becomes:: from el_pagination.views import AjaxListView class EntryListView(AjaxListView): context_object_name = "entry_list" template_name = "myapp/entry_list.html" page_template='myapp/entry_list_page.html' def get_queryset(self): return Entry.objects.all() or function-based:: def entry_list(request, template='myapp/entry_list.html', page_template='myapp/entry_list_page.html'): context = { 'entry_list': Entry.objects.all(), 'page_template': page_template, } if request.headers.get('x-requested-with') == 'XMLHttpRequest': template = page_template return render(request, template, context) See :ref:`below` how to obtain the same result **just decorating the view**. *myapp/entry_list.html* becomes: .. code-block:: html+django

Entries:

{% include page_template %} *myapp/entry_list_page.html* becomes: .. code-block:: html+django {% for entry in entry_list %} {# your code to show the entry #} {% endfor %} .. _twitter-page-template: A shortcut for ajaxed views ~~~~~~~~~~~~~~~~~~~~~~~~~~~ A good practice in writing views is to allow other developers to inject the template name and extra data, so that they are added to the context. This allows the view to be easily reused. Let's resume the original view with extra context injection: *views.py*:: def entry_index(request, template='myapp/entry_list.html', extra_context=None): context = { 'entry_list': Entry.objects.all(), } if extra_context is not None: context.update(extra_context) return render(request, template, context) Splitting templates and putting the Ajax template name in the context is easily achievable by using an included decorator. *views.py* becomes:: from el_pagination.decorators import page_template @page_template('myapp/entry_list_page.html') # just add this decorator def entry_list(request, template='myapp/entry_list.html', extra_context=None): context = { 'entry_list': Entry.objects.all(), } if extra_context is not None: context.update(extra_context) return render(request, template, context) Paginating objects ~~~~~~~~~~~~~~~~~~ All that's left is changing the page template and loading the :doc:`endless templatetags`, the jQuery library and the jQuery plugin ``el-pagination.js`` included in the distribution under ``/static/el-pagination/js/``. *myapp/entry_list.html* becomes: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} *myapp/entry_list_page.html* becomes: .. code-block:: html+django {% load el_pagination_tags %} {% paginate entry_list %} {% for entry in entry_list %} {# your code to show the entry #} {% endfor %} {% show_more %} The :ref:`templatetags-paginate` template tag takes care of customizing the given queryset and the current template context. In the context of a Twitter-style pagination the :ref:`templatetags-paginate` tag is often replaced by the :ref:`templatetags-lazy-paginate` one, which offers, more or less, the same functionalities and allows for reducing database access: see :doc:`lazy_pagination`. The :ref:`templatetags-show-more` one displays the link to navigate to the next page. You might want to glance at the :doc:`javascript` for a detailed explanation of how to integrate JavaScript and Ajax features in Django Endless Pagination. Pagination on scroll ~~~~~~~~~~~~~~~~~~~~ If you want new items to load when the user scroll down the browser page, you can use the :ref:`pagination on scroll` feature: just set the *paginateOnScroll* option of *$.endlessPaginate()* to *true*, e.g.: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} That's all. See the :doc:`templatetags_reference` to improve the use of included templatetags. It is possible to set the bottom margin used for :ref:`pagination on scroll` (default is 1 pixel). For example, if you want the pagination on scroll to be activated when 20 pixels remain to the end of the page: .. code-block:: html+django

Entries:

{% include page_template %} {% block js %} {{ block.super }} {% endblock %} Again, see the :doc:`javascript`. On scroll pagination using chunks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sometimes, when using on scroll pagination, you may want to still display the *show more* link after each *N* pages. In Django Endless Pagination this is called *chunk size*. For instance, a chunk size of 5 means that a *show more* link is displayed after page 5 is loaded, then after page 10, then after page 15 and so on. Activating :ref:`chunks` is straightforward, just use the *paginateOnScrollChunkSize* option: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} Specifying where the content will be inserted ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are paginating a table, you can use :ref:`templatetags-show-more-table` or you may want to include the *show_more* link after the table itself, but the loaded content should be placed inside the table. For any case like this, you may specify the *contentSelector* option that points to the element that will wrap the cumulative data: .. code-block:: html+django {% block js %} {{ block.super }} {% endblock %} .. note:: By default, the contentSelector is null, making each new page be inserted before the *show_more* link container. When using this approach, you should take 2 more actions. At first, the page template must be splitted a little different. You must do the pagination in the main template and only apply pagination in the page template if under ajax: *myapp/entry_list.html* becomes: .. code-block:: html+django

Entries:

{% paginate entry_list %}
    {% include page_template %}
{% show_more %} {% block js %} {{ block.super }} {% endblock %} *myapp/entry_list_page.html* becomes: .. code-block:: html+django {% load el_pagination_tags %} {% if request.is_ajax %}{% paginate entry_list %}{% endif %} {% for entry in entry_list %} {# your code to show the entry #} {% endfor %} This is needed because the *show_more* button now is taken off the page_template and depends of the *paginate* template tag. To avoid apply pagination twice, we avoid run it a first time in the page_template. You may also set the *EL_PAGINATION_PAGE_OUT_OF_RANGE_404* to True, so a blank page wouldn't render the first page (the default behavior). When a blank page is loaded and propagates the 404 error, the *show_more* link is removed. Before version 2.0 ~~~~~~~~~~~~~~~~~~ Django Endless Pagination v2.0 introduces a redesigned Ajax support for pagination. As seen above, Ajax can now be enabled using a brand new jQuery plugin that can be found in ``static/el-pagination/js/el-pagination.js``. Old code was removed: .. code-block:: html+django {# new jQuery plugin #} {# Removed. #} However, please consider :ref:`migrating` as soon as possible: the old JavaScript files are removed. Please refer to the :doc:`javascript` for a detailed overview of the new features and for instructions on :ref:`how to migrate` from the old JavaScript files to the new one. ================================================ FILE: el_pagination/__init__.py ================================================ """Django pagination tools supporting Ajax, multiple and lazy pagination, Twitter-style and Digg-style pagination. """ VERSION = (4, 1, 2) __version__ = '.'.join(map(str, VERSION)) def get_version(): """Return the Django EL Pagination version as a string.""" return __version__ ================================================ FILE: el_pagination/decorators.py ================================================ """View decorators for Ajax powered pagination.""" from functools import wraps from el_pagination.settings import PAGE_LABEL, TEMPLATE_VARNAME QS_KEY = "querystring_key" def page_template(template, key=PAGE_LABEL): """Return a view dynamically switching template if the request is Ajax. Decorate a view that takes a *template* and *extra_context* keyword arguments (like generic views). The template is switched to *page_template* if request is ajax and if *querystring_key* variable passed by the request equals to *key*. This allows multiple Ajax paginations in the same page. The name of the page template is given as *page_template* in the extra context. """ def decorator(view): @wraps(view) def decorated(request, *args, **kwargs): # Trust the developer: he wrote ``context.update(extra_context)`` # in his view. extra_context = kwargs.setdefault("extra_context", {}) extra_context["page_template"] = template # Switch the template when the request is Ajax. querystring_key = request.GET.get( QS_KEY, request.POST.get(QS_KEY, PAGE_LABEL) ) if ( request.headers.get("x-requested-with") == "XMLHttpRequest" and querystring_key == key ): kwargs[TEMPLATE_VARNAME] = template return view(request, *args, **kwargs) return decorated return decorator def _get_template(querystring_key, mapping): """Return the template corresponding to the given ``querystring_key``.""" default = None try: template_and_keys = mapping.items() except AttributeError: template_and_keys = mapping for template, key in template_and_keys: if key is None: key = PAGE_LABEL default = template if key == querystring_key: return template return default def page_templates(mapping): """Like the *page_template* decorator but manage multiple paginations. You can map multiple templates to *querystring_keys* using the *mapping* dict, e.g.:: @page_templates({ 'page_contents1.html': None, 'page_contents2.html': 'go_to_page', }) def myview(request): ... When the value of the dict is None then the default *querystring_key* (defined in settings) is used. You can use this decorator instead of chaining multiple *page_template* calls. """ def decorator(view): @wraps(view) def decorated(request, *args, **kwargs): # Trust the developer: he wrote ``context.update(extra_context)`` # in his view. extra_context = kwargs.setdefault("extra_context", {}) querystring_key = request.GET.get( QS_KEY, request.POST.get(QS_KEY, PAGE_LABEL) ) template = _get_template(querystring_key, mapping) extra_context["page_template"] = template # Switch the template when the request is Ajax. if request.headers.get("x-requested-with") == "XMLHttpRequest" and template: kwargs[TEMPLATE_VARNAME] = template return view(request, *args, **kwargs) return decorated return decorator ================================================ FILE: el_pagination/exceptions.py ================================================ """Pagination exceptions.""" class PaginationError(Exception): """Error in the pagination process.""" ================================================ FILE: el_pagination/loaders.py ================================================ """Django EL Pagination object loaders.""" from importlib import import_module from django.core.exceptions import ImproperlyConfigured def load_object(path): """Return the Python object represented by dotted *path*.""" i = path.rfind('.') module_name, object_name = path[:i], path[i + 1 :] # Load module. try: module = import_module(module_name) except ImportError as exc: raise ImproperlyConfigured(f'Module {module_name} not found') from exc except ValueError as exc: raise ImproperlyConfigured(f'Invalid module {module_name}') from exc # Load object. try: return getattr(module, object_name) except AttributeError as exc: msg = 'Module %r does not define an object named %r' raise ImproperlyConfigured(msg % (module_name, object_name)) from exc ================================================ FILE: el_pagination/locale/de/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2009. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2010-03-04 17:52+0100\n" "PO-Revision-Date: 2010-03-04 18:00+0100\n" "Last-Translator: Jannis Leidel \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: templates/endless/show_more.html:4 msgid "more" msgstr "mehr" ================================================ FILE: el_pagination/locale/es/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2013. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-02-11 18:03+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Francesco Banconi \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: templates/endless/show_more.html:5 msgid "more" msgstr "Más resultados" ================================================ FILE: el_pagination/locale/fr/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2013. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-02-11 16:06+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" #: templates/endless/show_more.html:5 msgid "more" msgstr "En voir plus" ================================================ FILE: el_pagination/locale/it/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2009. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-08-22 19:21+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Francesco Banconi \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/endless/show_more.html:4 msgid "more" msgstr "mostra altri" ================================================ FILE: el_pagination/locale/pt_BR/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2009. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-05-08 20:34-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Michel Sabchuk \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: templates/el_pagination/show_more.html:5 msgid "more" msgstr "mais" #: views.py:102 msgid "Empty list and ``%(class_name)s.allow_empty`` is False." msgstr "Lista vazia e ``%(class_name)s.allow_empty`` é Falso." ================================================ FILE: el_pagination/locale/zh_CN/LC_MESSAGES/django.po ================================================ # Django Endless Pagination Locale # Copyright (C) 2009-2013 Francesco Banconi # This file is distributed under the same license as the django-endless-pagination package. # Francesco Banconi , 2013. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-08-22 19:21+0200\n" "PO-Revision-Date: 2013-02-11 16:54+0800\n" "Last-Translator: mozillazg \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: templates/endless/show_more.html:4 msgid "more" msgstr "查看更多" ================================================ FILE: el_pagination/models.py ================================================ """Ephemeral models used to represent a page and a list of pages.""" from django.template import loader from django.utils.encoding import force_str, iri_to_uri from el_pagination import loaders, settings, utils # Page templates cache. _template_cache = {} class ELPage: """A page link representation. Interesting attributes: - *self.number*: the page number; - *self.label*: the label of the link (usually the page number as string); - *self.url*: the url of the page (strting with "?"); - *self.path*: the path of the page; - *self.is_current*: return True if page is the current page displayed; - *self.is_first*: return True if page is the first page; - *self.is_last*: return True if page is the last page. """ def __init__( self, request, number, current_number, total_number, querystring_key, label=None, default_number=1, override_path=None, context=None, ): self._request = request self.number = number self.label = force_str(number) if label is None else label self.querystring_key = querystring_key self.context = context or {} self.context['request'] = request self.is_current = number == current_number self.is_first = number == 1 self.is_last = number == total_number if settings.USE_NEXT_PREVIOUS_LINKS: self.is_previous = label and number == current_number - 1 self.is_next = label and number == current_number + 1 self.url = utils.get_querystring_for_page( request, number, self.querystring_key, default_number=default_number ) path = iri_to_uri(override_path or request.path) self.path = f"{path}{self.url}" def render_link(self): """Render the page as a link.""" extra_context = { 'add_nofollow': settings.ADD_NOFOLLOW, 'page': self, 'querystring_key': self.querystring_key, } if self.is_current: template_name = 'el_pagination/current_link.html' else: template_name = 'el_pagination/page_link.html' if settings.USE_NEXT_PREVIOUS_LINKS: if self.is_previous: template_name = 'el_pagination/previous_link.html' if self.is_next: template_name = 'el_pagination/next_link.html' if template_name not in _template_cache: _template_cache[template_name] = loader.get_template(template_name) template = _template_cache[template_name] with self.context.push(**extra_context): return template.render(self.context.flatten()) class PageList: """A sequence of endless pages.""" def __init__( self, request, page, querystring_key, context, default_number=None, override_path=None, ): self._request = request self._page = page self.context = context self.context['request'] = request if default_number is None: self._default_number = 1 else: self._default_number = int(default_number) self._querystring_key = querystring_key self._override_path = override_path self._pages_list = [] def _endless_page(self, number, label=None): """Factory function that returns a *ELPage* instance. This method works just like a partial constructor. """ return ELPage( self._request, number, self._page.number, len(self), self._querystring_key, label=label, default_number=self._default_number, override_path=self._override_path, context=self.context, ) def __getitem__(self, value): # The type conversion is required here because in templates Django # performs a dictionary lookup before the attribute lookups # (when a dot is encountered). try: value = int(value) except (TypeError, ValueError) as exc: # A TypeError says to django to continue with an attribute lookup. raise TypeError from exc if 1 <= value <= len(self): return self._endless_page(value) raise IndexError('page list index out of range') def __len__(self): """The length of the sequence is the total number of pages.""" return self._page.paginator.num_pages def __iter__(self): """Iterate over all the endless pages (from first to last).""" for i in range(len(self)): yield self[i + 1] def __str__(self): """Return a rendered Digg-style pagination (by default). The callable *settings.PAGE_LIST_CALLABLE* can be used to customize how the pages are displayed. The callable takes the current page number and the total number of pages, and must return a sequence of page numbers that will be displayed. The sequence can contain other values: - *'previous'*: will display the previous page in that position; - *'next'*: will display the next page in that position; - *'first'*: will display the first page as an arrow; - *'last'*: will display the last page as an arrow; - *None*: a separator will be displayed in that position. Here is an example of custom calable that displays the previous page, then the first page, then a separator, then the current page, and finally the last page:: def get_page_numbers(current_page, num_pages): return ('previous', 1, None, current_page, 'last') If *settings.PAGE_LIST_CALLABLE* is None an internal callable is used, generating a Digg-style pagination. The value of *settings.PAGE_LIST_CALLABLE* can also be a dotted path to a callable. """ return '' def get_pages_list(self): if not self._pages_list: callable_or_path = settings.PAGE_LIST_CALLABLE if callable_or_path: if callable(callable_or_path): pages_callable = callable_or_path else: pages_callable = loaders.load_object(callable_or_path) else: pages_callable = utils.get_page_numbers pages = [] for item in pages_callable(self._page.number, len(self)): if item is None: pages.append(None) elif item == 'previous': pages.append(self.previous()) elif item == 'next': pages.append(self.next()) elif item == 'first': pages.append(self.first_as_arrow()) elif item == 'last': pages.append(self.last_as_arrow()) else: pages.append(self[item]) self._pages_list = pages return self._pages_list def get_rendered(self): if len(self) > 1: template = loader.get_template('el_pagination/show_pages.html') with self.context.push(pages=self.get_pages_list()): return template.render(self.context.flatten()) return '' def current(self): """Return the current page.""" return self._endless_page(self._page.number) def current_start_index(self): """Return the 1-based index of the first item on the current page.""" return self._page.start_index() def current_end_index(self): """Return the 1-based index of the last item on the current page.""" return self._page.end_index() def total_count(self): """Return the total number of objects, across all pages.""" return self._page.paginator.count def first(self, label=None): """Return the first page.""" return self._endless_page(1, label=label) def last(self, label=None): """Return the last page.""" return self._endless_page(len(self), label=label) def first_as_arrow(self): """Return the first page as an arrow. The page label (arrow) is defined in ``settings.FIRST_LABEL``. """ return self.first(label=settings.FIRST_LABEL) def last_as_arrow(self): """Return the last page as an arrow. The page label (arrow) is defined in ``settings.LAST_LABEL``. """ return self.last(label=settings.LAST_LABEL) def previous(self): """Return the previous page. The page label is defined in ``settings.PREVIOUS_LABEL``. Return an empty string if current page is the first. """ if self._page.has_previous(): return self._endless_page( self._page.previous_page_number(), label=settings.PREVIOUS_LABEL ) return '' def next(self): """Return the next page. The page label is defined in ``settings.NEXT_LABEL``. Return an empty string if current page is the last. """ if self._page.has_next(): return self._endless_page( self._page.next_page_number(), label=settings.NEXT_LABEL ) return '' def paginated(self): """Return True if this page list contains more than one page.""" return len(self) > 1 def per_page_number(self): """Return the numbers of objects are normally display in per page.""" return self._page.paginator.per_page ================================================ FILE: el_pagination/paginators.py ================================================ """Customized Django paginators.""" from math import ceil from django.core.paginator import EmptyPage, Page, PageNotAnInteger, Paginator class CustomPage(Page): """Handle different number of items on the first page.""" def start_index(self): """Return the 1-based index of the first item on this page.""" paginator = self.paginator # Special case, return zero if no items. if paginator.count == 0: return 0 if self.number == 1: return 1 return (self.number - 2) * paginator.per_page + paginator.first_page + 1 def end_index(self): """Return the 1-based index of the last item on this page.""" paginator = self.paginator # Special case for the last page because there can be orphans. if self.number == paginator.num_pages: return paginator.count return (self.number - 1) * paginator.per_page + paginator.first_page class BasePaginator(Paginator): """A base paginator class subclassed by the other real paginators. Handle different number of items on the first page. """ def __init__(self, object_list, per_page, **kwargs): self._num_pages = None if 'first_page' in kwargs: self.first_page = kwargs.pop('first_page') else: self.first_page = per_page super().__init__(object_list, per_page, **kwargs) def get_current_per_page(self, number): return self.first_page if number == 1 else self.per_page class DefaultPaginator(BasePaginator): """The default paginator used by this application.""" def page(self, number): number = self.validate_number(number) if number == 1: bottom = 0 else: bottom = (number - 2) * self.per_page + self.first_page top = bottom + self.get_current_per_page(number) if top + self.orphans >= self.count: top = self.count return CustomPage(self.object_list[bottom:top], number, self) def _get_num_pages(self): if self._num_pages is None: if self.count == 0 and not self.allow_empty_first_page: self._num_pages = 0 else: hits = max(0, self.count - self.orphans - self.first_page) try: self._num_pages = int(ceil(hits / float(self.per_page))) + 1 except ZeroDivisionError: self._num_pages = 0 # fallback to a safe value return self._num_pages num_pages = property(_get_num_pages) class LazyPaginatorCustomPage(Page): """Handle different number of items on the first page.""" def start_index(self): """Return the 1-based index of the first item on this page.""" paginator = self.paginator if self.number == 1: return 1 return (self.number - 2) * paginator.per_page + paginator.first_page + 1 def end_index(self): """Return the 1-based index of the last item on this page.""" paginator = self.paginator return (self.number - 1) * paginator.per_page + paginator.first_page class LazyPaginator(BasePaginator): """Implement lazy pagination.""" def validate_number(self, number): try: number = int(number) except ValueError as exc: raise PageNotAnInteger('That page number is not an integer') from exc if number < 1: raise EmptyPage('That page number is less than 1') return number def page(self, number): number = self.validate_number(number) current_per_page = self.get_current_per_page(number) if number == 1: bottom = 0 else: bottom = (number - 2) * self.per_page + self.first_page top = bottom + current_per_page # Retrieve more objects to check if there is a next page. objects = list(self.object_list[bottom : top + self.orphans + 1]) objects_count = len(objects) if objects_count > (current_per_page + self.orphans): # If another page is found, increase the total number of pages. self._num_pages = number + 1 # In any case, return only objects for this page. objects = objects[:current_per_page] elif (number != 1) and (objects_count <= self.orphans): raise EmptyPage('That page contains no results') else: # This is the last page. self._num_pages = number return LazyPaginatorCustomPage(objects, number, self) def _get_count(self): raise NotImplementedError count = property(_get_count) def _get_num_pages(self): return self._num_pages num_pages = property(_get_num_pages) def _get_page_range(self): raise NotImplementedError page_range = property(_get_page_range) ================================================ FILE: el_pagination/settings.py ================================================ # """Django Endless Pagination settings file.""" from django.conf import settings # How many objects are normally displayed in a page # (overwriteable by templatetag). PER_PAGE = getattr(settings, 'EL_PAGINATION_PER_PAGE', 10) # The querystring key of the page number. PAGE_LABEL = getattr(settings, 'EL_PAGINATION_PAGE_LABEL', 'page') # See django *Paginator* definition of orphans. ORPHANS = getattr(settings, 'EL_PAGINATION_ORPHANS', 0) # If you use the default *show_more* template, here you can customize # the content of the loader hidden element. # Html is safe here, e.g. you can show your pretty animated gif: # EL_PAGINATION_LOADING = """ # loading # """ LOADING = getattr(settings, 'EL_PAGINATION_LOADING', 'loading') USE_NEXT_PREVIOUS_LINKS = getattr( settings, 'EL_PAGINATION_USE_NEXT_PREVIOUS_LINKS', False ) # Labels for previous and next page links. PREVIOUS_LABEL = getattr(settings, 'EL_PAGINATION_PREVIOUS_LABEL', '<') NEXT_LABEL = getattr(settings, 'EL_PAGINATION_NEXT_LABEL', '>') # Labels for first and last page links. FIRST_LABEL = getattr(settings, 'EL_PAGINATION_FIRST_LABEL', '<<') LAST_LABEL = getattr(settings, 'EL_PAGINATION_LAST_LABEL', '>>') # Set to True if your SEO alchemist wants all the links in Digg-style # pagination to be ``nofollow``. ADD_NOFOLLOW = getattr(settings, 'EL_PAGINATION_ADD_NOFOLLOW', False) # Callable (or dotted path to a callable) returning pages to be displayed. # If None, a default callable is used (which produces Digg-style pagination). PAGE_LIST_CALLABLE = getattr(settings, 'EL_PAGINATION_PAGE_LIST_CALLABLE', None) # The default callable returns a sequence of pages producing Digg-style # pagination, and depending on the settings below. DEFAULT_CALLABLE_EXTREMES = getattr( settings, 'EL_PAGINATION_DEFAULT_CALLABLE_EXTREMES', 3 ) DEFAULT_CALLABLE_AROUNDS = getattr( settings, 'EL_PAGINATION_DEFAULT_CALLABLE_AROUNDS', 2 ) # Whether or not the first and last pages arrows are displayed. DEFAULT_CALLABLE_ARROWS = getattr( settings, 'EL_PAGINATION_DEFAULT_CALLABLE_ARROWS', False ) # Template variable name for *page_template* decorator. TEMPLATE_VARNAME = getattr(settings, 'EL_PAGINATION_TEMPLATE_VARNAME', 'template') # If page out of range, throw a 404 exception PAGE_OUT_OF_RANGE_404 = getattr(settings, 'EL_PAGINATION_PAGE_OUT_OF_RANGE_404', False) ================================================ FILE: el_pagination/static/el-pagination/js/el-pagination.js ================================================ 'use strict'; (function ($) { $.fn.endlessPaginate = function(options) { var defaults = { // Twitter-style pagination container selector. containerSelector: '.endless_container', // Twitter-style pagination loading selector. loadingSelector: '.endless_loading', // Twitter-style pagination link selector. moreSelector: 'a.endless_more', // Twitter-style pagination content wrapper selector. contentSelector: null, // Digg-style pagination page template selector. pageSelector: '.endless_page_template', // Digg-style pagination link selector. pagesSelector: 'a.endless_page_link', // Callback called when the user clicks to get another page. onClick: function() {}, // Callback called when the new page is correctly displayed. onCompleted: function() {}, // Set this to true to use the paginate-on-scroll feature. paginateOnScroll: false, // If paginate-on-scroll is on, this margin will be used. paginateOnScrollMargin : 1, // If paginate-on-scroll is on, it is possible to define chunks. paginateOnScrollChunkSize: 0 }, settings = $.extend(defaults, options); var getContext = function(link) { return { key: link.data("el-querystring-key").split(' ')[0], url: link.attr('href') }; }; return this.each(function() { var element = $(this), loadedPages = 1; // Twitter-style pagination. element.on('click', settings.moreSelector, function() { var link = $(this), html_link = link.get(0), content_wrapper = element.find(settings.contentSelector), container = link.closest(settings.containerSelector), loading = container.find(settings.loadingSelector); // Avoid multiple Ajax calls. if (loading.is(':visible')) { return false; } link.hide(); loading.show(); var context = getContext(link); // Fire onClick callback. if (settings.onClick.apply(html_link, [context]) !== false) { var data = 'querystring_key=' + context.key; // Send the Ajax request. $.get(context.url, data, function (fragment) { // Increase the number of loaded pages. loadedPages += 1; if (!content_wrapper.length) { // Replace pagination container (the default behavior) container.before(fragment); container.remove(); } else { // Insert the content in the specified wrapper and increment link content_wrapper.append(fragment); var nextPage = 'page=' + (loadedPages + 1); link.attr('href', link.attr('href').replace(/page=\d+/, nextPage)); link.show(); loading.hide(); } // Fire onCompleted callback. settings.onCompleted.apply( html_link, [context, $.trim(fragment)]); }).fail(function (xhr, textStatus, error) { // Remove the container left if any container.remove(); }); } return false; }); // On scroll pagination. if (settings.paginateOnScroll) { var win = $(window), doc = $(document); doc.on('scroll', function () { if (doc.height() - win.height() - win.scrollTop() <= settings.paginateOnScrollMargin) { // Do not paginate on scroll if chunks are used and // the current chunk is complete. var chunckSize = settings.paginateOnScrollChunkSize; if (!chunckSize || loadedPages % chunckSize) { element.find(settings.moreSelector).trigger('click'); } else { element.find(settings.moreSelector).addClass('endless_chunk_complete'); } } }); } // Digg-style pagination. element.on('click', settings.pagesSelector, function() { var link = $(this), html_link = link.get(0), context = getContext(link); // Fire onClick callback. if (settings.onClick.apply(html_link, [context]) !== false) { var page_template = link.closest(settings.pageSelector), data = 'querystring_key=' + context.key; // Send the Ajax request. page_template.load(context.url, data, function(fragment) { // Fire onCompleted callback. settings.onCompleted.apply( html_link, [context, $.trim(fragment)]); }); } return false; }); }); }; $.endlessPaginate = function(options) { return $('body').endlessPaginate(options); }; })(jQuery); ================================================ FILE: el_pagination/templates/el_pagination/current_link.html ================================================ {{ page.label|safe }} ================================================ FILE: el_pagination/templates/el_pagination/next_link.html ================================================ {{ page.label|safe }} ================================================ FILE: el_pagination/templates/el_pagination/page_link.html ================================================ {{ page.label|safe }} ================================================ FILE: el_pagination/templates/el_pagination/previous_link.html ================================================ {{ page.label|safe }} ================================================ FILE: el_pagination/templates/el_pagination/show_more.html ================================================ {% load i18n %} {% if querystring %} {% endif %} ================================================ FILE: el_pagination/templates/el_pagination/show_more_table.html ================================================ {% load i18n %} {% if querystring %} {% if label %}{{ label|safe }}{% else %}{% trans "more" %}{% endif %} {% endif %} ================================================ FILE: el_pagination/templates/el_pagination/show_pages.html ================================================ {% for page in pages %} {{ page.render_link|default:'...' }} {% endfor %} ================================================ FILE: el_pagination/templatetags/__init__.py ================================================ ================================================ FILE: el_pagination/templatetags/el_pagination_tags.py ================================================ """Django EL(Endless) Pagination template tags.""" import re from django import template from django.http import Http404 from django.utils.encoding import force_str, iri_to_uri from el_pagination import models, settings, utils from el_pagination.paginators import DefaultPaginator, EmptyPage, LazyPaginator register = template.Library() __all__ = ['register'] PAGINATE_EXPRESSION = re.compile( r""" ^ # Beginning of line. (((?P\w+)\,)?(?P\w+(\.\w+)?)\s+)? # First page, per page. (?P[\.\w]+) # Objects / queryset. (\s+starting\s+from\s+page\s+(?P[\-]?\d+|\w+))? # Page start. (\s+using\s+(?P[\"\'\-\w]+))? # Querystring key. (\s+with\s+(?P[\"\'\/\w]+))? # Override path. (\s+as\s+(?P\w+))? # Context variable name. $ # End of line. """, re.VERBOSE, ) SHOW_CURRENT_NUMBER_EXPRESSION = re.compile( r""" ^ # Beginning of line. (starting\s+from\s+page\s+(?P\w+))?\s* # Page start. (using\s+(?P[\"\'\-\w]+))?\s* # Querystring key. (as\s+(?P\w+))? # Context variable name. $ # End of line. """, re.VERBOSE, ) @register.tag def paginate(parser, token, paginator_class=None): """Paginate objects. Usage: .. code-block:: html+django {% paginate entries %} After this call, the *entries* variable in the template context is replaced by only the entries of the current page. You can also keep your *entries* original variable (usually a queryset) and add to the context another name that refers to entries of the current page, e.g.: .. code-block:: html+django {% paginate entries as page_entries %} The *as* argument is also useful when a nested context variable is provided as queryset. In this case, and only in this case, the resulting variable name is mandatory, e.g.: .. code-block:: html+django {% paginate entries.all as entries %} The number of paginated entries is taken from settings, but you can override the default locally, e.g.: .. code-block:: html+django {% paginate 20 entries %} Of course you can mix it all: .. code-block:: html+django {% paginate 20 entries as paginated_entries %} By default, the first page is displayed the first time you load the page, but you can change this, e.g.: .. code-block:: html+django {% paginate entries starting from page 3 %} When changing the default page, it is also possible to reference the last page (or the second last page, and so on) by using negative indexes, e.g: .. code-block:: html+django {% paginate entries starting from page -1 %} This can be also achieved using a template variable that was passed to the context, e.g.: .. code-block:: html+django {% paginate entries starting from page page_number %} If the passed page number does not exist, the first page is displayed. If you have multiple paginations in the same page, you can change the querydict key for the single pagination, e.g.: .. code-block:: html+django {% paginate entries using article_page %} In this case *article_page* is intended to be a context variable, but you can hardcode the key using quotes, e.g.: .. code-block:: html+django {% paginate entries using 'articles_at_page' %} Again, you can mix it all (the order of arguments is important): .. code-block:: html+django {% paginate 20 entries starting from page 3 using page_key as paginated_entries %} Additionally you can pass a path to be used for the pagination: .. code-block:: html+django {% paginate 20 entries using page_key with pagination_url as paginated_entries %} This way you can easily create views acting as API endpoints, and point your Ajax calls to that API. In this case *pagination_url* is considered a context variable, but it is also possible to hardcode the URL, e.g.: .. code-block:: html+django {% paginate 20 entries with "/mypage/" %} If you want the first page to contain a different number of items than subsequent pages, you can separate the two values with a comma, e.g. if you want 3 items on the first page and 10 on other pages: .. code-block:: html+django {% paginate 3,10 entries %} You must use this tag before calling the {% show_more %} one. """ # Validate arguments. try: tag_name, tag_args = token.contents.split(None, 1) except ValueError as exc: tag = token.contents.split()[0] msg = f'{tag!r} tag requires arguments' raise template.TemplateSyntaxError(msg) from exc # Use a regexp to catch args. match = PAGINATE_EXPRESSION.match(tag_args) if match is None: msg = f'Invalid arguments for {tag_name!r} tag' raise template.TemplateSyntaxError(msg) # Retrieve objects. kwargs = match.groupdict() objects = kwargs.pop('objects') # The variable name must be present if a nested context variable is passed. if '.' in objects and kwargs['var_name'] is None: msg = ( '%(tag)r tag requires a variable name `as` argumnent if the ' 'queryset is provided as a nested context variable (%(objects)s). ' 'You must either pass a direct queryset (e.g. taking advantage ' 'of the `with` template tag) or provide a new variable name to ' 'store the resulting queryset (e.g. `%(tag)s %(objects)s as ' 'objects`).' ) % {'tag': tag_name, 'objects': objects} raise template.TemplateSyntaxError(msg) # Call the node. return PaginateNode(paginator_class, objects, **kwargs) @register.tag def lazy_paginate(parser, token): """Lazy paginate objects. Paginate objects without hitting the database with a *select count* query. Use this the same way as *paginate* tag when you are not interested in the total number of pages. """ return paginate(parser, token, paginator_class=LazyPaginator) class PaginateNode(template.Node): """Add to context the objects of the current page. Also add the Django paginator's *page* object. """ def __init__( self, paginator_class, objects, first_page=None, per_page=None, var_name=None, number=None, key=None, override_path=None, ): self.paginator = paginator_class or DefaultPaginator self.objects = template.Variable(objects) # If *var_name* is not passed, then the queryset name will be used. self.var_name = objects if var_name is None else var_name # If *per_page* is not passed then the default value form settings # will be used. self.per_page_variable = None if per_page is None: self.per_page = settings.PER_PAGE elif per_page.isdigit(): self.per_page = int(per_page) else: self.per_page_variable = template.Variable(per_page) # Handle first page: if it is not passed then *per_page* is used. self.first_page_variable = None if first_page is None: self.first_page = None elif first_page.isdigit(): self.first_page = int(first_page) else: self.first_page_variable = template.Variable(first_page) # Handle page number when it is not specified in querystring. self.page_number_variable = None if number is None: self.page_number = 1 else: try: self.page_number = int(number) except ValueError: self.page_number_variable = template.Variable(number) # Set the querystring key attribute. self.querystring_key_variable = None if key is None: self.querystring_key = settings.PAGE_LABEL elif key[0] in ('"', "'") and key[-1] == key[0]: self.querystring_key = key[1:-1] else: self.querystring_key_variable = template.Variable(key) # Handle *override_path*. self.override_path_variable = None if override_path is None: self.override_path = None elif ( override_path[0] in ('"', "'") and override_path[-1] == override_path[0] ): # noqa self.override_path = override_path[1:-1] else: self.override_path_variable = template.Variable(override_path) def render(self, context): # Handle page number when it is not specified in querystring. if self.page_number_variable is None: default_number = self.page_number else: default_number = int(self.page_number_variable.resolve(context)) # Calculate the number of items to show on each page. if self.per_page_variable is None: per_page = self.per_page else: per_page = int(self.per_page_variable.resolve(context)) # Calculate the number of items to show in the first page. if self.first_page_variable is None: first_page = self.first_page or per_page else: first_page = int(self.first_page_variable.resolve(context)) # User can override the querystring key to use in the template. # The default value is defined in the settings file. if self.querystring_key_variable is None: querystring_key = self.querystring_key else: querystring_key = self.querystring_key_variable.resolve(context) # Retrieve the override path if used. if self.override_path_variable is None: override_path = self.override_path else: override_path = self.override_path_variable.resolve(context) # Retrieve the queryset and create the paginator object. objects = self.objects.resolve(context) paginator = self.paginator( objects, per_page, first_page=first_page, orphans=settings.ORPHANS ) # Normalize the default page number if a negative one is provided. if default_number < 0: default_number = utils.normalize_page_number( default_number, paginator.page_range ) # The current request is used to get the requested page number. page_number = utils.get_page_number_from_request( context['request'], querystring_key, default=default_number ) # Get the page. try: page = paginator.page(page_number) except EmptyPage: page = paginator.page(1) if settings.PAGE_OUT_OF_RANGE_404: raise Http404('Page out of range') # pylint: disable=raise-missing-from # Populate the context with required data. data = { 'default_number': default_number, 'override_path': override_path, 'page': page, 'querystring_key': querystring_key, } context.update({'endless': data, self.var_name: page.object_list}) return '' @register.inclusion_tag('el_pagination/show_more.html', takes_context=True) def show_more(context, label=None, loading=settings.LOADING, class_name=None): """Show the link to get the next page in a Twitter-like pagination. Usage:: {% show_more %} Alternatively you can override the label passed to the default template:: {% show_more "even more" %} You can override the loading text too:: {% show_more "even more" "working" %} You could pass in the extra CSS style class name as a third argument {% show_more "even more" "working" "class_name" %} Must be called after ``{% paginate objects %}``. """ # This template tag could raise a PaginationError: you have to call # *paginate* or *lazy_paginate* before including the showmore template. data = utils.get_data_from_context(context) page = data['page'] # show the template only if there is a next page if page.has_next(): request = context['request'] page_number = page.next_page_number() # Generate the querystring. querystring_key = data['querystring_key'] querystring = utils.get_querystring_for_page( request, page_number, querystring_key, default_number=data['default_number'] ) return { 'label': label, 'loading': loading, 'class_name': class_name, 'path': iri_to_uri(data['override_path'] or request.path), 'querystring': querystring, 'querystring_key': querystring_key, 'request': request, } # No next page, nothing to see. return {} @register.inclusion_tag('el_pagination/show_more_table.html', takes_context=True) def show_more_table(context, label=None, loading=settings.LOADING): """Show the link to get the next page in a Twitter-like pagination in a template for table. Usage:: {% show_more_table %} Alternatively you can override the label passed to the default template:: {% show_more_table "even more" %} You can override the loading text too:: {% show_more_table "even more" "working" %} Must be called after ``{% paginate objects %}``. """ # This template tag could raise a PaginationError: you have to call # *paginate* or *lazy_paginate* before including the showmore template. return show_more(context, label, loading) @register.tag def get_pages(parser, token): """Add to context the list of page links. Usage: .. code-block:: html+django {% get_pages %} This is mostly used for Digg-style pagination. This call inserts in the template context a *pages* variable, as a sequence of page links. You can use *pages* in different ways: - just print *pages.get_rendered* and you will get Digg-style pagination displayed: .. code-block:: html+django {{ pages.get_rendered }} - display pages count: .. code-block:: html+django {{ pages|length }} - check if the page list contains more than one page: .. code-block:: html+django {{ pages.paginated }} {# the following is equivalent #} {{ pages|length > 1 }} - get a specific page: .. code-block:: html+django {# the current selected page #} {{ pages.current }} {# the first page #} {{ pages.first }} {# the last page #} {{ pages.last }} {# the previous page (or nothing if you are on first page) #} {{ pages.previous }} {# the next page (or nothing if you are in last page) #} {{ pages.next }} {# the third page #} {{ pages.3 }} {# this means page.1 is the same as page.first #} {# the 1-based index of the first item on the current page #} {{ pages.current_start_index }} {# the 1-based index of the last item on the current page #} {{ pages.current_end_index }} {# the total number of objects, across all pages #} {{ pages.total_count }} {# the first page represented as an arrow #} {{ pages.first_as_arrow }} {# the last page represented as an arrow #} {{ pages.last_as_arrow }} - iterate over *pages* to get all pages: .. code-block:: html+django {% for page in pages %} {# display page link #} {{ page.render_link}} {# the page url (beginning with "?") #} {{ page.url }} {# the page path #} {{ page.path }} {# the page number #} {{ page.number }} {# a string representing the page (commonly the page number) #} {{ page.label }} {# check if the page is the current one #} {{ page.is_current }} {# check if the page is the first one #} {{ page.is_first }} {# check if the page is the last one #} {{ page.is_last }} {% endfor %} You can change the variable name, e.g.: .. code-block:: html+django {% get_pages as page_links %} Must be called after ``{% paginate objects %}``. """ # Validate args. try: tag_name, args = token.contents.split(None, 1) except ValueError: var_name = 'pages' else: args = args.split() if len(args) == 2 and args[0] == 'as': var_name = args[1] else: msg = f'Invalid arguments for {tag_name!r} tag' raise template.TemplateSyntaxError(msg) # Call the node. return GetPagesNode(var_name) class GetPagesNode(template.Node): """Add the page list to context.""" def __init__(self, var_name): self.var_name = var_name def render(self, context): # This template tag could raise a PaginationError: you have to call # *paginate* or *lazy_paginate* before including the getpages template. data = utils.get_data_from_context(context) # Add the PageList instance to the context. context[self.var_name] = models.PageList( context['request'], data['page'], data['querystring_key'], context=context, default_number=data['default_number'], override_path=data['override_path'], ) return '' @register.tag def show_pages(parser, token): """Show page links. Usage: .. code-block:: html+django {% show_pages %} It is just a shortcut for: .. code-block:: html+django {% get_pages %} {{ pages.get_rendered }} You can set ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` in your *settings.py* to a callable, or to a dotted path representing a callable, used to customize the pages that are displayed. See the *__unicode__* method of ``endless_pagination.models.PageList`` for a detailed explanation of how the callable can be used. Must be called after ``{% paginate objects %}``. """ # Validate args. if len(token.contents.split()) != 1: tag = token.contents.split()[0] raise template.TemplateSyntaxError(f'{tag!r} tag takes no arguments') # Call the node. return ShowPagesNode() class ShowPagesNode(template.Node): """Show the pagination.""" def render(self, context): # This template tag could raise a PaginationError: you have to call # *paginate* or *lazy_paginate* before including the getpages template. data = utils.get_data_from_context(context) # Return the string representation of the sequence of pages. pages = models.PageList( context['request'], data['page'], data['querystring_key'], default_number=data['default_number'], override_path=data['override_path'], context=context, ) return pages.get_rendered() @register.tag def show_current_number(parser, token): """Show the current page number, or insert it in the context. This tag can for example be useful to change the page title according to the current page number. To just show current page number: .. code-block:: html+django {% show_current_number %} If you use multiple paginations in the same page, you can get the page number for a specific pagination using the querystring key, e.g.: .. code-block:: html+django {% show_current_number using mykey %} The default page when no querystring is specified is 1. If you changed it in the `paginate`_ template tag, you have to call ``show_current_number`` according to your choice, e.g.: .. code-block:: html+django {% show_current_number starting from page 3 %} This can be also achieved using a template variable you passed to the context, e.g.: .. code-block:: html+django {% show_current_number starting from page page_number %} You can of course mix it all (the order of arguments is important): .. code-block:: html+django {% show_current_number starting from page 3 using mykey %} If you want to insert the current page number in the context, without actually displaying it in the template, use the *as* argument, i.e.: .. code-block:: html+django {% show_current_number as page_number %} {% show_current_number starting from page 3 using mykey as page_number %} """ # Validate args. try: tag_name, args = token.contents.split(None, 1) except ValueError: key = None number = None tag_name = token.contents[0] var_name = None else: # Use a regexp to catch args. match = SHOW_CURRENT_NUMBER_EXPRESSION.match(args) if match is None: msg = f'Invalid arguments for {tag_name!r} tag' raise template.TemplateSyntaxError(msg) # Retrieve objects. groupdict = match.groupdict() key = groupdict['key'] number = groupdict['number'] var_name = groupdict['var_name'] # Call the node. return ShowCurrentNumberNode(number, key, var_name) class ShowCurrentNumberNode(template.Node): """Show the page number taken from context.""" def __init__(self, number, key, var_name): # Retrieve the page number. self.page_number_variable = None if number is None: self.page_number = 1 elif number.isdigit(): self.page_number = int(number) else: self.page_number_variable = template.Variable(number) # Get the queystring key. self.querystring_key_variable = None if key is None: self.querystring_key = settings.PAGE_LABEL elif key[0] in ('"', "'") and key[-1] == key[0]: self.querystring_key = key[1:-1] else: self.querystring_key_variable = template.Variable(key) # Get the template variable name. self.var_name = var_name def render(self, context): # Get the page number to use if it is not specified in querystring. if self.page_number_variable is None: default_number = self.page_number else: default_number = int(self.page_number_variable.resolve(context)) # User can override the querystring key to use in the template. # The default value is defined in the settings file. if self.querystring_key_variable is None: querystring_key = self.querystring_key else: querystring_key = self.querystring_key_variable.resolve(context) # The request object is used to retrieve the current page number. page_number = utils.get_page_number_from_request( context['request'], querystring_key, default=default_number ) if self.var_name is None: return force_str(page_number) context[self.var_name] = page_number return '' ================================================ FILE: el_pagination/tests/__init__.py ================================================ """Test model definitions.""" from django.core.management import call_command call_command('makemigrations', verbosity=0) call_command('migrate', verbosity=0) ================================================ FILE: el_pagination/tests/integration/__init__.py ================================================ """Integration tests base objects definitions.""" import os import unittest from contextlib import contextmanager from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.http import QueryDict from django.urls import reverse from selenium.common import exceptions # from selenium.webdriver import Firefox from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.support import ui # Disable selenium as default. Difficult to setup for local tests. Must be enabled # in CI environment. USE_SELENIUM = os.getenv('USE_SELENIUM', 0) in (1, True, '1') def setup_package(): """Set up the Selenium driver once for all tests.""" # Just skipping *setup_package* and *teardown_package* generates an # uncaught exception under Python 2.6. if USE_SELENIUM: # Create a Selenium browser instance. options = Options() options.add_argument('-headless') selenium = SeleniumTestCase.selenium = WebDriver(options=options) selenium.maximize_window() SeleniumTestCase.wait = ui.WebDriverWait(selenium, 5) SeleniumTestCase.selenium.implicitly_wait(3) def teardown_package(): """Quit the Selenium driver.""" if USE_SELENIUM: SeleniumTestCase.selenium.quit() @unittest.skipIf( not USE_SELENIUM, 'excluding integration tests: environment variable USE_SELENIUM is not set.') class SeleniumTestCase(StaticLiveServerTestCase): """Base test class for integration tests.""" PREVIOUS = '<' NEXT = '>' MORE = 'More results' selector = 'div.{0} > h4' def setUp(self): self.url = self.live_server_url + reverse(self.view_name) # Give the browser a little time; Firefox sometimes throws # random errors if you hit it too soon # time.sleep(1) # @classmethod # def setUpClass(cls): # if not SHOW_BROWSER: # # start display # cls.xvfb = Xvfb(width=1280, height=720) # cls.xvfb.start() # # Create a Selenium browser instance. # cls.browser = os.getenv('SELENIUM_BROWSER', 'firefox') # # start browser # if cls.browser == 'firefox': # cls.selenium = webdriver.Firefox() # elif cls.browser == 'htmlunit': # cls.selenium = webdriver.Remote(desired_capabilities=DesiredCapabilities.HTMLUNITWITHJS) # elif cls.browser == 'iphone': # command_executor = "http://127.0.0.1:3001/wd/hub" # cls.selenium = webdriver.Remote(command_executor=command_executor, # desired_capabilities=DesiredCapabilities.IPHONE) # elif cls.browser == 'safari': # cls.selenium = webdriver.Remote(desired_capabilities={ # "browserName": "safari", "version": "", # "platform": "MAC", "javascriptEnabled": True}) # else: # cls.selenium = webdriver.Chrome() # cls.selenium.maximize_window() # cls.wait = ui.WebDriverWait(cls.selenium, 10) # cls.selenium.implicitly_wait(3) # # super(SeleniumTestCase, cls).setUpClass() # @classmethod # def tearDownClass(cls): # # stop browser # cls.selenium.quit() # super(SeleniumTestCase, cls).tearDownClass() # if not SHOW_BROWSER: # # stop display # cls.xvfb.stop() def get(self, url=None, data=None, **kwargs): """Load a web page in the current browser session. If *url* is None, *self.url* is used. The querydict can be expressed providing *data* or *kwargs*. """ if url is None: url = self.url querydict = QueryDict('', mutable=True) if data is not None: querydict.update(data) querydict.update(kwargs) path = f'{url}?{querydict.urlencode()}' # the following javascript scrolls down the entire page body. Since Twitter # uses "infinite scrolling", more content will be added to the bottom of the # DOM as you scroll... since it is in the loop, it will scroll down up to 100 # times. # for _ in range(100): # self.selenium.execute_script("window.scrollTo(0, document.body.scrollHeight);") # print all of the page source that was loaded # print self.selenium.page_source.encode("utf-8") return self.selenium.get(path) def wait_ajax(self): """Wait for the document to be ready.""" def document_ready(driver): script = """ return ( document.readyState === 'complete' && jQuery.active === 0 && typeof jQuery != 'undefined' ); """ return driver.execute_script(script) self.wait.until(document_ready) return self.wait def click_link(self, text, index=0): """Click the link with the given *text* and *index*.""" link = self.selenium.find_elements_by_link_text(str(text))[index] link.click() return link def scroll_down(self): """Scroll down to the bottom of the page.""" script = 'window.scrollTo(0, document.body.scrollHeight);' self.selenium.execute_script(script) def get_current_elements(self, class_name, driver=None): """Return the range of current elements as a list of numbers.""" elements = [] selector = self.selector.format(class_name) if driver is None: driver = self.selenium for element in driver.find_elements_by_css_selector(selector): elements.append(int(element.text.split()[1])) return elements def asserLinksEqual(self, count, text): """Assert the page contains *count* links with given *text*.""" def link_condition_attended(driver): links = driver.find_elements_by_link_text(str(text)) return len(links) == count self.wait.until(link_condition_attended) return self.wait def assertElements(self, class_name, elements): """Assert the current page contains the given *elements*.""" current_elements = self.get_current_elements(class_name) self.assertSequenceEqual( elements, current_elements, ( 'Elements differ: {expected} != {actual}\n' 'Class name: {class_name}\n' 'Expected elements: {expected}\n' 'Actual elements: {actual}' ).format( actual=current_elements, expected=elements, class_name=class_name, ) ) @contextmanager def assertNewElements(self, class_name, new_elements): """Fail when new elements are not found in the page.""" def new_elements_loaded(driver): elements = self.get_current_elements(class_name, driver=driver) return elements == new_elements yield try: self.wait_ajax().until(new_elements_loaded) except exceptions.TimeoutException: self.assertElements(class_name, new_elements) @contextmanager def assertSameURL(self): """Assert the URL does not change after executing the yield block.""" current_url = self.selenium.current_url yield self.wait_ajax() self.assertEqual(current_url, self.selenium.current_url) ================================================ FILE: el_pagination/tests/integration/test_callbacks.py ================================================ """Javascript callbacks integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class CallbacksTest(SeleniumTestCase): view_name = 'callbacks' def notifications_loaded(self, driver): return driver.find_elements_by_id('fragment') def assertNotificationsEqual(self, notifications): """Assert the given *notifications* equal the ones in the DOM.""" self.wait_ajax().until(self.notifications_loaded) find = self.selenium.find_element_by_id for key, value in notifications.items(): self.assertEqual(value, find(key).text) def test_can_navigate_site(self): self.selenium.get(self.live_server_url) # use the live server url assert 'Testing project - Django Endless Pagination' in \ self.selenium.title def test_on_click(self): # Ensure the onClick callback is correctly called. self.get() self.click_link(2) self.assertNotificationsEqual({ 'onclick': 'Object 1', 'onclick-label': '2', 'onclick-url': '/callbacks/?page=2', 'onclick-key': 'page', }) def test_on_completed(self): # Ensure the onCompleted callback is correctly called. self.get(page=10) self.click_link(1) self.assertNotificationsEqual({ 'oncompleted': 'Object 1', 'oncompleted-label': '1', 'oncompleted-url': '/callbacks/', 'oncompleted-key': 'page', 'fragment': 'Object 3', }) ================================================ FILE: el_pagination/tests/integration/test_chunks.py ================================================ """On scroll chunks integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class ChunksPaginationTest(SeleniumTestCase): view_name = 'chunks' def test_new_elements_loaded(self): # Ensure new pages are loaded on scroll. self.get() with self.assertNewElements('object', range(1, 11)): with self.assertNewElements('item', range(1, 11)): self.scroll_down() def test_url_not_changed(self): # Ensure the request is done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.scroll_down() def test_direct_link(self): # Ensure direct links work. self.get(data={'page': 2, 'items-page': 3}) current_url = self.selenium.current_url self.assertElements('object', range(6, 11)) self.assertElements('item', range(11, 16)) self.assertIn('page=2', current_url) self.assertIn('items-page=3', current_url) def test_subsequent_page(self): # Ensure next page is correctly loaded in a subsequent page, even if # normally it is the last page of the chunk. self.get(page=3) with self.assertNewElements('object', range(11, 21)): self.scroll_down() def test_chunks(self): # Ensure new items are not loaded on scroll if the chunk is complete. self.get() while len(self.get_current_elements('item')) < 20: self.scroll_down() self.wait_ajax() self.scroll_down() self.wait_ajax() self.assertElements('object', range(1, 16)) self.assertElements('item', range(1, 21)) ================================================ FILE: el_pagination/tests/integration/test_digg.py ================================================ """Digg-style pagination integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class DiggPaginationTest(SeleniumTestCase): view_name = 'digg' def test_new_elements_loaded(self): # Ensure a new page is loaded on click. self.get() with self.assertNewElements('object', range(6, 11)): self.click_link(2) def test_url_not_changed(self): # Ensure the request is done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.click_link(2) def test_direct_link(self): # Ensure direct links work. self.get(page=4) self.assertElements('object', range(16, 21)) self.assertIn('page=4', self.selenium.current_url) def test_next(self): # Ensure next page is correctly loaded. self.get() with self.assertSameURL(): with self.assertNewElements('object', range(6, 11)): self.click_link(self.NEXT) def test_previous(self): # Ensure previous page is correctly loaded. self.get(page=4) with self.assertSameURL(): with self.assertNewElements('object', range(11, 16)): self.click_link(self.PREVIOUS) def test_no_previous_link_in_first_page(self): # Ensure there is no previous link on the first page. self.get() self.asserLinksEqual(0, self.PREVIOUS) def test_no_next_link_in_last_page(self): # Ensure there is no forward link on the last page. self.get(page=10) self.asserLinksEqual(0, self.NEXT) class DiggPaginationTableTest(DiggPaginationTest): view_name = 'digg-table' selector = 'tr.{0} td > h4' ================================================ FILE: el_pagination/tests/integration/test_feed_wrapper.py ================================================ """Twitter-style pagination feeding an specific content wrapper integration tests.""" import el_pagination.settings from el_pagination.tests.integration import SeleniumTestCase class FeedWrapperPaginationTest(SeleniumTestCase): view_name = 'feed-wrapper' selector = 'tbody > tr > td:first-child' def test_new_elements_loaded(self): # Ensure a new page is loaded on click. self.get() with self.assertNewElements('object', range(1, 21)): self.click_link(self.MORE) def test_url_not_changed(self): # Ensure the request is done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.click_link(self.MORE) def test_direct_link(self): # Ensure direct links work. self.get(page=3) self.assertElements('object', range(21, 31)) self.assertIn('page=3', self.selenium.current_url) def test_subsequent_page(self): # Ensure next page is correctly loaded in a subsequent page. self.get(page=2) with self.assertNewElements('object', range(11, 31)): self.click_link(self.MORE) def test_feed_wrapper__test_multiple_show_more_through_all_pages(self): # Make a 404 error when page is empty el_pagination.settings.PAGE_OUT_OF_RANGE_404 = True # Ensure new pages are loaded again and again. self.get() for page in range(2, 6): expected_range = range(1, 10 * page + 1) with self.assertSameURL(): with self.assertNewElements('object', expected_range): self.click_link(self.MORE) # The more link is kept even in the last page, once it # doesn't know it is the last self.asserLinksEqual(1, self.MORE) # After one more click, the more link itself is removed self.click_link(self.MORE) self.asserLinksEqual(0, self.MORE) # Return to initial condition el_pagination.settings.PAGE_OUT_OF_RANGE_404 = False def test_no_more_link_in_last_page_opened_directly(self): self.get(page=5) self.asserLinksEqual(0, self.MORE) self.assertElements('object', range(41, 51)) ================================================ FILE: el_pagination/tests/integration/test_multiple.py ================================================ """Multiple pagination integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class MultiplePaginationTest(SeleniumTestCase): view_name = 'multiple' def test_new_elements_loaded(self): # Ensure a new page is loaded on click for each paginated elements. self.get() with self.assertNewElements('object', range(4, 7)): with self.assertNewElements('item', range(7, 10)): with self.assertNewElements('entry', range(1, 5)): self.click_link(2, 0) self.click_link(3, 1) self.click_link(self.MORE) def test_url_not_changed(self): # Ensure the requests are done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.click_link(2, 0) self.click_link(3, 1) self.click_link(self.MORE) def test_direct_link(self): # Ensure direct links work. self.get(data={'objects-page': 3, 'items-page': 4, 'entries-page': 5}) self.assertElements('object', range(7, 10)) self.assertElements('item', range(10, 13)) self.assertElements('entry', range(11, 14)) self.assertIn('objects-page=3', self.selenium.current_url) self.assertIn('items-page=4', self.selenium.current_url) self.assertIn('entries-page=5', self.selenium.current_url) def test_subsequent_pages(self): # Ensure elements are correctly loaded starting from a subsequent page. self.get(data={'objects-page': 2, 'items-page': 2, 'entries-page': 2}) with self.assertNewElements('object', range(1, 4)): with self.assertNewElements('item', range(7, 10)): with self.assertNewElements('entry', range(2, 8)): self.click_link(self.PREVIOUS, 0) self.click_link(self.NEXT, 1) self.click_link(self.MORE) def test_no_more_link_in_last_page(self): # Ensure there is no more or forward links on the last pages. self.get(data={'objects-page': 7, 'items-page': 7, 'entries-page': 8}) self.asserLinksEqual(0, self.NEXT) self.asserLinksEqual(0, self.MORE) ================================================ FILE: el_pagination/tests/integration/test_onscroll.py ================================================ """On scroll pagination integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class OnScrollPaginationTest(SeleniumTestCase): view_name = 'onscroll' def test_new_elements_loaded(self): # Ensure a new page is loaded on scroll. self.get() with self.assertNewElements('object', range(1, 21)): self.scroll_down() def test_url_not_changed(self): # Ensure the request is done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.scroll_down() def test_direct_link(self): # Ensure direct links work. self.get(page=3) self.assertElements('object', range(21, 31)) self.assertIn('page=3', self.selenium.current_url) def test_subsequent_page(self): # Ensure next page is correctly loaded in a subsequent page. self.get(page=2) with self.assertNewElements('object', range(11, 31)): self.scroll_down() def test_multiple_show_more(self): # Ensure new pages are loaded again and again. self.get() for page in range(2, 5): expected_range = range(1, 10 * page + 1) with self.assertSameURL(): with self.assertNewElements('object', expected_range): self.scroll_down() def test_scrolling_last_page(self): # Ensure scrolling on the last page is a no-op. self.get(page=5) with self.assertNewElements('object', range(41, 51)): self.scroll_down() class OnScrollPaginationTableTest(OnScrollPaginationTest): view_name = 'onscroll-table' selector = 'tr.{0} td > h4' ================================================ FILE: el_pagination/tests/integration/test_twitter.py ================================================ """Twitter-style pagination integration tests.""" from el_pagination.tests.integration import SeleniumTestCase class TwitterPaginationTest(SeleniumTestCase): view_name = 'twitter' def test_new_elements_loaded(self): # Ensure a new page is loaded on click. self.get() with self.assertNewElements('object', range(1, 11)): self.click_link(self.MORE) def test_url_not_changed(self): # Ensure the request is done using Ajax (the page does not refresh). self.get() with self.assertSameURL(): self.click_link(self.MORE) def test_direct_link(self): # Ensure direct links work. self.get(page=4) self.assertElements('object', range(16, 21)) self.assertIn('page=4', self.selenium.current_url) def test_subsequent_page(self): # Ensure next page is correctly loaded in a subsequent page. self.get(page=2) with self.assertNewElements('object', range(6, 16)): self.click_link(self.MORE) def test_multiple_show_more(self): # Ensure new pages are loaded again and again. self.get() for page in range(2, 5): expected_range = range(1, 5 * page + 1) with self.assertSameURL(): with self.assertNewElements('object', expected_range): self.click_link(self.MORE) def test_no_more_link_in_last_page(self): # Ensure there is no more link on the last page. self.get(page=10) self.asserLinksEqual(0, self.MORE) class TwitterPaginationTableTest(TwitterPaginationTest): view_name = 'twitter-table' selector = 'tr.{0} td > h4' ================================================ FILE: el_pagination/tests/templatetags/__init__.py ================================================ ================================================ FILE: el_pagination/tests/templatetags/test_el_pagination_tags.py ================================================ """Endless template tags tests.""" import string import sys import unittest import xml.etree.ElementTree as etree from django.http import Http404 from django.template import Context, Template, TemplateSyntaxError from django.template.context import make_context from django.test import TestCase from django.test.client import RequestFactory from el_pagination import settings from el_pagination.exceptions import PaginationError from el_pagination.models import PageList from project.models import make_model_instances skip_if_old_etree = unittest.skipIf( sys.version_info < (2, 7), 'XPath not supported by this Python version.') class TemplateTagsTestMixin(object): """Base test mixin for template tags.""" def setUp(self): self.factory = RequestFactory() def render(self, request, contents, **kwargs): """Render *contents* using given *request*. The context data is represented by keyword arguments. Is no keyword arguments are provided, a default context will be used. Return the generated HTML and the modified context. """ template = Template('{% load el_pagination_tags %}' + contents) context_data = kwargs.copy() if kwargs else {'objects': range(47)} context_data['request'] = request context = Context(context_data) if isinstance(context, dict): # <-- my temporary workaround context = make_context(context, request, autoescape=self.backend.engine.autoescape) html = template.render(context) return html.strip(), context def request(self, url='/', page=None, data=None, **kwargs): """Return a Django request for the given *page*.""" querydict = {} if data is None else data querydict.update(kwargs) if page is not None: querydict[settings.PAGE_LABEL] = page return self.factory.get(url, querydict) class EtreeTemplateTagsTestMixin(TemplateTagsTestMixin): """Mixin for template tags returning a rendered HTML.""" def render(self, request, contents, **kwargs): """Return the etree root node of the HTML output. Does not return the context. """ html, _ = super().render( request, contents, **kwargs) if html: return etree.fromstring('{0}'.format(html)) class PaginateTestMixin(TemplateTagsTestMixin): """Test mixin for *paginate* and *lazy_paginate* tags. Subclasses must define *tagname*. """ def assertPaginationNumQueries(self, num_queries, template, queryset=None): """Assert the expected *num_queries* are actually executed. The given *queryset* is paginated using *template*. If the *queryset* is not given, a default queryset containing 47 model instances is used. In the *template*, the queryset must be referenced as ``objects``. Return the resulting list of objects for the current page. """ if queryset is None: queryset = make_model_instances(47) request = self.request() with self.assertNumQueries(num_queries): _, context = self.render(request, template, objects=queryset) objects = list(context['objects']) return objects def assertRangeEqual(self, expected, actual): """Assert the *expected* range equals the *actual* one.""" self.assertListEqual(list(expected), list(actual)) def render(self, request, contents, **kwargs): text = string.Template(contents).substitute(tagname=self.tagname) return super().render(request, text, **kwargs) def test_object_list(self): # Ensure the queryset is correctly updated. template = '{% $tagname objects %}' html, context = self.render(self.request(), template) self.assertRangeEqual(range(settings.PER_PAGE), context['objects']) self.assertEqual('', html) def test_per_page_argument(self): # Ensure the queryset reflects the given ``per_page`` argument. template = '{% $tagname 20 objects %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(20), context['objects']) def test_per_page_argument_as_variable(self): # Ensure the queryset reflects the given ``per_page`` argument. # In this case, the argument is provided as context variable. template = '{% $tagname per_page entries %}' _, context = self.render( self.request(), template, entries=range(47), per_page=5) self.assertRangeEqual(range(5), context['entries']) def test_first_page_argument(self): # Ensure the queryset reflects the given ``first_page`` argument. template = '{% $tagname 10,20 objects %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(10), context['objects']) # Check the second page. _, context = self.render(self.request(page=2), template) self.assertRangeEqual(range(10, 30), context['objects']) def test_first_page_argument_as_variable(self): # Ensure the queryset reflects the given ``first_page`` argument. # In this case, the argument is provided as context variable. template = '{% $tagname first_page,subsequent_pages entries %}' context_data = { 'entries': range(47), 'first_page': 1, 'subsequent_pages': 40, } _, context = self.render(self.request(), template, **context_data) self.assertSequenceEqual([0], context['entries']) # Check the second page. _, context = self.render( self.request(page=2), template, **context_data) self.assertRangeEqual(range(1, 41), context['entries']) def test_starting_from_page_argument(self): # Ensure the queryset reflects the given ``starting_from_page`` arg. template = '{% $tagname 10 objects starting from page 3 %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(20, 30), context['objects']) def test_starting_from_page_argument_as_variable(self): # Ensure the queryset reflects the given ``starting_from_page`` arg. # In this case, the argument is provided as context variable. template = '{% $tagname 10 entries starting from page mypage %}' _, context = self.render( self.request(), template, entries=range(47), mypage=2) self.assertRangeEqual(range(10, 20), context['entries']) def test_using_argument(self): # Ensure the template tag uses the given querystring key. template = '{% $tagname 20 objects using "my-page" %}' _, context = self.render( self.request(data={'my-page': 2}), template) self.assertRangeEqual(range(20, 40), context['objects']) def test_using_argument_as_variable(self): # Ensure the template tag uses the given querystring key. # In this case, the argument is provided as context variable. template = '{% $tagname 20 entries using qskey %}' _, context = self.render( self.request(p=3), template, entries=range(47), qskey='p') self.assertRangeEqual(range(40, 47), context['entries']) def test_with_argument(self): # Ensure the context contains the correct override path. template = '{% $tagname 10 objects with "/mypath/" %}' _, context = self.render(self.request(), template) self.assertEqual('/mypath/', context['endless']['override_path']) def test_with_argument_as_variable(self): # Ensure the context contains the correct override path. # In this case, the argument is provided as context variable. path = '/my/path/' template = '{% $tagname 10 entries with path %}' _, context = self.render( self.request(), template, entries=range(47), path=path) self.assertEqual(path, context['endless']['override_path']) def test_as_argument(self): # Ensure it is possible to change the resulting context variable. template = '{% $tagname 20 objects as object_list %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(20), context['object_list']) # The input queryset has not been changed. self.assertRangeEqual(range(47), context['objects']) def test_complete_argument_list(self): # Ensure the tag works providing all the arguments. template = ( '{% $tagname 5,10 objects ' 'starting from page 2 ' 'using mypage ' 'with path ' 'as paginated %}' ) _, context = self.render( self.request(), template, objects=range(47), mypage='page-number', path='mypath') self.assertRangeEqual(range(5, 15), context['paginated']) self.assertEqual('mypath', context['endless']['override_path']) def test_invalid_arguments(self): # An error is raised if invalid arguments are provided. templates = ( '{% $tagname %}', '{% $tagname foo bar spam eggs %}', '{% $tagname 20 objects as object_list using "mykey" %}', ) request = self.request() for template in templates: with self.assertRaises(TemplateSyntaxError): self.render(request, template) def test_invalid_page(self): # The first page is displayed if an invalid page is provided. settings.PAGE_OUT_OF_RANGE_404 = False template = '{% $tagname 5 objects %}' _, context = self.render(self.request(page=0), template) self.assertRangeEqual(range(5), context['objects']) def test_invalid_page_with_raise_404_enabled(self): # The 404 error is raised if an invalid page is provided and # env variable PAGE_OUT_OF_RANGE_404 is set to True. settings.PAGE_OUT_OF_RANGE_404 = True template = '{% $tagname 5 objects %}' with self.assertRaises(Http404): self.render(self.request(page=0), template) def test_nested_context_variable(self): # Ensure nested context variables are correctly handled. manager = {'all': range(47)} template = '{% $tagname 5 manager.all as objects %}' _, context = self.render(self.request(), template, manager=manager) self.assertRangeEqual(range(5), context['objects']) def test_failing_nested_context_variable(self): # An error is raised if a nested context variable is used but no # alias is provided. manager = {'all': range(47)} template = '{% $tagname 5 manager.all %}' with self.assertRaises(TemplateSyntaxError) as cm: self.render(self.request(), template, manager=manager) self.assertIn('manager.all', str(cm.exception)) def test_multiple_pagination(self): # Ensure multiple pagination works correctly. letters = string.ascii_letters template = ( '{% $tagname 10,20 objects %}' '{% $tagname 1 items using items_page %}' '{% $tagname 5 entries.all using "entries" as myentries %}' ) _, context = self.render( self.request(page=2, entries=3), template, objects=range(47), entries={'all': letters}, items=['foo', 'bar'], items_page='p') self.assertRangeEqual(range(10, 30), context['objects']) self.assertSequenceEqual(['foo'], context['items']) self.assertSequenceEqual(letters[10:15], context['myentries']) self.assertSequenceEqual(letters, context['entries']['all']) class PaginateTest(PaginateTestMixin, TestCase): tagname = 'paginate' def test_starting_from_last_page_argument(self): # Ensure the queryset reflects the given ``starting_from_page`` # argument when the last page is requested. template = '{% $tagname 10 objects starting from page -1 %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(40, 47), context['objects']) def test_starting_from_negative_page_argument(self): # Ensure the queryset reflects the given ``starting_from_page`` # argument when a negative number is passed as value. template = '{% $tagname 10 objects starting from page -3 %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(20, 30), context['objects']) def test_starting_from_negative_page_argument_as_variable(self): # Ensure the queryset reflects the given ``starting_from_page`` # argument when a negative number is passed as value. # In this case, the argument is provided as context variable. template = '{% $tagname 10 objects starting from page mypage %}' _, context = self.render( self.request(), template, objects=range(47), mypage=-2 ) self.assertRangeEqual(range(30, 40), context['objects']) def test_starting_from_negative_page_out_of_range(self): # Ensure the last page is returned when the ``starting_from_page`` # argument, given a negative value, produces an out of range error. template = '{% $tagname 10 objects starting from page -5 %}' _, context = self.render(self.request(), template) self.assertRangeEqual(range(10), context['objects']) def test_num_queries(self): # Ensure paginating objects hits the database for the correct number # of times. template = '{% $tagname 10 objects %}' objects = self.assertPaginationNumQueries(2, template) self.assertEqual(10, len(objects)) def test_num_queries_starting_from_another_page(self): # Ensure paginating objects hits the database for the correct number # of times if pagination is performed starting from another page. template = '{% $tagname 10 objects starting from page 3 %}' self.assertPaginationNumQueries(2, template) def test_num_queries_starting_from_last_page(self): # Ensure paginating objects hits the database for the correct number # of times if pagination is performed starting from last page. template = '{% $tagname 10 objects starting from page -1 %}' self.assertPaginationNumQueries(2, template) class LazyPaginateTest(PaginateTestMixin, TestCase): tagname = 'lazy_paginate' def test_starting_from_negative_page_raises_error(self): # A *NotImplementedError* is raised if a negative value is given to # the ``starting_from_page`` argument of ``lazy_paginate``. template = '{% $tagname 10 objects starting from page -1 %}' with self.assertRaises(NotImplementedError): self.render(self.request(), template) def test_num_queries(self): # Ensure paginating objects hits the database for the correct number # of times. If lazy pagination is used, the ``SELECT COUNT`` query # should be avoided. template = '{% $tagname 10 objects %}' objects = self.assertPaginationNumQueries(1, template) self.assertEqual(10, len(objects)) def test_num_queries_starting_from_another_page(self): # Ensure paginating objects hits the database for the correct number # of times if pagination is performed starting from another page. template = '{% $tagname 10 objects starting from page 3 %}' self.assertPaginationNumQueries(1, template) @skip_if_old_etree class ShowMoreTest(EtreeTemplateTagsTestMixin, TestCase): tagname = 'show_more' def render(self, request, contents, **kwargs): text = string.Template(contents).substitute(tagname=self.tagname) return super(ShowMoreTest, self).render(request, text, **kwargs) def test_first_page_next_url(self): # Ensure the link to the next page is correctly generated # in the first page. template = '{% paginate objects %}{% $tagname %}' tree = self.render(self.request(), template) link = tree.find('.//a[@class="endless_more"]') expected = '/?{0}={1}'.format(settings.PAGE_LABEL, 2) self.assertEqual(expected, link.attrib['href']) def test_page_next_url(self): # Ensure the link to the next page is correctly generated. template = '{% paginate objects %}{% $tagname %}' tree = self.render(self.request(page=3), template) link = tree.find('.//a[@class="endless_more"]') expected = '/?{0}={1}'.format(settings.PAGE_LABEL, 4) self.assertEqual(expected, link.attrib['href']) def test_last_page(self): # Ensure the output for the last page is empty. template = '{% paginate 40 objects %}{% $tagname %}' tree = self.render(self.request(page=2), template) self.assertIsNone(tree) def test_customized_label(self): # Ensure the link to the next page is correctly generated. template = '{% paginate objects %}{% $tagname "again and again" %}' tree = self.render(self.request(), template) link = tree.find('.//a[@class="endless_more"]') self.assertEqual('again and again', link.text) def test_customized_loading(self): # Ensure the link to the next page is correctly generated. template = '{% paginate objects %}{% $tagname "more" "working" %}' tree = self.render(self.request(), template) loading = tree.find('.//*[@class="endless_loading"]') self.assertEqual('working', loading.text) @skip_if_old_etree class ShowMoreTableTest(ShowMoreTest): tagname = 'show_more_table' class GetPagesTest(TemplateTagsTestMixin, TestCase): def test_page_list(self): # Ensure the page list is correctly included in the context. template = '{% paginate objects %}{% get_pages %}' html, context = self.render(self.request(), template) self.assertEqual('', html) self.assertIn('pages', context) self.assertIsInstance(context['pages'], PageList) def test_different_varname(self): # Ensure the page list is correctly included in the context when # using a different variable name. template = '{% paginate objects %}{% get_pages as page_list %}' _, context = self.render(self.request(), template) self.assertIn('page_list', context) self.assertIsInstance(context['page_list'], PageList) def test_page_numbers(self): # Ensure the current page in the page list reflects the current # page number. template = '{% lazy_paginate objects %}{% get_pages %}' for page_number in range(1, 5): _, context = self.render(self.request(page=page_number), template) page = context['pages'].current() self.assertEqual(page_number, page.number) def test_without_paginate_tag(self): # An error is raised if this tag is used before the paginate one. template = '{% get_pages %}' with self.assertRaises(PaginationError): self.render(self.request(), template) def test_invalid_arguments(self): # An error is raised if invalid arguments are provided. template = '{% lazy_paginate objects %}{% get_pages foo bar %}' request = self.request() with self.assertRaises(TemplateSyntaxError): self.render(request, template) def test_starting_from_negative_page_in_another_page(self): # Ensure the default page is missing the querystring when another # page is displayed. template = ( '{% paginate 10 objects starting from page -1 %}' '{% get_pages %}' ) _, context = self.render( self.request(), template, objects=range(47), page=1) page = context['pages'].last() self.assertEqual('', page.url) def test_pages_length(self): # Ensure the pages length returns the correct number of pages. template = '{% paginate 10 objects %}{% get_pages %}{{ pages|length }}' html, context = self.render(self.request(), template) self.assertEqual('5', html) @skip_if_old_etree class ShowPagesTest(EtreeTemplateTagsTestMixin, TestCase): def test_current_page(self): # Ensure the current page in the page list reflects the current # page number. template = '{% paginate objects %}{% show_pages %}' for page_number in range(1, 6): tree = self.render(self.request(page=page_number), template) current = tree.find('.//*[@class="endless_page_current"]') text = ''.join(element.text for element in current) self.assertEqual(str(page_number), text) def test_links(self): # Ensure the correct number of links is always displayed. template = '{% paginate objects %}{% show_pages %}' for page_number in range(1, 6): tree = self.render(self.request(page=page_number), template) links = tree.findall('.//a') expected = 5 if page_number == 1 or page_number == 5 else 6 self.assertEqual(expected, len(links)) def test_without_paginate_tag(self): # An error is raised if this tag is used before the paginate one. template = '{% show_pages %}' with self.assertRaises(PaginationError): self.render(self.request(), template) def test_invalid_arguments(self): # An error is raised if invalid arguments are provided. template = '{% lazy_paginate objects %}{% show_pages foo bar %}' request = self.request() with self.assertRaises(TemplateSyntaxError): self.render(request, template) class ShowCurrentNumberTest(TemplateTagsTestMixin, TestCase): def test_current_number(self): # Ensure the current number is correctly returned. template = '{% show_current_number %}' for page_number in range(1, 6): html, _ = self.render(self.request(page=page_number), template) self.assertEqual(page_number, int(html)) def test_starting_from_page_argument(self): # Ensure the number reflects the given ``starting_from_page`` arg. template = '{% show_current_number starting from page 3 %}' html, _ = self.render(self.request(), template) self.assertEqual(3, int(html)) def test_starting_from_page_argument_as_variable(self): # Ensure the number reflects the given ``starting_from_page`` arg. # In this case, the argument is provided as context variable. template = '{% show_current_number starting from page mypage %}' html, _ = self.render( self.request(), template, entries=range(47), mypage=2) self.assertEqual(2, int(html)) def test_using_argument(self): # Ensure the template tag uses the given querystring key. template = '{% show_current_number using "mypage" %}' html, _ = self.render( self.request(mypage=2), template) self.assertEqual(2, int(html)) def test_using_argument_as_variable(self): # Ensure the template tag uses the given querystring key. # In this case, the argument is provided as context variable. template = '{% show_current_number using qskey %}' html, _ = self.render( self.request(p=5), template, entries=range(47), qskey='p') self.assertEqual(5, int(html)) def test_as_argument(self): # Ensure it is possible add the page number to context. template = '{% show_current_number as page_number %}' html, context = self.render(self.request(page=4), template) self.assertEqual('', html) self.assertIn('page_number', context) self.assertEqual(4, context['page_number']) def test_complete_argument_list(self): # Ensure the tag works providing all the arguments. template = ( '{% show_current_number ' 'starting from page 2 ' 'using mypage ' 'as number %}' ) html, context = self.render( self.request(), template, objects=range(47), mypage='page-number') self.assertEqual(2, context['number']) def test_invalid_arguments(self): # An error is raised if invalid arguments are provided. templates = ( '{% show_current_number starting from page %}', '{% show_current_number foo bar spam eggs %}', '{% show_current_number as number using key %}', ) request = self.request() for template in templates: with self.assertRaises(TemplateSyntaxError): self.render(request, template) ================================================ FILE: el_pagination/tests/test_decorators.py ================================================ """Decorator tests.""" from django.test import TestCase from django.test.client import RequestFactory from el_pagination import decorators class DecoratorsTestMixin(object): """Base test mixin for decorators. Subclasses (actual test cases) must implement the ``get_decorator`` method and the ``arg`` attribute to be used as argument for the decorator. """ def setUp(self): self.factory = RequestFactory() self.ajax_headers = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} self.default = 'default.html' self.page = 'page.html' self.page_url = '/?page=2&mypage=10&querystring_key=page' self.mypage = 'mypage.html' self.mypage_url = '/?page=2&mypage=10&querystring_key=mypage' def get_decorator(self): """Return the decorator that must be exercised.""" raise NotImplementedError def assertTemplatesEqual(self, expected_active, expected_page, templates): """Assert active template and page template are the ones given.""" self.assertSequenceEqual([expected_active, expected_page], templates) def decorate(self, *args, **kwargs): """Return a view decorated with ``self.decorator(*args, **kwargs)``.""" def view(request, extra_context=None, template=self.default): """Test view that will be decorated in tests.""" context = {} if extra_context is not None: context.update(extra_context) return template, context['page_template'] decorator = self.get_decorator() return decorator(*args, **kwargs)(view) def test_decorated(self): # Ensure the view is correctly decorated. view = self.decorate(self.arg) templates = view(self.factory.get('/')) self.assertTemplatesEqual(self.default, self.page, templates) def test_request_with_querystring_key(self): # If the querystring key refers to the handled template, # the view still uses the default template if the request is not Ajax. view = self.decorate(self.arg) templates = view(self.factory.get(self.page_url)) self.assertTemplatesEqual(self.default, self.page, templates) def test_ajax_request(self): # Ensure the view serves the template fragment if the request is Ajax. view = self.decorate(self.arg) templates = view(self.factory.get('/', **self.ajax_headers)) self.assertTemplatesEqual(self.page, self.page, templates) def test_ajax_request_with_querystring_key(self): # If the querystring key refers to the handled template, # the view switches the template if the request is Ajax. view = self.decorate(self.arg) templates = view(self.factory.get(self.page_url, **self.ajax_headers)) self.assertTemplatesEqual(self.page, self.page, templates) def test_unexistent_page(self): # Ensure the default page and is returned if the querystring points # to a page that is not defined. view = self.decorate(self.arg) templates = view(self.factory.get('/?querystring_key=does-not-exist')) self.assertTemplatesEqual(self.default, self.page, templates) class PageTemplateTest(DecoratorsTestMixin, TestCase): arg = 'page.html' def get_decorator(self): return decorators.page_template def test_request_with_querystring_key_to_mypage(self): # If the querystring key refers to another template, # the view still uses the default template if the request is not Ajax. view = self.decorate(self.arg) templates = view(self.factory.get(self.mypage_url)) self.assertTemplatesEqual(self.default, self.page, templates) def test_ajax_request_with_querystring_key_to_mypage(self): # If the querystring key refers to another template, # the view still uses the default template even if the request is Ajax. view = self.decorate(self.arg) templates = view( self.factory.get(self.mypage_url, **self.ajax_headers)) self.assertTemplatesEqual(self.default, self.page, templates) def test_ajax_request_to_mypage(self): # Ensure the view serves the template fragment if the request is Ajax # and another template fragment is requested. view = self.decorate(self.mypage, key='mypage') templates = view( self.factory.get(self.mypage_url, **self.ajax_headers)) self.assertTemplatesEqual(self.mypage, self.mypage, templates) class PageTemplatesTest(DecoratorsTestMixin, TestCase): arg = {'page.html': None, 'mypage.html': 'mypage'} def get_decorator(self): return decorators.page_templates def test_request_with_querystring_key_to_mypage(self): # If the querystring key refers to another template, # the view still uses the default template if the request is not Ajax. view = self.decorate(self.arg) templates = view(self.factory.get(self.mypage_url)) self.assertTemplatesEqual(self.default, self.mypage, templates) def test_ajax_request_with_querystring_key_to_mypage(self): # If the querystring key refers to another template, # the view switches to the givent template if the request is Ajax. view = self.decorate(self.arg) templates = view( self.factory.get(self.mypage_url, **self.ajax_headers)) self.assertTemplatesEqual(self.mypage, self.mypage, templates) class PageTemplatesWithTupleTest(PageTemplatesTest): arg = (('page.html', None), ('mypage.html', 'mypage')) ================================================ FILE: el_pagination/tests/test_loaders.py ================================================ """Loader tests.""" from contextlib import contextmanager from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from el_pagination import loaders test_object = 'test object' class ImproperlyConfiguredTestMixin(object): """Include an ImproperlyConfigured assertion.""" @contextmanager def assertImproperlyConfigured(self, message): """Assert the code in the context manager block raises an error. The error must be ImproperlyConfigured, and the error message must include the given *message*. """ try: yield except ImproperlyConfigured as err: self.assertIn(message, str(err).lower()) else: self.fail('ImproperlyConfigured not raised') class AssertImproperlyConfiguredTest(ImproperlyConfiguredTestMixin, TestCase): def test_assertion(self): # Ensure the assertion does not fail if ImproperlyConfigured is raised # with the given error message. with self.assertImproperlyConfigured('error'): raise ImproperlyConfigured('Example error text') def test_case_insensitive(self): # Ensure the error message test is case insensitive. with self.assertImproperlyConfigured('error'): raise ImproperlyConfigured('Example ERROR text') def test_assertion_fails_different_message(self): # Ensure the assertion fails if ImproperlyConfigured is raised with # a different message. with self.assertRaises(AssertionError): with self.assertImproperlyConfigured('failure'): raise ImproperlyConfigured('Example error text') def test_assertion_fails_no_exception(self): # Ensure the assertion fails if ImproperlyConfigured is not raised. with self.assertRaises(AssertionError) as cm: with self.assertImproperlyConfigured('error'): pass self.assertEqual('ImproperlyConfigured not raised', str(cm.exception)) def test_assertion_fails_different_exception(self): # Ensure other exceptions are not swallowed. with self.assertRaises(TypeError): with self.assertImproperlyConfigured('error'): raise TypeError class LoadObjectTest(ImproperlyConfiguredTestMixin, TestCase): def setUp(self): self.module = self.__class__.__module__ def test_valid_path(self): # Ensure the object is correctly loaded if the provided path is valid. path = '.'.join((self.module, 'test_object')) self.assertIs(test_object, loaders.load_object(path)) def test_module_not_found(self): # An error is raised if the module cannot be found. with self.assertImproperlyConfigured('not found'): loaders.load_object('__invalid__.module') def test_invalid_module(self): # An error is raised if the provided path is not a valid dotted string. with self.assertImproperlyConfigured('invalid'): loaders.load_object('') def test_object_not_found(self): # An error is raised if the object cannot be found in the module. path = '.'.join((self.module, '__does_not_exist__')) with self.assertImproperlyConfigured('object'): loaders.load_object(path) ================================================ FILE: el_pagination/tests/test_models.py ================================================ """Model tests.""" from contextlib import contextmanager from django.template import Context from django.test import TestCase from django.test.client import RequestFactory from django.utils.encoding import force_str from el_pagination import models as el_models from el_pagination import settings, utils from el_pagination.paginators import DefaultPaginator @contextmanager def local_settings(**kwargs): """Override local Django Endless Pagination settings. This context manager can be used in a way similar to Django own ``TestCase.settings()``. """ original_values = [] for key, value in kwargs.items(): original_values.append([key, getattr(settings, key)]) setattr(settings, key, value) try: yield finally: for key, value in original_values: setattr(settings, key, value) class LocalSettingsTest(TestCase): def setUp(self): settings._LOCAL_SETTINGS_TEST = 'original' def tearDown(self): del settings._LOCAL_SETTINGS_TEST def test_settings_changed(self): # Check that local settings are changed. with local_settings(_LOCAL_SETTINGS_TEST='changed'): self.assertEqual('changed', settings._LOCAL_SETTINGS_TEST) def test_settings_restored(self): # Check that local settings are restored. with local_settings(_LOCAL_SETTINGS_TEST='changed'): pass self.assertEqual('original', settings._LOCAL_SETTINGS_TEST) def test_restored_after_exception(self): # Check that local settings are restored after an exception. with self.assertRaises(RuntimeError): with local_settings(_LOCAL_SETTINGS_TEST='changed'): raise RuntimeError() self.assertEqual('original', settings._LOCAL_SETTINGS_TEST) def page_list_callable_arrows(number, num_pages): """Wrap ``el_pagination.utils.get_page_numbers``. Set first / last page arrows to True. """ return utils.get_page_numbers(number, num_pages, arrows=True) page_list_callable_dummy = lambda number, num_pages: [None] class PageListTest(TestCase): def setUp(self): self.paginator = DefaultPaginator(range(30), 7, orphans=2) self.current_number = 2 self.page_label = 'page' self.factory = RequestFactory() self.request = self.factory.get( self.get_path_for_page(self.current_number)) self.pages = el_models.PageList( self.request, self.paginator.page(self.current_number), self.page_label, context=Context()) def get_url_for_page(self, number): """Return a url for the given page ``number``.""" return '?{0}={1}'.format(self.page_label, number) def get_path_for_page(self, number): """Return a path for the given page ``number``.""" return '/' + self.get_url_for_page(number) def check_page( self, page, number, is_first, is_last, is_current, label=None): """Perform several assertions on the given page attrs.""" if label is None: label = force_str(page.number) self.assertEqual(label, page.label) self.assertEqual(number, page.number) self.assertEqual(is_first, page.is_first) self.assertEqual(is_last, page.is_last) self.assertEqual(is_current, page.is_current) def check_page_list_callable(self, callable_or_path): """Check the provided *page_list_callable* is actually used.""" with local_settings(PAGE_LIST_CALLABLE=callable_or_path): rendered = force_str(self.pages.get_rendered()).strip() expected = '...' self.assertEqual(expected, rendered) def test_length(self): # Ensure the length of the page list equals the number of pages. self.assertEqual(self.paginator.num_pages, len(self.pages)) def test_paginated(self): # Ensure the *paginated* method returns True if the page list contains # more than one page, False otherwise. page = DefaultPaginator(range(10), 10).page(1) pages = el_models.PageList(self.request, page, self.page_label, context=Context()) self.assertFalse(pages.paginated()) self.assertTrue(self.pages.paginated()) def test_first_page(self): # Ensure the attrs of the first page are correctly defined. page = self.pages.first() self.assertEqual('/', page.path) self.assertEqual('', page.url) self.check_page(page, 1, True, False, False) def test_last_page(self): # Ensure the attrs of the last page are correctly defined. page = self.pages.last() self.check_page(page, len(self.pages), False, True, False) def test_first_page_as_arrow(self): # Ensure the attrs of the first page are correctly defined when the # page is represented as an arrow. page = self.pages.first_as_arrow() self.assertEqual('/', page.path) self.assertEqual('', page.url) self.check_page( page, 1, True, False, False, label=settings.FIRST_LABEL) def test_last_page_as_arrow(self): # Ensure the attrs of the last page are correctly defined when the # page is represented as an arrow. page = self.pages.last_as_arrow() self.check_page( page, len(self.pages), False, True, False, label=settings.LAST_LABEL) def test_current_page(self): # Ensure the attrs of the current page are correctly defined. page = self.pages.current() self.check_page(page, self.current_number, False, False, True) def test_path(self): # Ensure the path of each page is correctly generated. for num, page in enumerate(list(self.pages)[1:]): expected = self.get_path_for_page(num + 2) self.assertEqual(expected, page.path) def test_url(self): # Ensure the path of each page is correctly generated. for num, page in enumerate(list(self.pages)[1:]): expected = self.get_url_for_page(num + 2) self.assertEqual(expected, page.url) def test_current_indexes(self): # Ensure the 1-based indexes of the first and last items on the current # page are correctly returned. self.assertEqual(8, self.pages.current_start_index()) self.assertEqual(14, self.pages.current_end_index()) def test_total_count(self): # Ensure the total number of objects is correctly returned. self.assertEqual(30, self.pages.total_count()) def test_page_render(self): # Ensure the page is correctly rendered. page = self.pages.first() rendered_page = force_str(page.render_link()) self.assertIn('href="/"', rendered_page) self.assertIn(page.label, rendered_page) def test_current_page_render(self): # Ensure the page is correctly rendered. page = self.pages.current() rendered_page = force_str(page.render_link()) self.assertNotIn('href', rendered_page) self.assertIn(page.label, rendered_page) def test_page_list_render(self): # Ensure the page list is correctly rendered. rendered = force_str(self.pages.get_rendered()) self.assertEqual(5, rendered.count(' 1: pages.append(None) elif diff < 1: to_add = current[abs(diff) + 1 :] pages.extend(to_add) # Mix current with last pages. if extremes: diff = last[0] - current[-1] to_add = last if diff > 1: pages.append(None) elif diff < 1: to_add = last[abs(diff) + 1 :] pages.extend(to_add) if current_page != num_pages: pages.append('next') if arrows: pages.append('last') return pages def _iter_factors(starting_factor=1): """Generator yielding something like 1, 3, 10, 30, 100, 300 etc. The series starts from starting_factor. """ while True: yield starting_factor yield starting_factor * 3 starting_factor *= 10 def _make_elastic_range(begin, end): """Generate an S-curved range of pages. Start from both left and right, adding exponentially growing indexes, until the two trends collide. """ # Limit growth for huge numbers of pages. starting_factor = max(1, (end - begin) // 100) factor = _iter_factors(starting_factor) left_half, right_half = [], [] left_val, right_val = begin, end while left_val < right_val: left_half.append(left_val) right_half.append(right_val) next_factor = next(factor) left_val = begin + next_factor right_val = end - next_factor # If the trends happen to meet exactly at one point, retain it. if left_val == right_val: left_half.append(left_val) right_half.reverse() return left_half + right_half def get_elastic_page_numbers(current_page, num_pages): """Alternative callable for page listing. Produce an adaptive pagination, useful for big numbers of pages, by splitting the num_pages ranges in two parts at current_page. Each part will have its own S-curve. """ if num_pages <= 10: return list(range(1, num_pages + 1)) if current_page == 1: pages = [1] else: pages = ['first', 'previous'] pages.extend(_make_elastic_range(1, current_page)) if current_page != num_pages: pages.extend(_make_elastic_range(current_page, num_pages)[1:]) pages.extend(['next', 'last']) return pages def get_querystring_for_page(request, page_number, querystring_key, default_number=1): """Return a querystring pointing to *page_number*.""" querydict = request.GET.copy() querydict[querystring_key] = page_number # For the default page number (usually 1) the querystring is not required. if page_number == default_number: del querydict[querystring_key] if 'querystring_key' in querydict: del querydict['querystring_key'] if querydict: return '?' + querydict.urlencode() return '' def normalize_page_number(page_number, page_range): """Handle a negative *page_number*. Return a positive page number contained in *page_range*. If the negative index is out of range, return the page number 1. """ try: return page_range[page_number] except IndexError: return page_range[0] ================================================ FILE: el_pagination/views.py ================================================ """Django EL Pagination class-based views.""" from django.core.exceptions import ImproperlyConfigured from django.http import Http404 from django.utils.encoding import smart_str from django.utils.translation import gettext as _ from django.views.generic.base import View from django.views.generic.list import MultipleObjectTemplateResponseMixin from el_pagination.settings import PAGE_LABEL class MultipleObjectMixin: allow_empty = True context_object_name = None model = None queryset = None def get_queryset(self): """Get the list of items for this view. This must be an iterable, and may be a queryset (in which qs-specific behavior will be enabled). See original in ``django.views.generic.list.MultipleObjectMixin``. """ if self.queryset is not None: queryset = self.queryset if hasattr(queryset, '_clone'): queryset = queryset._clone() # pylint: disable=protected-access elif self.model is not None: queryset = ( self.model._default_manager.all() # pylint: disable=protected-access ) else: msg = '{0} must define ``queryset`` or ``model``' raise ImproperlyConfigured(msg.format(self.__class__.__name__)) return queryset def get_allow_empty(self): """Returns True if the view should display empty lists. Return False if a 404 should be raised instead. See original in ``django.views.generic.list.MultipleObjectMixin``. """ return self.allow_empty def get_context_object_name(self, object_list): """Get the name of the item to be used in the context. See original in ``django.views.generic.list.MultipleObjectMixin``. """ if self.context_object_name: return self.context_object_name if hasattr(object_list, 'model'): object_name = object_list.model._meta.object_name.lower() return smart_str(f'{object_name}_list') return None def get_context_data(self, **kwargs): """Get the context for this view. Also adds the *page_template* variable in the context. If the *page_template* is not given as a kwarg of the *as_view* method then it is generated using app label, model name (obviously if the list is a queryset), *self.template_name_suffix* and *self.page_template_suffix*. For instance, if the list is a queryset of *blog.Entry*, the template will be ``blog/entry_list_page.html``. """ queryset = kwargs.pop('object_list') page_template = kwargs.pop('page_template') context_object_name = self.get_context_object_name(queryset) context = {'object_list': queryset, 'view': self} context.update(kwargs) if context_object_name is not None: context[context_object_name] = queryset if page_template is None: if hasattr(queryset, 'model'): page_template = self.get_page_template(**kwargs) else: raise ImproperlyConfigured('AjaxListView requires a page_template') context['page_template'] = self.page_template = page_template return context class BaseListView(MultipleObjectMixin, View): object_list = None def get(self, request, *args, **kwargs): self.object_list = self.get_queryset() allow_empty = self.get_allow_empty() if not allow_empty and len(self.object_list) == 0: msg = _('Empty list and ``%(class_name)s.allow_empty`` is False.') raise Http404(msg % {'class_name': self.__class__.__name__}) context = self.get_context_data( object_list=self.object_list, page_template=self.page_template, ) return self.render_to_response(context) # pylint: disable=no-member class InvalidPaginationListView: def get(self, request, *args, **kwargs): """Wraps super().get(...) in order to return 404 status code if the page parameter is invalid """ response = super().get(request, args, kwargs) # pylint: disable=no-member try: response.render() except Http404: request.GET = request.GET.copy() request.GET['page'] = '1' response = super().get(request, args, kwargs) # pylint: disable=no-member response.status_code = 404 return response class AjaxMultipleObjectTemplateResponseMixin(MultipleObjectTemplateResponseMixin): key = PAGE_LABEL page_template = None page_template_suffix = '_page' template_name_suffix = '_list' def get_page_template(self, **kwargs): """Return the template name used for this request. Only called if *page_template* is not given as a kwarg of *self.as_view*. """ opts = self.object_list.model._meta object_name = opts.object_name.lower() t_name_suffix = self.template_name_suffix page_template_suffix = self.page_template_suffix return ( f'{opts.app_label}/{object_name}{t_name_suffix}{page_template_suffix}.html' ) def get_template_names(self): """Switch the templates for Ajax requests.""" request = self.request key = 'querystring_key' querystring_key = request.GET.get(key, request.POST.get(key, PAGE_LABEL)) if ( request.headers.get('x-requested-with') == 'XMLHttpRequest' and querystring_key == self.key ): return [self.page_template or self.get_page_template()] return super().get_template_names() class AjaxListView(AjaxMultipleObjectTemplateResponseMixin, BaseListView): """Allows Ajax pagination of a list of objects. You can use this class-based view in place of *ListView* in order to recreate the behaviour of the *page_template* decorator. For instance, assume you have this code (taken from Django docs):: from django.conf.urls.defaults import * from django.views.generic import ListView from books.models import Publisher urlpatterns = patterns('', (r'^publishers/$', ListView.as_view(model=Publisher)), ) You want to Ajax paginate publishers, so, as seen, you need to switch the template if the request is Ajax and put the page template into the context as a variable named *page_template*. This is straightforward, you only need to replace the view class, e.g.:: from django.conf.urls.defaults import * from books.models import Publisher from el_pagination.views import AjaxListView urlpatterns = patterns('', (r'^publishers/$', AjaxListView.as_view(model=Publisher)), ) NOTE: Django >= 1.3 is required to use this view. """ ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=45", "wheel"] build-backend = "setuptools.build_meta" [project] name = "django-el-pagination" dynamic = ["version"] description = "Django pagination tools supporting Ajax, multiple and lazy pagination, Twitter-style and Digg-style pagination." readme = "README.rst" requires-python = ">=3.8" license = {file = "LICENSE"} authors = [ {name = "Francesco Banconi"}, {name = "Oleksandr Shtalinberg", email = "O.Shtalinberg@gmail.com"} ] dependencies = [ "django>=3.2.0", ] keywords = ["django", "pagination", "ajax", "endless",] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Utilities", ] [project.urls] Homepage = "http://github.com/shtalinberg/django-el-pagination" Documentation = "http://django-el-pagination.readthedocs.org/" Repository = "https://github.com/shtalinberg/django-el-pagination.git" "Bug Tracker" = "https://github.com/shtalinberg/django-el-pagination/issues" [tool.setuptools] packages = ["el_pagination", "el_pagination.templatetags"] include-package-data = true zip-safe = false [tool.setuptools.dynamic] version = {attr = "el_pagination.get_version"} [tool.setuptools.package-data] el_pagination = [ "templates/el_pagination/*.html", "static/el-pagination/js/*.js", "locale/*/LC_MESSAGES/*.mo", "locale/*/LC_MESSAGES/*.po", "templatetags/*.py", ] [tool.black] line_length = 88 skip-string-normalization = true target-version = ['py38'] include = '\.pyi?$' extend-exclude = ''' /( # The following are specific for Django. | tests | migrations | \.venv )/ ''' [tool.isort] profile = "black" blocked_extensions = [ "rst","html","js","svg","txt","css","scss","png","snap","tsx" ] combine_as_imports = true default_section = "THIRDPARTY" force_grid_wrap = 0 include_trailing_comma = true use_parentheses = true known_django = "django" sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"] skip = ["migrations",".git","__pycache__","LC_MESSAGES"] src_paths = ["el_pagination","tests"] line_length = 88 multi_line_output = 5 no_lines_before="LOCALFOLDER" ================================================ FILE: release-requirements.txt ================================================ build>=1.0.0 twine>=4.0.0 ================================================ FILE: setup.cfg ================================================ [bdist_wheel] universal = 0 [flake8] exclude = docs/*,.tox,.git,build,dist ignore = E123,E128,E402,W503,E731,W601 max-line-length = 119 [isort] not_skip=__init__.py atomic=True multi_line_output=5 include_trailing_comma=True balanced=True [coverage:run] source = el_pagination omit = */tests/* branch = True [metadata] license_files = LICENSE ================================================ FILE: setup.py ================================================ from setuptools import setup if __name__ == "__main__": setup() ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/develop.py ================================================ """Create a development and testing environment using a virtualenv.""" import os import subprocess import sys TESTS = os.path.abspath(os.path.dirname(__file__)) REQUIREMENTS = os.path.join(TESTS, 'requirements.pip') WITH_VENV = os.path.join(TESTS, 'with_venv.sh') VENV = os.path.abspath(os.path.join(TESTS, '..', '.venv')) def call(*args): """Simple ``subprocess.call`` wrapper.""" if subprocess.call(args): raise SystemExit('Error running {0}.'.format(args)) def pip_install(*args): """Install packages using pip inside the venv.""" call(WITH_VENV, '.venv', 'pip', 'install', *args) if __name__ == '__main__': call(sys.executable, '-m', 'venv', VENV) pip_install('-r', REQUIREMENTS) ================================================ FILE: tests/manage.py ================================================ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() ================================================ FILE: tests/project/__init__.py ================================================ ================================================ FILE: tests/project/context_processors.py ================================================ """Navigation bar context processor.""" import platform import django from django.urls import reverse import el_pagination VOICES = ( # Name and label pairs. ('complete', 'Complete example'), ('digg', 'Digg-style'), ('twitter', 'Twitter-style'), ('onscroll', 'On scroll'), ('feed-wrapper', 'Feed wrapper'), ('multiple', 'Multiple'), ('callbacks', 'Callbacks'), ('chunks', 'On scroll/chunks'), ('digg-table', 'Digg-style table'), ('twitter-table', 'Twitter-style table'), ('onscroll-table', 'On scroll table'), ) def navbar(request): """Generate a list of voices for the navigation bar.""" voice_list = [] current_path = request.path for name, label in VOICES: path = reverse(name) voice_list.append( { 'label': label, 'path': path, 'is_active': path == current_path, } ) return {'navbar': voice_list} def versions(request): """Add to context the version numbers of relevant apps.""" values = ( ('Python', platform.python_version()), ('Django', django.get_version()), ('EL Pagination', el_pagination.get_version()), ) return {'versions': values} ================================================ FILE: tests/project/models.py ================================================ from django.db import models def make_model_instances(number): """Make a ``number`` of test model instances and return a queryset.""" for _ in range(number): TestModel.objects.create() return TestModel.objects.all().order_by('pk') class TestModel(models.Model): """A model used in tests.""" class Meta: app_label = 'el_pagination' def __str__(self): return f'TestModel: {self.id}' ================================================ FILE: tests/project/settings.py ================================================ import os import sys """Settings file for the Django project used for tests.""" DEBUG = True ALLOWED_HOSTS = ['*'] # Disable 1.9 arguments '--parallel' and try exclude “Address already in use” at “setUpClass” os.environ['DJANGO_TEST_PROCESSES'] = "1" os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = "localhost:8000-8010,8080,9200-9300" PROJECT_NAME = 'project' # Base paths. ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT = os.path.join(ROOT, PROJECT_NAME) # Django configuration. DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' INSTALLED_APPS = ( 'django.contrib.staticfiles', 'el_pagination', PROJECT_NAME, ) gettext = lambda s: s LANGUAGES = (('en', gettext('English')),) LANGUAGE_CODE = os.getenv('EL_PAGINATION_LANGUAGE_CODE', 'en') ROOT_URLCONF = PROJECT_NAME + '.urls' SECRET_KEY = os.getenv('EL_PAGINATION_SECRET_KEY', 'secret') SITE_ID = 1 STATIC_ROOT = os.path.join(PROJECT, 'static') STATIC_URL = '/static/' USE_TZ = True STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.join(PROJECT, 'templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'debug': DEBUG, 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.template.context_processors.media', 'django.template.context_processors.static', PROJECT_NAME + '.context_processors.navbar', PROJECT_NAME + '.context_processors.versions', ], }, }, ] MIDDLEWARE = ('django.middleware.common.CommonMiddleware',) try: from settings_local import * # noqa INSTALLED_APPS = INSTALLED_APPS + INSTALLED_APPS_LOCAL # noqa except ImportError: sys.stderr.write('settings_local.py not loaded\n') TEMPLATES[0]['OPTIONS']['debug'] = DEBUG ================================================ FILE: tests/project/static/pagination.css ================================================ /* Customized css for the pagination elements. */ .pagination div, .pagination td, .endless_container { display: inline-block; *display: inline; margin-bottom: 0; margin-left: 0; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; *zoom: 1; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .pagination div > a, .pagination td > a, .pagination span, .endless_container a { float: left; padding: 0 14px; line-height: 38px; background-color: #ffffff; border: 1px solid #dddddd; border-left-width: 0; } .pagination div > a:hover, .pagination td > a:hover, .pagination .endless_page_current, .endless_container a:hover { background-color: #eeeeee; } .pagination div > a:first-child, .pagination div > span:first-child, .pagination td > a:first-child, .pagination td > span:first-child, .endless_container a { border-left-width: 1px; -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .endless_container { width: 50%; } .endless_container a { text-align: center; width: inherit; text-decoration: none; } td { padding: 20px; } ================================================ FILE: tests/project/templates/404.html ================================================ ================================================ FILE: tests/project/templates/500.html ================================================ ================================================ FILE: tests/project/templates/base.html ================================================ {% load static %} {% block title %}Testing project{% endblock %} - Django Endless Pagination {% block js %} {% endblock %} ================================================ FILE: tests/project/templates/callbacks/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include page_template %}

Notifications

None so far...
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/callbacks/page.html ================================================ {% load el_pagination_tags %} {% paginate 3 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} {% get_pages %}
Objects {{ pages.current_start_index }} ... {{ pages.current_end_index }} of {{ pages.total_count }}
================================================ FILE: tests/project/templates/chunks/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include "chunks/objects_page.html" %}
{% include "chunks/items_page.html" %}
Objects are paginated on scroll using 3 pages chunks.
Items are paginated on scroll using 4 pages chunks.
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/chunks/items_page.html ================================================ {% load el_pagination_tags %} {% paginate 5 items using "items-page" %} {% for item in items %}

{{ item.title }}

{{ item.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/chunks/objects_page.html ================================================ {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/complete/articles_page.html ================================================ {% load el_pagination_tags %} {% lazy_paginate 3 articles using "articles-page" %} {% for article in articles %}

{{ article.title }}

{{ article.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/complete/entries_page.html ================================================ {% load el_pagination_tags %} {% lazy_paginate 1,3 entries using "entries-page" %} {% for entry in entries %}

{{ entry.title }}

{{ entry.contents }}
{% endfor %} {% show_more %} ================================================ FILE: tests/project/templates/complete/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include "complete/objects_page.html" %} {% comment %}
{% include "complete/objects_simple_page.html" %}
{% include "complete/objects_page.html" %}
{% endcomment %}
{% include "complete/items_page.html" %}
This complete example shows several pagination styles.
Objects are paginated using Digg-style, defaulting to the last page, with no Ajax involved.
Items are paginated using Digg-style with Ajax support.
{% include "complete/entries_page.html" %}
{% include "complete/articles_page.html" %}
Entries are paginated using Twitter-style with Ajax enabled. The first page contains just one entry. Subsequent pages contain three entries.
Articles are paginated using Twitter-style. The subsequent pages are loaded on scroll.
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/complete/items_page.html ================================================ {% load el_pagination_tags %} {% paginate 3 items using "items-page" %} {% for item in items %}

{{ item.title }}

{{ item.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/complete/objects_page.html ================================================ {% load el_pagination_tags %} {% paginate 3 objects starting from page -1 using "objects-page" %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/complete/objects_simple_page.html ================================================ {% load el_pagination_tags %} {% paginate 3 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/digg/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/digg/page.html ================================================ {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/digg/table/index.html ================================================ {% extends "base.html" %} {% block content %} {% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/digg/table/page.html ================================================ {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }} {% endfor %} {% show_pages %} ================================================ FILE: tests/project/templates/feed_wrapper/index.html ================================================ {% extends "base.html" %} {% load el_pagination_tags %} {% block content %} {% lazy_paginate 10 objects %} {% include page_template %}
Object
{% show_more "More results" %} {% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/feed_wrapper/page.html ================================================ {% load el_pagination_tags %} {% if request.is_ajax %} {% lazy_paginate 10 objects %} {% endif %} {% for object in objects %} {{ object.title }} {% endfor %} ================================================ FILE: tests/project/templates/home.html ================================================ {% extends "base.html" %} {% block content %}

This project is intended to be used as a testing environment for Django EL(Endless) Pagination.

This project also contains a basic collection of examples on how to use this application, providing both the ability to manually test Django EL(Endless) Pagination in the browser, and a demo showing some of the application features in action.

The documentation is avaliable online or in the docs directory of the project.
The source code for this app is hosted at Github and

The template for this project is realized using the Bootstrap framework. However, Bootstrap is not required in order to use Django EL(Endless) Pagination.

{% endblock %} ================================================ FILE: tests/project/templates/multiple/entries_page.html ================================================ {% load el_pagination_tags %} {% lazy_paginate 1,3 entries using "entries-page" %} {% for entry in entries %}

{{ entry.title }}

{{ entry.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/multiple/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include "multiple/objects_page.html" %}
{% include "multiple/items_page.html" %}
{% include "multiple/entries_page.html" %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/multiple/items_page.html ================================================ {% load el_pagination_tags %} {% paginate 3 items using "items-page" %} {% for item in items %}

{{ item.title }}

{{ item.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/multiple/objects_page.html ================================================ {% load el_pagination_tags %} {% paginate 3 objects using "objects-page" %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} ================================================ FILE: tests/project/templates/onscroll/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/onscroll/page.html ================================================ {% load el_pagination_tags %} {% paginate 10 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/onscroll/table/index.html ================================================ {% extends "base.html" %} {% block content %} {% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/onscroll/table/page.html ================================================ {% load el_pagination_tags %} {% paginate 10 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }} {% endfor %} {% show_more_table "More results" %} ================================================ FILE: tests/project/templates/twitter/index.html ================================================ {% extends "base.html" %} {% block content %}
{% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/twitter/page.html ================================================ {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }}
{% endfor %} {% show_more "More results" %} ================================================ FILE: tests/project/templates/twitter/table/index.html ================================================ {% extends "base.html" %} {% block content %} {% include page_template %}
{% endblock %} {% block js %} {{ block.super }} {% endblock %} ================================================ FILE: tests/project/templates/twitter/table/page.html ================================================ {% load el_pagination_tags %} {% paginate 5 objects %} {% for object in objects %}

{{ object.title }}

{{ object.contents }} {% endfor %} {% show_more_table "More results" %} ================================================ FILE: tests/project/urls.py ================================================ """Test project URL patterns.""" from django.conf import settings from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, re_path as url from django.views.generic import TemplateView from el_pagination.decorators import page_template, page_templates from project.views import generic # Avoid lint errors for the following Django idiom: flake8: noqa. urlpatterns = [ url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), url( r'^complete/$', page_templates( { 'complete/objects_page.html': 'objects-page', 'complete/items_page.html': 'items-page', 'complete/entries_page.html': 'entries-page', 'complete/articles_page.html': 'articles-page', } )(generic), {'template': 'complete/index.html', 'number': 21}, name='complete', ), url( r'^digg/$', page_template('digg/page.html')(generic), {'template': 'digg/index.html'}, name='digg', ), url( r'^digg/table$', page_template('digg/table/page.html')(generic), {'template': 'digg/table/index.html'}, name='digg-table', ), url( r'^twitter/$', page_template('twitter/page.html')(generic), {'template': 'twitter/index.html'}, name='twitter', ), url( r'^twitter/table$', page_template('twitter/table/page.html')(generic), {'template': 'twitter/table/index.html'}, name='twitter-table', ), url( r'^onscroll/$', page_template('onscroll/page.html')(generic), {'template': 'onscroll/index.html'}, name='onscroll', ), url( r'^onscroll/table$', page_template('onscroll/table/page.html')(generic), {'template': 'onscroll/table/index.html'}, name='onscroll-table', ), url( r'^feed-wrapper/$', page_template('feed_wrapper/page.html')(generic), {'template': 'feed_wrapper/index.html'}, name='feed-wrapper', ), url( r'^chunks/$', page_templates( { 'chunks/objects_page.html': None, 'chunks/items_page.html': 'items-page', } )(generic), {'template': 'chunks/index.html', 'number': 50}, name='chunks', ), url( r'^multiple/$', page_templates( { 'multiple/objects_page.html': 'objects-page', 'multiple/items_page.html': 'items-page', 'multiple/entries_page.html': 'entries-page', } )(generic), {'template': 'multiple/index.html', 'number': 21}, name='multiple', ), url( r'^callbacks/$', page_template('callbacks/page.html')(generic), {'template': 'callbacks/index.html'}, name='callbacks', ), ] if settings.DEBUG: if 'debug_toolbar' in settings.INSTALLED_APPS: import debug_toolbar urlpatterns += [ url(r'^__debug__/', include(debug_toolbar.urls)), ] urlpatterns += staticfiles_urlpatterns() ================================================ FILE: tests/project/views.py ================================================ """Test project views.""" from django.shortcuts import render from django.views.generic import ListView LOREM = """Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. """ def _make(title, number): """Make a *number* of items.""" return [ {'title': '{0} {1}'.format(title, i + 1), 'contents': LOREM} for i in range(number) ] def generic(request, extra_context=None, template=None, number=50): context = { 'objects': _make('Object', number), 'items': _make('Item', number), 'entries': _make('Entry', number), 'articles': _make('Article', number), } if extra_context is not None: context.update(extra_context) return render(request, template, context) class SearchListView(ListView): pass ================================================ FILE: tests/requirements.pip ================================================ # Django Endless Pagination test requirements. # Dependencies are installed by the ``make`` command. django>=3.2.0 codecov coverage==7.2.2 black==24.10.0 isort==5.13.2 flake8==7.1.1 pylint==3.3.1 pylint-django==2.6.1 ipdb selenium<4.0 #xvfbwrapper==0.2.9 django-test-without-migrations==0.6 # Docs sphinx>=5.0.0,<6.0.0 sphinx-rtd-theme>=1.0.0 jinja2>=3.0.0 docutils>=0.17.1 sphinxcontrib-applehelp>=1.0.4 ================================================ FILE: tests/with_venv.sh ================================================ #!/bin/bash export DJANGO_LIVE_TEST_SERVER_ADDRESS="localhost:8000-8010,8080,9200-9300" export DJANGO_TEST_PROCESSES="1" TESTS=`dirname $0` VENV=$TESTS/../$1 shift if [ ! -f "$VENV/bin/activate" ]; then echo "Virtual environment not found at $VENV" echo "Please run 'make develop' first" exit 1 fi source "$VENV/bin/activate" exec "$@" ================================================ FILE: tox.ini ================================================ [tox] envlist = py3{8,9,10,11,12}-django42 py3{12}-django50 py3{10,11,12,13}-django51 py3{10,11,12,13}-django52 py3{12,13}-djdev black isort flake8 docs # Default testenv [testenv] passenv = CI USE_SELENIUM deps = -r{toxinidir}/tests/requirements.pip django-42: Django>=4.2,<4.3 django-50: Django>=5.0,<5.1 django-51: Django>=5.1,<5.2 django-52: Django>=5.2,<5.3 djdev: https://github.com/django/django/archive/master.tar.gz commands = {envpython} --version {envpython} -Wd {envbindir}/coverage run --branch {toxinidir}/tests/manage.py test coverage report -m setenv = DJANGO_SETTINGS_MODULE=project.settings PYTHONPATH={toxinidir} DJANGO_LIVE_TEST_SERVER_ADDRESS=localhost:8000-8010,8080,9200-9300 DJANGO_TEST_PROCESSES=1 basepython = py313: python3.13 py312: python3.12 py311: python3.11 py310: python3.10 py39: python3.9 py38: python3.8 [testenv:black] basepython = python3 usedevelop = false deps = black changedir = {toxinidir} commands = black --check --diff . [testenv:flake8] basepython = python3 usedevelop = false deps = flake8 >= 3.7.0 changedir = {toxinidir} commands = flake8 . [testenv:isort] basepython = python3 usedevelop = false deps = isort >= 5.1.0 changedir = {toxinidir} commands = isort --check-only --diff django tests scripts ########################### # Run docs builder ########################### [testenv:docs] deps = sphinx sphinx_rtd_theme changedir=doc commands = sphinx-build -W -b html -d {envtmpdir}/doctrees doc doc/_build/html ########################### # Run docs linkcheck ########################### [testenv:docs-linkcheck] deps = {[testenv:docs]deps} commands = sphinx-build -b html -d {envtmpdir}/doctrees doc doc/_build/html sphinx-build -b linkcheck doc doc/_build/html [pep8] exclude = migrations,.tox,doc,docs,tests,setup.py [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 3.13: py313