├── sorter ├── models.py ├── templatetags │ ├── __init__.py │ └── sorter_tags.py ├── __init__.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── en │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── templates │ └── sorter │ │ ├── sortlink.html │ │ └── sortform.html ├── utils.py ├── conf.py ├── test_settings.py └── tests.py ├── AUTHORS ├── requirements └── tests.txt ├── .gitignore ├── MANIFEST.in ├── docs ├── index.rst ├── changelog.rst ├── settings.rst ├── Makefile ├── make.bat ├── conf.py └── usage.rst ├── Makefile ├── .tx └── config ├── CONTRIBUTING.md ├── .travis.yml ├── LICENSE ├── setup.py └── README.rst /sorter/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sorter/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jannis Leidel -------------------------------------------------------------------------------- /sorter/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3" 2 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | docutils 2 | model_mommy 3 | django-ttag>=2.3 4 | django-discover-runner 5 | coverage 6 | flake8 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | MANIFEST 4 | *.pyc 5 | *.egg-info 6 | .tox/ 7 | *.egg 8 | docs/_build/ 9 | .coverage 10 | -------------------------------------------------------------------------------- /sorter/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-sorter/HEAD/sorter/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sorter/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-sorter/HEAD/sorter/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include sorter/templates *.html 5 | recursive-include sorter/locale *.po *.mo 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Contents: 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | usage 9 | settings 10 | changelog 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | coverage run --branch --source=sorter `which django-admin.py` test --settings=sorter.test_settings sorter 3 | coverage report --omit=sorter/test* 4 | -------------------------------------------------------------------------------- /sorter/templates/sorter/sortlink.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{{ label }} -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.net 3 | 4 | [django-sorter.main] 5 | file_filter = sorter/locale//LC_MESSAGES/django.po 6 | source_file = sorter/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | 9 | -------------------------------------------------------------------------------- /sorter/templates/sorter/sortform.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Condut](https://jazzband.co/docs/conduct) and follow the [guidelines](https://jazzband.co/docs/guidelines). 4 | -------------------------------------------------------------------------------- /sorter/utils.py: -------------------------------------------------------------------------------- 1 | from itertools import tee, izip, chain 2 | 3 | 4 | def cycle_pairs(iterable): 5 | """ 6 | Cycles through the given iterable, returning an iterator which 7 | returns the current and the next item. When reaching the end 8 | it returns the last and the first item. 9 | """ 10 | first, last = iterable[0], iterable[-1] 11 | a, b = tee(iterable) 12 | iter(b).next() 13 | return chain(izip(a, b), [(last, first)]) 14 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.2 (2012-05-26) 5 | ----------------- 6 | 7 | - Made sure the ordering isn't reset when no querystring parameter is passed. 8 | 9 | - Moved to Travis for testing: http://travis-ci.org/jazzband/django-sorter 10 | 11 | - Updated dependency of URLObject to > 2.0.1. This could backwards 12 | incomaptible if you're using a previous version (< 2.0). 13 | 14 | - Dropped support for Python 2.5. 15 | 16 | v0.1 (2011-09-07) 17 | ----------------- 18 | 19 | - Initial release, yay! 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | before_install: 6 | - export DJANGO_SETTINGS_MODULE=sorter.test_settings 7 | install: 8 | - pip install -e . 9 | - pip install -r requirements/tests.txt Django==$DJANGO 10 | before_script: 11 | - flake8 sorter --ignore=E501 12 | script: 13 | - coverage run --branch --source=sorter `which django-admin.py` test --settings=sorter.test_settings sorter 14 | - coverage report --omit=sorter/test* 15 | env: 16 | - DJANGO=1.3.1 17 | - DJANGO=1.4 18 | branches: 19 | only: 20 | - develop 21 | -------------------------------------------------------------------------------- /sorter/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings # noqa 2 | from django.core.exceptions import ImproperlyConfigured 3 | from appconf import AppConf 4 | 5 | 6 | class SorterConf(AppConf): 7 | DEFAULT_QUERY_NAME = 'sort' 8 | ALLOWED_CRITERIA = None 9 | 10 | def configure_ALLOWED_CRITERIA(self, value): 11 | if not value: 12 | raise ImproperlyConfigured("The SORTER_ALLOWED_CRITERIA " 13 | "setting is empty. Please set it.") 14 | for name, criteria in value.items(): 15 | if not name: 16 | raise ImproperlyConfigured("The '%s' SORTER_ALLOWED_CRITERIA " 17 | "setting is empty. Please set it." % 18 | name) 19 | return value or {} 20 | -------------------------------------------------------------------------------- /sorter/test_settings.py: -------------------------------------------------------------------------------- 1 | SITE_ID = 1 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3', 6 | 'NAME': ':memory:', 7 | } 8 | } 9 | 10 | INSTALLED_APPS = [ 11 | 'django.contrib.contenttypes', 12 | 'django.contrib.sites', 13 | 'django.contrib.auth', 14 | 'django.contrib.admin', 15 | 'sorter', 16 | ] 17 | 18 | TEMPLATES = [ 19 | { 20 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 21 | 'APP_DIRS': True, 22 | 'OPTIONS': { 23 | 'context_processors': ( 24 | "django.contrib.auth.context_processors.auth", 25 | ), 26 | 'builtins': [ 27 | 'sorter.templatetags.sorter_tags', 28 | 'sorter.tests', 29 | ] 30 | }, 31 | }, 32 | ] 33 | 34 | SECRET_KEY = 'something-something' 35 | -------------------------------------------------------------------------------- /sorter/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: 0.1\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2011-09-01 16:40+0200\n" 7 | "PO-Revision-Date: 2011-09-01 16:26+0200\n" 8 | "Last-Translator: Jannis Leidel \n" 9 | "Language-Team: en \n" 10 | "Language: \n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | 15 | #. Translators: Used in title of descending sort fields 16 | #: templatetags/sorter_tags.py:137 17 | #, python-format 18 | msgid "'%(sort_field)s' (desc)" 19 | msgstr "" 20 | 21 | #. Translators: Used in title of ascending sort fields 22 | #: templatetags/sorter_tags.py:140 23 | #, python-format 24 | msgid "'%(sort_field)s' (asc)" 25 | msgstr "" 26 | 27 | #. Translators: Used for the link/form input title excluding the sort fields 28 | #: templatetags/sorter_tags.py:143 29 | #, python-format 30 | msgid "Sort by: %(sort_fields)s" 31 | msgstr "" 32 | 33 | #: templatetags/sorter_tags.py:144 34 | msgid "and" 35 | msgstr "" 36 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | SORTER_DEFAULT_QUERY_NAME 5 | ------------------------- 6 | 7 | Default: ``'sort'`` 8 | 9 | The name of the querystring used by default when looking 10 | at the current request path or generating links and or forms. 11 | 12 | SORTER_ALLOWED_CRITERIA 13 | ----------------------- 14 | 15 | Default: ``{}`` 16 | 17 | A mapping of query names to order field names that are checked before 18 | ordering is applied. 19 | 20 | The given names support Unix shell-style wildcards and define those 21 | that are *allowed*, e.g. ``'author__*'``. 22 | 23 | .. warning:: 24 | 25 | If the setting is empty, **no fields** will be allowed which renders 26 | the template tags useless. Hence, it's an **configuration error** to 27 | not define this setting. 28 | 29 | An example, which would apply to sort links like ``'/path/?sort=created'`` 30 | and ``'/path/?sort_posts=modified,author__username'``. 31 | 32 | :: 33 | 34 | SORTER_ALLOWED_CRITERIA = { 35 | 'sort': ['created', 'title'], 36 | 'sort_posts': ['modified', 'author__*'], 37 | } 38 | -------------------------------------------------------------------------------- /sorter/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # 2 | # Jannis Leidel , 2011. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: django-sorter\n" 6 | "Report-Msgid-Bugs-To: http://github.com/jezdez/django-sorter/issues\n" 7 | "POT-Creation-Date: 2011-09-01 16:40+0200\n" 8 | "PO-Revision-Date: 2011-09-01 15:21+0000\n" 9 | "Last-Translator: Jannis \n" 10 | "Language-Team: en \n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Language: de\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | 17 | #. Translators: Used in title of descending sort fields 18 | #: templatetags/sorter_tags.py:137 19 | #, python-format 20 | msgid "'%(sort_field)s' (desc)" 21 | msgstr "'%(sort_field)s' (absteigend)" 22 | 23 | #. Translators: Used in title of ascending sort fields 24 | #: templatetags/sorter_tags.py:140 25 | #, python-format 26 | msgid "'%(sort_field)s' (asc)" 27 | msgstr "'%(sort_field)s' (aufsteigend)" 28 | 29 | #. Translators: Used for the link/form input title excluding the sort fields 30 | #: templatetags/sorter_tags.py:143 31 | #, python-format 32 | msgid "Sort by: %(sort_fields)s" 33 | msgstr "Sortieren nach: %(sort_fields)s" 34 | 35 | #: templatetags/sorter_tags.py:144 36 | msgid "and" 37 | msgstr "und" 38 | 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012, Jannis Leidel and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of django-appconf nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | import re 4 | from setuptools import setup 5 | 6 | 7 | def read(*parts): 8 | return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read() 9 | 10 | 11 | def find_version(*file_paths): 12 | version_file = read(*file_paths) 13 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 14 | version_file, re.M) 15 | if version_match: 16 | return version_match.group(1) 17 | raise RuntimeError("Unable to find version string.") 18 | 19 | 20 | setup( 21 | name='django-sorter', 22 | version=find_version("sorter", "__init__.py"), 23 | description='A helper app for sorting objects in Django templates.', 24 | long_description=read('README.rst'), 25 | author='Jannis Leidel', 26 | author_email='jannis@leidel.info', 27 | license='BSD', 28 | url='https://django-sorter.readthedocs.io/', 29 | packages=['sorter', 'sorter.templatetags'], 30 | package_data={ 31 | 'sorter': [ 32 | 'templates/sorter/*.html', 33 | 'locale/*/*/*', 34 | ], 35 | }, 36 | classifiers=[ 37 | "Development Status :: 4 - Beta", 38 | 'Environment :: Web Environment', 39 | 'Framework :: Django', 40 | 'Framework :: Django :: 1.9', 41 | 'Framework :: Django :: 1.10', 42 | 'Framework :: Django :: 1.11', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: BSD License', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 2.5', 48 | 'Programming Language :: Python :: 2.6', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Topic :: Utilities', 51 | ], 52 | install_requires=[ 53 | 'django-appconf >= 0.4', 54 | 'django-ttag >= 2.3', 55 | 'URLObject >= 2.0.1', 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-sorter 2 | ============= 3 | 4 | .. image:: https://jazzband.co/static/img/badge.svg 5 | :target: https://jazzband.co/ 6 | :alt: Jazzband 7 | 8 | ``django-sorter`` helps with sorting objects in Django templates without 9 | modifying your views, can be used multiple times on the same page or 10 | template, provides helpers to easily generate links and forms to switch 11 | the sorting criteria (including the sort order) and has ~100% test coverage. 12 | 13 | Quickstart 14 | ---------- 15 | 16 | #. Get the app with your favorte Python packaging tool, e.g.:: 17 | 18 | pip install django-sorter 19 | 20 | #. List this application in the ``INSTALLED_APPS`` setting. 21 | Your settings file might look something like:: 22 | 23 | INSTALLED_APPS = ( 24 | # ... 25 | 'sorter', 26 | ) 27 | 28 | #. If it's not already added in your setup, add the ``request`` template 29 | context processor to the ``TEMPLATE_CONTEXT_PROCESSORS`` setting 30 | (you might need to `add it`_):: 31 | 32 | TEMPLATE_CONTEXT_PROCESSORS = ( 33 | # ... 34 | 'django.core.context_processors.request', 35 | ) 36 | 37 | #. Specify the allowed sorting criteria, for at least the default 38 | ``'sort'`` sorting querystring parameter:: 39 | 40 | SORTER_ALLOWED_CRITERIA = { 41 | 'sort': ['first_name', 'creation_date', 'title'], 42 | } 43 | 44 | #. Add this line at the top of your template to load the sorting tags:: 45 | 46 | {% load sorter_tags %} 47 | 48 | #. Decide on a variable that you would like to sort, and use the 49 | sort tag on that variable before iterating over it. 50 | 51 | :: 52 | 53 | {% sort objects as sorted_objects %} 54 | 55 | #. Optionally, you can display different sort links or forms:: 56 | 57 | 58 | {% sortlink by "first_name" %}By first name{% endsortlink %} 59 | {% sortlink by "creation_date,-title" %}By creation date and title{% endsortlink %} 60 | ... 61 | 62 | 63 | The template tag takes a comma separated list of sorting statements. 64 | It also is a block tag and allows you to set the label of the generated 65 | link. The previous snippet will be rendered like this:: 66 | 67 | 68 | By name 69 | By creation and title 70 | ... 71 | 72 | 73 | Similarly the ``{% sortform %}`` template tag renders a form instead of 74 | a simple link. 75 | 76 | .. _`add it`: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 77 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-sorter.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-sorter.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-sorter" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-sorter" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-sorter.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-sorter.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /sorter/templatetags/sorter_tags.py: -------------------------------------------------------------------------------- 1 | from fnmatch import fnmatch 2 | from urlobject import URLObject 3 | 4 | from django import template 5 | from django.template import TemplateSyntaxError 6 | from django.template.loader import render_to_string 7 | from django.utils.six import string_types 8 | from django.utils.translation import ugettext as _ 9 | from django.utils.text import get_text_list 10 | 11 | import ttag 12 | 13 | from sorter.conf import settings 14 | from sorter.utils import cycle_pairs 15 | 16 | register = template.Library() 17 | 18 | 19 | class SorterAsTag(ttag.helpers.AsTag): 20 | 21 | def clean(self, data, context): 22 | """ 23 | Checks if there is a ``request`` variable 24 | included in the context. 25 | """ 26 | request = context.get('request') 27 | if not request: 28 | raise TemplateSyntaxError("Couldn't find request in context: %s" % 29 | context) 30 | return super(SorterAsTag, self).clean(data, context) 31 | 32 | def clean_with(self, value): 33 | """ 34 | Cleans the given name of the sort query 35 | """ 36 | if not isinstance(value, string_types): 37 | raise TemplateSyntaxError("Value '%s' is not a string" % value) 38 | # in case the value equals the default query name 39 | # or it already has the default query name prefixed 40 | if (value == settings.SORTER_DEFAULT_QUERY_NAME or 41 | value.startswith(settings.SORTER_DEFAULT_QUERY_NAME)): 42 | return value 43 | return '%s_%s' % (settings.SORTER_DEFAULT_QUERY_NAME, value) 44 | 45 | 46 | class Sort(SorterAsTag): 47 | """ 48 | {% sort queryset [with NAME] as VARIABLE %} 49 | 50 | {% sort object_list with "objects" as sorted_objects %} 51 | 52 | """ 53 | data = ttag.Arg() 54 | with_ = ttag.Arg(named=True, required=False, default=settings.SORTER_DEFAULT_QUERY_NAME) 55 | 56 | def as_value(self, data, context): 57 | value = data['data'] 58 | ordering = self.ordering(context, data['with']) 59 | if ordering: 60 | return value.order_by(*ordering) 61 | return value 62 | 63 | def ordering(self, context, name): 64 | """ 65 | Given the template context and the name of the sorting 66 | should return a list of ordering values. 67 | """ 68 | try: 69 | sort_fields = context['request'].GET[name].split(',') 70 | except (KeyError, ValueError, TypeError): 71 | return [] 72 | result = [] 73 | allowed_criteria = settings.SORTER_ALLOWED_CRITERIA.get(name) 74 | if allowed_criteria is None: 75 | return result 76 | for sort_field in sort_fields: 77 | for criteria in allowed_criteria: 78 | if fnmatch(sort_field.lstrip('-'), criteria): 79 | result.append(sort_field) 80 | return result 81 | 82 | 83 | class TemplateAsTagOptions(ttag.helpers.as_tag.AsTagOptions): 84 | 85 | def __init__(self, meta, *args, **kwargs): 86 | super(TemplateAsTagOptions, self).__init__(meta=meta, *args, **kwargs) 87 | self.template_name = getattr(meta, 'template_name', 'sortlink') 88 | 89 | 90 | class TemplateAsTagMetaclass(ttag.helpers.as_tag.AsTagMetaclass): 91 | options_class = TemplateAsTagOptions 92 | 93 | 94 | class SortURL(SorterAsTag): 95 | """ 96 | Parses a tag that's supposed to be in this format: 97 | 98 | {% sorturl [with NAME] [rel REL] [class CLASS] [as VARIABLE] by ORDER_A1[,ORDER_A2,..] [ORDER_B1[,ORDER_B2,..]] .. %} 99 | 100 | {% sorturl with "objects" by "creation_date,-title" %} 101 | 102 | """ 103 | __metaclass__ = TemplateAsTagMetaclass 104 | 105 | with_ = ttag.Arg(required=False, named=True, default=settings.SORTER_DEFAULT_QUERY_NAME) 106 | rel = ttag.Arg(required=False, named=True) 107 | class_ = ttag.Arg(required=False, named=True) 108 | by = ttag.MultiArg(named=True) 109 | 110 | class Meta: 111 | as_required = False 112 | template_name = 'sorturl' 113 | name = 'sorturl' 114 | 115 | def as_value(self, data, context): 116 | # The queries of the current URL, not using sequences here 117 | # since the order of sorting arguments matter 118 | url = URLObject(context['request'].get_full_path()) 119 | queries = url.query.dict 120 | 121 | name, orderings = data['with'], data['by'] 122 | query = self.find_query(queries.get(name), orderings, orderings[0]) 123 | url = url.set_query_param(name, query) 124 | 125 | # If this isn't a block tag we probably only want the URL 126 | if not self._meta.block: 127 | return url 128 | 129 | label = self.nodelist.render(context) 130 | if not label.strip(): 131 | raise TemplateSyntaxError("No label was specified") 132 | 133 | parts = [] 134 | for part in query.split(','): 135 | part = part.strip() 136 | if part.startswith('-'): 137 | part = part.lstrip('-') 138 | # Translators: Used in title of descending sort fields 139 | text = _("'%(sort_field)s' (desc)") 140 | else: 141 | # Translators: Used in title of ascending sort fields 142 | text = _("'%(sort_field)s' (asc)") 143 | parts.append(text % {'sort_field': part}) 144 | # Translators: Used for the link/form input title excluding the sort fields 145 | title = (_('Sort by: %(sort_fields)s') % 146 | {'sort_fields': get_text_list(parts, _('and'))}) 147 | 148 | extra_context = dict(data, title=title, label=label, url=url, query=query) 149 | extra_context.update(context.flatten()) 150 | return render_to_string(self.using(data), extra_context) 151 | 152 | def find_query(self, wanted, orderings, default): 153 | """ 154 | Given the list of order statements and a query that is currently 155 | found in the request's querystring returns the next in line, 156 | or falls back to the given default. 157 | """ 158 | for current, next in cycle_pairs(orderings): 159 | if current == wanted: 160 | return next 161 | return default 162 | 163 | def using(self, data): 164 | """ 165 | This template tag will use 'sorter/sorturl.html' by default, 166 | but uses 'sorter/sorturl_NAME.html' additionally if the 167 | 'with' argument is given. 168 | """ 169 | name = data.get('with') 170 | template_names = [self._meta.template_name] 171 | if name and name != settings.SORTER_DEFAULT_QUERY_NAME: 172 | template_names.append(u'%s_%s' % (self._meta.template_name, name)) 173 | return [u"sorter/%s.html" % name for name in template_names] 174 | 175 | 176 | class Sortlink(SortURL): 177 | """ 178 | Parses a tag that's supposed to be in this format: 179 | 180 | {% sortlink [with NAME] [rel REL] [class CLASS] [as VARIABLE] by ORDER_A1[,ORDER_A2,..] [ORDER_B1[,ORDER_B2,..]] .. %} 181 | LABEL 182 | {% endsortlink %} 183 | 184 | {% sortlink with "objects" by "creation_date,-title" %} 185 | {% trans "Creation and title" %} 186 | {% endsortlink %} 187 | 188 | """ 189 | class Meta: 190 | block = True 191 | as_required = False 192 | template_name = 'sortlink' 193 | 194 | 195 | class Sortform(SortURL): 196 | """ 197 | Parses a tag that's supposed to be in this format: 198 | 199 | {% sortform [with NAME] [rel REL] [class CLASS] [as VARIABLE] by ORDER_A1[,ORDER_A2,..] [ORDER_B1[,ORDER_B2,..]] .. %} 200 | LABEL 201 | {% endsortform %} 202 | 203 | {% sortform with "objects" by "creation_date,-title" %} 204 | {% trans "Creation and title" %} 205 | {% endsortform %} 206 | 207 | """ 208 | class Meta: 209 | block = True 210 | as_required = False 211 | template_name = 'sortform' 212 | 213 | 214 | register.tag(Sort) 215 | register.tag(SortURL) 216 | register.tag(Sortlink) 217 | register.tag(Sortform) 218 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-sorter documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 1 19:29:09 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | from django.conf import settings 22 | 23 | settings.configure() 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'django-sorter' 48 | copyright = u'2011-2012, Jannis Leidel and individual contributors' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | try: 55 | from sorter import __version__ 56 | # The short X.Y version. 57 | version = '.'.join(__version__.split('.')[:2]) 58 | # The full version, including alpha/beta/rc tags. 59 | release = __version__ 60 | except ImportError: 61 | version = release = 'dev' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = ['_build'] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all documents. 78 | #default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | #add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | #add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | #show_authors = False 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | #modindex_common_prefix = [] 96 | 97 | 98 | # -- Options for HTML output --------------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | html_theme = 'default' 103 | 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | #html_theme_options = {} 108 | 109 | # Add any paths that contain custom themes here, relative to this directory. 110 | #html_theme_path = [] 111 | 112 | # The name for this set of Sphinx documents. If None, it defaults to 113 | # " v documentation". 114 | #html_title = None 115 | 116 | # A shorter title for the navigation bar. Default is the same as html_title. 117 | #html_short_title = None 118 | 119 | # The name of an image file (relative to this directory) to place at the top 120 | # of the sidebar. 121 | #html_logo = None 122 | 123 | # The name of an image file (within the static path) to use as favicon of the 124 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 125 | # pixels large. 126 | #html_favicon = None 127 | 128 | # Add any paths that contain custom static files (such as style sheets) here, 129 | # relative to this directory. They are copied after the builtin static files, 130 | # so a file named "default.css" will overwrite the builtin "default.css". 131 | html_static_path = ['_static'] 132 | 133 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 134 | # using the given strftime format. 135 | #html_last_updated_fmt = '%b %d, %Y' 136 | 137 | # If true, SmartyPants will be used to convert quotes and dashes to 138 | # typographically correct entities. 139 | #html_use_smartypants = True 140 | 141 | # Custom sidebar templates, maps document names to template names. 142 | #html_sidebars = {} 143 | 144 | # Additional templates that should be rendered to pages, maps page names to 145 | # template names. 146 | #html_additional_pages = {} 147 | 148 | # If false, no module index is generated. 149 | #html_domain_indices = True 150 | 151 | # If false, no index is generated. 152 | #html_use_index = True 153 | 154 | # If true, the index is split into individual pages for each letter. 155 | #html_split_index = False 156 | 157 | # If true, links to the reST sources are added to the pages. 158 | #html_show_sourcelink = True 159 | 160 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 161 | #html_show_sphinx = True 162 | 163 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 164 | #html_show_copyright = True 165 | 166 | # If true, an OpenSearch description file will be output, and all pages will 167 | # contain a tag referring to it. The value of this option must be the 168 | # base URL from which the finished HTML is served. 169 | #html_use_opensearch = '' 170 | 171 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 172 | #html_file_suffix = None 173 | 174 | # Output file base name for HTML help builder. 175 | htmlhelp_basename = 'django-sorter-doc' 176 | 177 | 178 | # -- Options for LaTeX output -------------------------------------------------- 179 | 180 | # The paper size ('letter' or 'a4'). 181 | #latex_paper_size = 'letter' 182 | 183 | # The font size ('10pt', '11pt' or '12pt'). 184 | #latex_font_size = '10pt' 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'django-sorter.tex', u'django-sorter Documentation', 190 | u'Jannis Leidel and individual contributors', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Additional stuff for the LaTeX preamble. 208 | #latex_preamble = '' 209 | 210 | # Documents to append as an appendix to all manuals. 211 | #latex_appendices = [] 212 | 213 | # If false, no module index is generated. 214 | #latex_domain_indices = True 215 | 216 | 217 | # -- Options for manual page output -------------------------------------------- 218 | 219 | # One entry per manual page. List of tuples 220 | # (source start file, name, description, authors, manual section). 221 | man_pages = [ 222 | ('index', 'django-sorter', u'django-sorter Documentation', 223 | [u'Jannis Leidel and individual contributors'], 1) 224 | ] 225 | 226 | 227 | # Example configuration for intersphinx: refer to the Python standard library. 228 | intersphinx_mapping = {'http://docs.python.org/': None} 229 | -------------------------------------------------------------------------------- /sorter/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.admin.models import LogEntry 3 | from django.http import HttpResponse 4 | from django.template import Library, Template, Context, TemplateSyntaxError 5 | from django.test import TestCase 6 | from django.test.client import RequestFactory 7 | 8 | from model_mommy import mommy 9 | 10 | from sorter.conf import settings 11 | from sorter.utils import cycle_pairs 12 | 13 | register = Library() 14 | 15 | 16 | @register.filter 17 | def sorter_tests_pks(value): 18 | pk_list = [] 19 | for obj in value: 20 | pk_list.append(str(obj.pk)) 21 | if pk_list: 22 | return u'.'.join(pk_list) 23 | return '' 24 | 25 | 26 | class SorterTestCase(TestCase): 27 | 28 | def setUp(self): 29 | self.rf = RequestFactory() 30 | self.old_sorter_allowed_criteria = settings.SORTER_ALLOWED_CRITERIA 31 | settings.SORTER_ALLOWED_CRITERIA = { 32 | 'sort': ['*'], 33 | 'sort_objects': ['*'], 34 | 'sort1': ['*'], 35 | 'sort2': ['*'], 36 | 'sort_others': ['*'], 37 | } 38 | 39 | def tearDown(self): 40 | settings.SORTER_ALLOWED_CRITERIA = self.old_sorter_allowed_criteria 41 | 42 | def create_entries(self, count, **kwargs): 43 | entries = [mommy.make(LogEntry, **kwargs) for i in range(count)] 44 | return LogEntry.objects.filter(pk__in=[entry.pk for entry in entries]) 45 | 46 | def create_response(self, request, template, context=None): 47 | return HttpResponse(Template(template).render(Context(context))) 48 | 49 | def create_context(self, **kwargs): 50 | context = {} 51 | context.update(kwargs) 52 | return context 53 | 54 | def assertViewRenders(self, template, result, query=None, func=None, **kwargs): 55 | # Create an instance of a GET request. 56 | request = self.rf.get('/', data=query or {}) 57 | context = self.create_context(request=request, **kwargs) 58 | response = self.create_response(request, template, context) 59 | if func is None: 60 | func = self.assertContains 61 | func(response, result, msg_prefix="Got: '%s'" % response.content.strip()) 62 | 63 | def assertViewNotRenders(self, template, result, query=None, **kwargs): 64 | self.assertViewRenders(template, result, query=None, 65 | func=self.assertNotContains, **kwargs) 66 | 67 | def assertViewRaises(self, exception, template, query=None, with_request=True, **kwargs): 68 | request = self.rf.get('/', data=query or {}) 69 | context = self.create_context(**kwargs) 70 | if with_request: 71 | context['request'] = request 72 | self.assertRaises(exception, self.create_response, request, template, context) 73 | 74 | 75 | class SortTests(SorterTestCase): 76 | 77 | def setUp(self): 78 | super(SortTests, self).setUp() 79 | self.entry1, self.entry2, self.entry3 = self.create_entries(3) 80 | 81 | def tearDown(self): 82 | self.entry1.delete() 83 | self.entry2.delete() 84 | self.entry3.delete() 85 | 86 | def test_simple(self): 87 | self.assertViewRenders( 88 | "{% sort objects as objects %}{{ objects|sorter_tests_pks }}", 89 | "1.2.3", {'sort': 'id'}, objects=LogEntry.objects.all()) 90 | self.assertViewRenders( 91 | "{% sort objects as objects %}{{ objects|sorter_tests_pks }}", 92 | "3.2.1", {'sort': '-id'}, objects=LogEntry.objects.all()) 93 | self.assertViewNotRenders( 94 | "{% sort objects as objects %}{{ objects|sorter_tests_pks }}", 95 | "3.2.1", {}, objects=LogEntry.objects.order_by('?')) 96 | 97 | def test_custom_name(self): 98 | query = {'sort_objects': '-id'} 99 | kwargs = dict(objects=LogEntry.objects.all()) 100 | self.assertViewRenders( 101 | """{% sort objects with "objects" as objects %}{{ objects|sorter_tests_pks }}""", 102 | "3.2.1", query=query, **kwargs) 103 | self.assertViewRenders( 104 | """{% sort objects with "sort_objects" as objects %}{{ objects|sorter_tests_pks }}""", 105 | "3.2.1", query=query, **kwargs) 106 | self.assertViewRenders( 107 | """{% sort objects with "sort_a_completely_different_objects" as objects %}{{ objects|sorter_tests_pks }}""", 108 | "3.2.1", query=query, **kwargs) 109 | 110 | def test_request_not_in_context(self): 111 | self.assertViewRaises(TemplateSyntaxError, 112 | """{% sort objects with "objects" as objects %}{{ objects|sorter_tests_pks }}""", 113 | {'sort': 'id'}, with_request=False, objects=LogEntry.objects.all()) 114 | 115 | def test_multiple_sorting(self): 116 | 117 | testuser = mommy.make(User) 118 | testuser.set_password("letmein") 119 | testuser.save() 120 | 121 | self.create_entries(3, user=testuser) 122 | self.assertEqual(LogEntry.objects.count(), 6) 123 | self.assertViewRenders(""" 124 | {% sort objects with "objects" as objects %} 125 | {% sort others with "others" as others %} 126 | {{ objects|sorter_tests_pks }}.{{ others|sorter_tests_pks }} 127 | """, "3.2.1.6.5.4", {"sort_objects": "-id", "sort_others": "-id"}, 128 | objects=LogEntry.objects.exclude(user=testuser), 129 | others=LogEntry.objects.filter(user=testuser)) 130 | 131 | def test_name_is_not_basestring(self): 132 | """ 133 | Validates that the given query name is a string and not 134 | accidently another object. 135 | """ 136 | self.assertViewRaises(TemplateSyntaxError, 137 | "{% sort objects with another_var as sorted %}{{ sorted|sorter_tests_pks }}", 138 | {'sort': 'id'}, objects=LogEntry.objects.all(), another_var=123) 139 | 140 | def test_ALLOWED_CRITERIA(self): 141 | old_setting = settings.SORTER_ALLOWED_CRITERIA 142 | try: 143 | settings.SORTER_ALLOWED_CRITERIA = { 144 | 'sort': ['non-existing'], 145 | 'sort_objects': ['action_time', 'user__*'], 146 | } 147 | # This will follow the default order of the LogEntry class, -action_time 148 | self.assertViewRenders( 149 | "{% sort objects as sorted %}{{ sorted|sorter_tests_pks }}", 150 | "3.2.1", {'sort': 'id'}, objects=LogEntry.objects.all()) 151 | self.assertViewRenders( 152 | "{% sort objects with 'objects' as sorted %}{{ sorted|sorter_tests_pks }}", 153 | "1.2.3", {'sort_objects': '-id,action_time'}, 154 | objects=LogEntry.objects.all()) 155 | finally: 156 | settings.SORTER_ALLOWED_CRITERIA = old_setting 157 | 158 | 159 | class SortURLTests(SorterTestCase): 160 | 161 | def test_cycle_pairs(self): 162 | self.assertEqual(list(cycle_pairs([1, 2, 3])), [(1, 2), (2, 3), (3, 1)]) 163 | 164 | def test_simple(self): 165 | self.assertViewRenders( 166 | """{% sorturl by "creation_date" %}""", 167 | """/?sort=creation_date""") 168 | 169 | 170 | class SortlinkTests(SorterTestCase): 171 | 172 | def test_simple(self): 173 | self.assertViewRenders( 174 | """{% sortlink by "creation_date" %}Creation date{% endsortlink %}""", 175 | """Creation date""") 176 | 177 | self.assertViewRenders( 178 | """{% sortlink with "objects" by "creation_date,-title" %}Creation and title{% endsortlink %}""", 179 | """Creation and title""") 180 | 181 | def test_attributes(self): 182 | self.assertViewRenders( 183 | """{% sortlink by "creation_date" rel "nofollow" class "sortlink" %}Creation date{% endsortlink %}""", 184 | """Creation date""") 185 | 186 | def test_cycling(self): 187 | self.assertViewRenders( 188 | """{% sortlink by "creation_date" "-creation_date" %}Creation date{% endsortlink %}""", 189 | """Creation date""", 190 | {'sort': 'creation_date'}) 191 | 192 | def test_errors(self): 193 | self.assertViewRaises(TemplateSyntaxError, 194 | """{% sortlink with "objects" by "creation_date,-title" %}""" 195 | """{% endsortlink %}""") 196 | 197 | 198 | class SortFormTests(SorterTestCase): 199 | 200 | def test_simple(self): 201 | self.assertViewRenders( 202 | """{% sortform by "creation_date" %}Creation date{% endsortform %}""", 203 | """\ 204 |
205 | 206 | 207 |
""") 208 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. highlight:: html+django 5 | 6 | .. _sort: 7 | 8 | Sorting 9 | ------- 10 | 11 | The center piece of the app is the ``{% sort %}`` template tag which 12 | takes one required and one optional argument. 13 | 14 | The bare bones example is:: 15 | 16 | {% load sorter_tags %} 17 | 18 | {% sort object_qs as sorted_objects %} 19 | 20 | {% for obj in sorted_objects %} 21 | {{ obj.title }} 22 | {% endfor %} 23 | 24 | If this template is rendered with a list of ``objects``, say a Django 25 | QuerySet, the template tag will order it by calling the ``order_by()``. 26 | The sorting criteria passed to this method is retrieved from the 27 | request's GET paramaters (the "querystring_") by looking for a field-value 28 | pair with the name **sort**. 29 | 30 | Simple 31 | ++++++ 32 | 33 | So imagine you have a list of blog posts that you want to sort by their 34 | creation dates, in an ascending order. With the example template above 35 | all you'd have to do is to call the view rendering those blog posts with 36 | the appropriate querystring:: 37 | 38 | http://example.com/blog/?sort=creation_date 39 | 40 | Since name of the field is automatically picked up and passed to 41 | ``order_by()`` you can also sort in descending order by prepending 42 | the name of the field with a negative sign (``-``):: 43 | 44 | http://example.com/blog/?sort=-creation_date 45 | 46 | You can also pass multiple fields to the querystring parameter to 47 | sort for multiple fields:: 48 | 49 | http://example.com/blog/?sort=-creation_date,title 50 | 51 | Complex 52 | +++++++ 53 | 54 | In some cases you may want to sort multiple lists of objects in the same 55 | template or use advanced techniques like chained sorting workflows. 56 | 57 | All you need to do is to pass a second parameter to the ``{% sort %}`` 58 | template tag which defines the name of the querystring parameter:: 59 | 60 | {% load sorter_tags %} 61 | 62 | {% sort object_qs with "posts" as sorted_objects %} 63 | 64 | {% for obj in sorted_objects %} 65 | {{ obj.title }} 66 | {% endfor %} 67 | 68 | Make sure to use unique names to prevent any clash with other sortings 69 | that may happen on the same page. As a matter of additional precaution 70 | the template tag will prepend it with ``'sort_'`` when analyzing the 71 | request's querystring. 72 | 73 | So if you'd use the example template above, the template tag would look 74 | for a querystring parameter named ``'sort_posts'`` because the second 75 | parameter to the template tag is ``"posts"``:: 76 | 77 | http://example.com/blog/?sort_posts=creation_date 78 | 79 | .. _sortlink: 80 | 81 | Links 82 | ----- 83 | 84 | When sorting objects it's usually required to link to other sorting 85 | criterias. ``django-sorter`` includes the ``{% sortlink %}`` template tag 86 | for that which takes a number of optional arguments and a required list of 87 | sorting criterias:: 88 | 89 | {% sortlink [with NAME] [rel REL] [class CLASS] [as VARIABLE] by ORDER_A1[,ORDER_A2,..] [ORDER_B1[,ORDER_B2,..]] .. %} 90 | LABEL 91 | {% endsortlink %} 92 | 93 | As with the :ref:`{% sort %}` template tag, ``sortlink`` takes the 94 | name of the querystring parameter it's supposed to be working with. 95 | 96 | For example, the following code would fit the example 97 | :ref:`shown above`:: 98 | 99 | {% sortlink with "posts" by "title" %}Title{% endsortlink %} 100 | 101 | If this snippet would be included in a template that renders your blog 102 | (with a request path of ``'/blog/'``), we'd get:: 103 | 104 | Title 105 | 106 | Multiple criterias 107 | ++++++++++++++++++ 108 | 109 | Multiple sorting criterias can be specified in comma separated form:: 110 | 111 | {% sortlink with "posts" by "creation_data,title" %} 112 | Creation date and title 113 | {% endsortlink %} 114 | 115 | would generate:: 116 | 117 | Creation date and title 118 | 119 | Link text 120 | +++++++++ 121 | 122 | The template tag is a block tag, which means translating the link text 123 | is as easy as using Django's ``trans`` template tag:: 124 | 125 | {% load i18n %} 126 | 127 | {% sortlink with "posts" by "creation_data,title" %} 128 | {% trans "Creation date and title" %} 129 | {% endsortlink %} 130 | 131 | Other paramters 132 | +++++++++++++++ 133 | 134 | - ``rel`` which sets the appropriate attribute of the link, 135 | e.g. useful when trying to set `rel="nofollow"`_. 136 | 137 | - ``class`` which is useful to style the link correctly. 138 | 139 | - ``as`` which allows assigning the result of the template tag to 140 | a template context variable. 141 | 142 | Further customization 143 | +++++++++++++++++++++ 144 | 145 | Of course any further customization is also possible by overriding the 146 | templates used by the template tag. By default ``django-sorter`` will use 147 | the ``sorter/sortlink.html`` template, to render each link. 148 | 149 | Furthermore -- if a name is given with the ``with`` argument -- it'll also 150 | look for the template ``sorter/sortlink_NAME.html``, where ``NAME`` is the 151 | value of the argument passed. E.g.:: 152 | 153 | {% sortlink with "posts" by "title" %}Title{% endsortlink %} 154 | 155 | would make the template tag look for a ``sorter/sortlink_posts.html`` 156 | *and* ``sorter/sortlink.html``. 157 | 158 | The template tag passes a bunch of variable to the template: 159 | 160 | - ``with`` - The name of querystring parameter to take into account. 161 | - ``rel`` - The value to be used for the HTML rel attribute. 162 | - ``class`` - The value to be used for the HTML class attribute. 163 | - ``by`` - The list of sorting criterias. 164 | - ``title`` - A string which lists all search criteria in prose. 165 | - ``label`` - The rendered content of the template block. 166 | - ``url`` - The URLObject_ instance with the querystring set appropriately. 167 | - ``query`` - The value of the querystring parameter. 168 | 169 | .. _`rel="nofollow"`: http://en.wikipedia.org/wiki/Nofollow 170 | .. _`URLObject`: https://github.com/zacharyvoase/urlobject 171 | 172 | Criteria cycling 173 | ++++++++++++++++ 174 | 175 | Sometimes you'll want to allow switching between criterias depending on 176 | the currently selected sorting criteria. For example, if you sort a 177 | list of blog posts in ascending order you might want to show a link 178 | to the same list but in *descending* order. 179 | 180 | With ``django-sorter`` this is as easy as passing a **series** of sorting 181 | criterias to the same template tag:: 182 | 183 | {% sortlink with "posts" by "title" "-title" %}Title{% endsortlink %} 184 | 185 | Now when the link is rendered it will check the current URL and select 186 | **the next** sorting criteria to render. 187 | 188 | For example, if you'd be on the page with the URL 189 | ``'/blog/?sort_posts=title'``, the result would be:: 190 | 191 | Title 192 | 193 | Of course, if the last sorting criteria is found the current request's 194 | querystring, it'll start with the first again. 195 | 196 | .. _sortform: 197 | 198 | Forms 199 | ----- 200 | 201 | Other than the :ref:`sortlink` template tag, ``django-sorter`` 202 | also ships with a second template tag to apply other sorting criterias -- 203 | the ``sortform`` tag. 204 | 205 | It works basically the same as ``sortlink`` and uses the same code behind 206 | the scenes, but looks for a different template: ``sorter/sortform.html``. 207 | Just like the :ref:`sortlink` tag it'll use the name of the 208 | querystring parameter if given to additionally look for a specific template, 209 | e.g. ``sorter/sortform_posts.html`` 210 | 211 | An example:: 212 | 213 | {% sortform with "posts" by "creation_date" %} 214 | {% trans "Creation and title" %} 215 | {% endsortform %} 216 | 217 | rendered:: 218 | 219 |
220 | 221 | 222 |
223 | 224 | .. _sorturl: 225 | 226 | URLs 227 | ---- 228 | 229 | As a quick helper in case you don't like ``django-sorter`` to generate 230 | the links or forms for your sorting efforts, you can also use the simple 231 | ``sorturl`` template tag:: 232 | 233 | {% sorturl with "posts" by "creation_date" %} 234 | 235 | would only return the URL to the sorting:: 236 | 237 | /blog/?sort_posts=creation_date 238 | 239 | Don't forget that it also takes an optional ``as`` parameter (like the rest 240 | of the parameters described for the :ref:`sort` template tag). That's 241 | great for storing the URL to further mangle it or use it for other template-y 242 | things, e.g.:: 243 | 244 | {% sorturl with "posts" by "creation_date" as sort_by_date_url %} 245 | 246 | {% blocktrans with sort_by_date_url as url %} 247 | Please visit the following URL to sort by date: 248 | 249 | http://example.com{{ sort_by_date_url }} 250 | 251 | Thanks! 252 | {% endblocktrans %} 253 | 254 | .. _querystring: http://en.wikipedia.org/wiki/Querystring 255 | --------------------------------------------------------------------------------