From d65aa3c3c146b12548a54c894060bce9a8715ad2 Mon Sep 17 00:00:00 2001 From: Jonas Genannt Date: Sun, 15 Nov 2015 22:26:06 +0100 Subject: Imported Upstream version 0.4 --- ._CHANGELOG.txt | Bin 187 -> 0 bytes ._MANIFEST.in | Bin 184 -> 0 bytes ._README.txt | Bin 184 -> 0 bytes CHANGELOG.txt | 37 +- INSTALL.txt | 14 - LICENSE.txt | 4 +- MANIFEST.in | 9 +- PKG-INFO | 188 +++- README.rst | 20 + README.txt | 10 - bootstrap.py | 189 ++++ buildout.cfg | 50 + django_tagging.egg-info/PKG-INFO | 186 ++++ django_tagging.egg-info/SOURCES.txt | 40 + django_tagging.egg-info/dependency_links.txt | 1 + django_tagging.egg-info/not-zip-safe | 1 + django_tagging.egg-info/top_level.txt | 1 + docs/._overview.txt | Bin 188 -> 0 bytes docs/Makefile | 192 ++++ docs/conf.py | 293 ++++++ docs/index.rst | 886 ++++++++++++++++ docs/overview.txt | 904 ----------------- setup.cfg | 8 + setup.py | 96 +- tagging/._models.py | Bin 187 -> 0 bytes tagging/._settings.py | Bin 184 -> 0 bytes tagging/__init__.py | 69 +- tagging/admin.py | 13 +- tagging/apps.py | 14 + tagging/fields.py | 13 +- tagging/forms.py | 13 +- tagging/generic.py | 13 +- tagging/managers.py | 13 +- tagging/migrations/0001_initial.py | 52 + tagging/migrations/__init__.py | 3 + tagging/models.py | 131 ++- tagging/registry.py | 51 + tagging/settings.py | 4 +- tagging/templatetags/__init__.py | 3 + tagging/templatetags/tagging_tags.py | 131 ++- tagging/tests/._settings.py | Bin 184 -> 0 bytes tagging/tests/._tags.txt | Bin 184 -> 0 bytes tagging/tests/__init__.py | 3 + tagging/tests/models.py | 19 +- tagging/tests/settings.py | 50 +- tagging/tests/tests.py | 1386 +++++++++++++++----------- tagging/tests/urls.py | 20 + tagging/tests/utils.py | 16 + tagging/utils.py | 92 +- tagging/views.py | 86 +- versions.cfg | 23 + 51 files changed, 3508 insertions(+), 1839 deletions(-) delete mode 100644 ._CHANGELOG.txt delete mode 100644 ._MANIFEST.in delete mode 100644 ._README.txt delete mode 100644 INSTALL.txt create mode 100644 README.rst delete mode 100644 README.txt create mode 100644 bootstrap.py create mode 100644 buildout.cfg create mode 100644 django_tagging.egg-info/PKG-INFO create mode 100644 django_tagging.egg-info/SOURCES.txt create mode 100644 django_tagging.egg-info/dependency_links.txt create mode 100644 django_tagging.egg-info/not-zip-safe create mode 100644 django_tagging.egg-info/top_level.txt delete mode 100644 docs/._overview.txt create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst delete mode 100644 docs/overview.txt create mode 100644 setup.cfg delete mode 100644 tagging/._models.py delete mode 100644 tagging/._settings.py create mode 100644 tagging/apps.py create mode 100644 tagging/migrations/0001_initial.py create mode 100644 tagging/migrations/__init__.py create mode 100644 tagging/registry.py delete mode 100644 tagging/tests/._settings.py delete mode 100644 tagging/tests/._tags.txt create mode 100644 tagging/tests/urls.py create mode 100644 tagging/tests/utils.py create mode 100644 versions.cfg diff --git a/._CHANGELOG.txt b/._CHANGELOG.txt deleted file mode 100644 index 9bf97be..0000000 Binary files a/._CHANGELOG.txt and /dev/null differ diff --git a/._MANIFEST.in b/._MANIFEST.in deleted file mode 100644 index 8af97db..0000000 Binary files a/._MANIFEST.in and /dev/null differ diff --git a/._README.txt b/._README.txt deleted file mode 100644 index 8645f1c..0000000 Binary files a/._README.txt and /dev/null differ diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4303c2a..5751870 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,10 +2,41 @@ Django Tagging Changelog ======================== -Version 0.3.1, Not released: ----------------------------- +Version 0.3.6, 13th May 2015: +----------------------------- + +* Corrected initial migration + +Version 0.3.5, 13th May 2015: +----------------------------- + +* Added support for Django 1.8 +* Using migrations to fix syncdb +* Rename get_query_set to get_queryset +* Import GenericForeignKey from the new location + +Version 0.3.4, 7th November 2014: +--------------------------------- + +* Fix unicode errors in admin + +Version 0.3.3, 15th October 2014: +--------------------------------- + +* Added support for Django 1.7 + +Version 0.3.2, 18th February 2014: +---------------------------------- + +* Added support for Django 1.4 and 1.5 +* Added support for Python 2.6 to 3.3 +* Added tox to test and coverage + +Version 0.3.1, 22nd January 2010: +--------------------------------- * Fixed Django 1.2 support (did not add anything new) +* Fixed #95 — tagging.register won't stomp on model attributes Version 0.3.0, 22nd August 2009: -------------------------------- @@ -24,7 +55,7 @@ Version 0.3.0, 22nd August 2009: * ``TaggedItemManager``'s methods now accept a ``QuerySet`` or a ``Model`` class. If a ``QuerySet`` is given, it will be used as the basis for the ``QuerySet``s the methods return, so can be used to - restrict results to a subset of a model's instances. The + restrict results to a subset of a model's instances. The `tagged_object_list`` generic view and ModelTaggedItemManager`` manager have been updated accordingly. diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index 9f13a3a..0000000 --- a/INSTALL.txt +++ /dev/null @@ -1,14 +0,0 @@ -Thanks for downloading django-tagging. - -To install it, run the following command inside this directory: - - python setup.py install - -Or if you'd prefer you can simply place the included ``tagging`` -directory somewhere on your Python path, or symlink to it from -somewhere on your Python path; this is useful if you're working from a -Subversion checkout. - -Note that this application requires Python 2.3 or later, and Django -1.0 or later. You can obtain Python from http://www.python.org/ and -Django from http://www.djangoproject.com/. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index 1654536..59f77e7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ Django Tagging -------------- -Copyright (c) 2007, Jonathan Buchanan +Copyright (c) 2007-2015, Jonathan Buchanan, Julien Fache 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 @@ -52,4 +52,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index 618270b..44e45f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,10 @@ include CHANGELOG.txt -include INSTALL.txt include LICENSE.txt include MANIFEST.in -include README.txt -recursive-include docs *.txt +include README.rst +include versions.cfg +include buildout.cfg +include bootstrap.py +recursive-include docs * recursive-include tagging/tests *.txt +prune docs/_build \ No newline at end of file diff --git a/PKG-INFO b/PKG-INFO index 2d1e79c..577b287 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,18 +1,186 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: django-tagging -Version: 0.3.1 +Version: 0.4 Summary: Generic tagging application for Django -Home-page: http://code.google.com/p/django-tagging/ -Author: Jonathan Buchanan -Author-email: jonathan.buchanan@gmail.com -License: UNKNOWN -Description: UNKNOWN +Home-page: https://github.com/Fantomas42/django-tagging +Author: Fantomas42 +Author-email: fantomas42@gmail.com +License: BSD License +Description: ============== + Django Tagging + ============== + + |travis-develop| |coverage-develop| + + This is a generic tagging application for Django projects + + http://django-tagging.readthedocs.org/ + + Note that this application requires Python 2.7 or later, and Django + 1.7 or later. You can obtain Python from http://www.python.org/ and + Django from http://www.djangoproject.com/. + + .. |travis-develop| image:: https://travis-ci.org/Fantomas42/django-tagging.png?branch=develop + :alt: Build Status - develop branch + :target: http://travis-ci.org/Fantomas42/django-tagging + .. |coverage-develop| image:: https://coveralls.io/repos/Fantomas42/django-tagging/badge.png?branch=develop + :alt: Coverage of the code + :target: https://coveralls.io/r/Fantomas42/django-tagging + + ======================== + Django Tagging Changelog + ======================== + + Version 0.3.6, 13th May 2015: + ----------------------------- + + * Corrected initial migration + + Version 0.3.5, 13th May 2015: + ----------------------------- + + * Added support for Django 1.8 + * Using migrations to fix syncdb + * Rename get_query_set to get_queryset + * Import GenericForeignKey from the new location + + Version 0.3.4, 7th November 2014: + --------------------------------- + + * Fix unicode errors in admin + + Version 0.3.3, 15th October 2014: + --------------------------------- + + * Added support for Django 1.7 + + Version 0.3.2, 18th February 2014: + ---------------------------------- + + * Added support for Django 1.4 and 1.5 + * Added support for Python 2.6 to 3.3 + * Added tox to test and coverage + + Version 0.3.1, 22nd January 2010: + --------------------------------- + + * Fixed Django 1.2 support (did not add anything new) + * Fixed #95 — tagging.register won't stomp on model attributes + + Version 0.3.0, 22nd August 2009: + -------------------------------- + + * Fixes for Django 1.0 compatibility. + + * Added a ``tagging.generic`` module for working with list of objects + which have generic relations, containing a ``fetch_content_objects`` + function for retrieving content objects for a list of ``TaggedItem``s + using ``number_of_content_types + 1`` queries rather than the + ``number_of_tagged_items * 2`` queries you'd get by iterating over the + list and accessing each item's ``object`` attribute. + + * Added a ``usage`` method to ``ModelTagManager``. + + * ``TaggedItemManager``'s methods now accept a ``QuerySet`` or a + ``Model`` class. If a ``QuerySet`` is given, it will be used as the + basis for the ``QuerySet``s the methods return, so can be used to + restrict results to a subset of a model's instances. The + `tagged_object_list`` generic view and ModelTaggedItemManager`` + manager have been updated accordingly. + + * Removed ``tagging\tests\runtests.py``, as tests can be run with + ``django-admin.py test --settings=tagging.tests.settings``. + + * A ``tagging.TagDescriptor`` is now added to models when registered. + This returns a ``tagging.managers.ModelTagManager`` when accessed on a + model class, and provide access to and control over tags when used on + an instance. + + * Added ``tagging.register`` to register models with the tagging app. + Initially, a ``tagging.managers.ModelTaggedItemManager`` is added for + convenient access to tagged items. + + * Moved ``TagManager`` and ``TaggedItemManager`` to ``models.py`` - gets + rid of some import related silliness, as ``TagManager`` needs access + to ``TaggedItem``. + + Version 0.2.1, 16th Jan 2008: + ----------------------------- + + * Fixed a bug with space-delimited tag input handling - duplicates + weren't being removed and the list of tag names wasn't sorted. + + Version 0.2, 12th Jan 2008: + --------------------------- + + Packaged from revision 122 in Subversion; download at + http://django-tagging.googlecode.com/files/tagging-0.2.zip + + * Added a ``tag_cloud_for_model`` template tag. + + * Added a ``MAX_TAG_LENGTH`` setting. + + * Multi-word tags are here - simple space-delimited input still works. + Double quotes and/or commas are used to delineate multi- word tags. + As far as valid tag contents - anything goes, at least initially. + + * BACKWARDS-INCOMPATIBLE CHANGE - ``django.utils.get_tag_name_list`` and + related regular expressions have been removed in favour of a new tag + input parsing function, ``django.utils.parse_tag_input``. + + * BACKWARDS-INCOMPATIBLE CHANGE - ``Tag`` and ``TaggedItem`` no longer + declare an explicit ``db_table``. If you can't rename your tables, + you'll have to put these back in manually. + + * Fixed a bug in calculation of logarithmic tag clouds - ``font_size`` + attributes were not being set in some cases when the least used tag in + the cloud had been used more than once. + + * For consistency of return type, ``TaggedItemManager.get_by_model`` now + returns an empty ``QuerySet`` instead of an empty ``list`` if + non-existent tags were given. + + * Fixed a bug caused by ``cloud_for_model`` not passing its + ``distribution`` argument to ``calculate_cloud``. + + * Added ``TaggedItemManager.get_union_by_model`` for looking up items + tagged with any one of a list of tags. + + * Added ``TagManager.add_tag`` for adding a single extra tag to an + object. + + * Tag names can now be forced to lowercase before they are saved to the + database by adding the appropriate ``FORCE_LOWERCASE_TAGS`` setting to + your project's settings module. This feature defaults to being off. + + * Fixed a bug where passing non-existent tag names to + ``TaggedItemManager.get_by_model`` caused database errors with some + backends. + + * Added ``tagged_object_list`` generic view for displaying paginated + lists of objects for a given model which have a given tag, and + optionally related tags for that model. + + + Version 0.1, 30th May 2007: + --------------------------- + + Packaged from revision 79 in Subversion; download at + http://django-tagging.googlecode.com/files/tagging-0.1.zip + + * First packaged version using distutils. + +Keywords: django,tag,tagging Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Environment :: Web Environment Classifier: Framework :: Django +Classifier: Environment :: Web Environment +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Utilities +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8a89c2f --- /dev/null +++ b/README.rst @@ -0,0 +1,20 @@ +============== +Django Tagging +============== + +|travis-develop| |coverage-develop| + +This is a generic tagging application for Django projects + +http://django-tagging.readthedocs.org/ + +Note that this application requires Python 2.7 or later, and Django +1.7 or later. You can obtain Python from http://www.python.org/ and +Django from http://www.djangoproject.com/. + +.. |travis-develop| image:: https://travis-ci.org/Fantomas42/django-tagging.png?branch=develop + :alt: Build Status - develop branch + :target: http://travis-ci.org/Fantomas42/django-tagging +.. |coverage-develop| image:: https://coveralls.io/repos/Fantomas42/django-tagging/badge.png?branch=develop + :alt: Coverage of the code + :target: https://coveralls.io/r/Fantomas42/django-tagging diff --git a/README.txt b/README.txt deleted file mode 100644 index acb2d47..0000000 --- a/README.txt +++ /dev/null @@ -1,10 +0,0 @@ -============== -Django Tagging -============== - -This is a generic tagging application for Django projects - -For installation instructions, see the file "INSTALL.txt" in this -directory; for instructions on how to use this application, and on -what it provides, see the file "overview.txt" in the "docs/" -directory. \ No newline at end of file diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 0000000..a629566 --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,189 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os +import shutil +import sys +import tempfile + +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --find-links to point to local resources, you can keep +this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", help="use a specific zc.buildout version") + +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", "--config-file", + help=("Specify the path to the buildout configuration " + "file to be used.")) +parser.add_option("-f", "--find-links", + help=("Specify a URL to search for buildout releases")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) +parser.add_option("--setuptools-version", + help="use a specific setuptools version") + + +options, args = parser.parse_args() + +###################################################################### +# load/install setuptools + +try: + if options.allow_site_packages: + import setuptools + import pkg_resources + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +ez = {} +exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + +if not options.allow_site_packages: + # ez_setup imports site, which adds site packages + # this will remove them from the path to ensure that incompatible versions + # of setuptools are not in the path + import site + # inside a virtualenv, there is no 'getsitepackages'. + # We can't remove these reliably + if hasattr(site, 'getsitepackages'): + for sitepackage_path in site.getsitepackages(): + sys.path[:] = [x for x in sys.path if sitepackage_path not in x] + +setup_args = dict(to_dir=tmpeggs, download_delay=0) + +if options.setuptools_version is not None: + setup_args['version'] = options.setuptools_version + +ez['use_setuptools'](**setup_args) +import setuptools +import pkg_resources + +# This does not (always?) update the default working set. We will +# do it. +for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +###################################################################### +# Install buildout + +ws = pkg_resources.working_set + +cmd = [sys.executable, '-c', + 'from setuptools.command.easy_install import main; main()', + '-mZqNxd', tmpeggs] + +find_links = os.environ.get( + 'bootstrap-testing-find-links', + options.find_links or + ('http://downloads.buildout.org/' + if options.accept_buildout_test_releases else None) + ) +if find_links: + cmd.extend(['-f', find_links]) + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + try: + return not parsed_version.is_prerelease + except AttributeError: + # Older setuptools + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + + index = setuptools.package_index.PackageIndex( + search_path=[setuptools_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +import subprocess +if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: + raise Exception( + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) + +###################################################################### +# Import and run buildout + +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout + +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff --git a/buildout.cfg b/buildout.cfg new file mode 100644 index 0000000..728c5b7 --- /dev/null +++ b/buildout.cfg @@ -0,0 +1,50 @@ +[buildout] +extends = versions.cfg +parts = test + test-and-cover + flake8 + evolution + coveralls +develop = . +eggs = django + django-tagging +show-picked-versions = true + +[test] +recipe = pbp.recipe.noserunner +eggs = nose + nose-sfd + nose-progressive + ${buildout:eggs} +defaults = --with-progressive + --with-sfd +environment = testenv + +[test-and-cover] +recipe = pbp.recipe.noserunner +eggs = nose + nose-sfd + coverage + ${buildout:eggs} +defaults = --with-coverage + --cover-package=tagging + --cover-erase + --with-sfd +environment = testenv + +[flake8] +recipe = zc.recipe.egg +eggs = flake8 + +[evolution] +recipe = zc.recipe.egg +eggs = buildout-versions-checker +arguments = '-w --sorting alpha -e pep8' +scripts = check-buildout-updates=${:_buildout_section_name_} + +[coveralls] +recipe = zc.recipe.egg +eggs = python-coveralls + +[testenv] +DJANGO_SETTINGS_MODULE = tagging.tests.settings diff --git a/django_tagging.egg-info/PKG-INFO b/django_tagging.egg-info/PKG-INFO new file mode 100644 index 0000000..577b287 --- /dev/null +++ b/django_tagging.egg-info/PKG-INFO @@ -0,0 +1,186 @@ +Metadata-Version: 1.1 +Name: django-tagging +Version: 0.4 +Summary: Generic tagging application for Django +Home-page: https://github.com/Fantomas42/django-tagging +Author: Fantomas42 +Author-email: fantomas42@gmail.com +License: BSD License +Description: ============== + Django Tagging + ============== + + |travis-develop| |coverage-develop| + + This is a generic tagging application for Django projects + + http://django-tagging.readthedocs.org/ + + Note that this application requires Python 2.7 or later, and Django + 1.7 or later. You can obtain Python from http://www.python.org/ and + Django from http://www.djangoproject.com/. + + .. |travis-develop| image:: https://travis-ci.org/Fantomas42/django-tagging.png?branch=develop + :alt: Build Status - develop branch + :target: http://travis-ci.org/Fantomas42/django-tagging + .. |coverage-develop| image:: https://coveralls.io/repos/Fantomas42/django-tagging/badge.png?branch=develop + :alt: Coverage of the code + :target: https://coveralls.io/r/Fantomas42/django-tagging + + ======================== + Django Tagging Changelog + ======================== + + Version 0.3.6, 13th May 2015: + ----------------------------- + + * Corrected initial migration + + Version 0.3.5, 13th May 2015: + ----------------------------- + + * Added support for Django 1.8 + * Using migrations to fix syncdb + * Rename get_query_set to get_queryset + * Import GenericForeignKey from the new location + + Version 0.3.4, 7th November 2014: + --------------------------------- + + * Fix unicode errors in admin + + Version 0.3.3, 15th October 2014: + --------------------------------- + + * Added support for Django 1.7 + + Version 0.3.2, 18th February 2014: + ---------------------------------- + + * Added support for Django 1.4 and 1.5 + * Added support for Python 2.6 to 3.3 + * Added tox to test and coverage + + Version 0.3.1, 22nd January 2010: + --------------------------------- + + * Fixed Django 1.2 support (did not add anything new) + * Fixed #95 — tagging.register won't stomp on model attributes + + Version 0.3.0, 22nd August 2009: + -------------------------------- + + * Fixes for Django 1.0 compatibility. + + * Added a ``tagging.generic`` module for working with list of objects + which have generic relations, containing a ``fetch_content_objects`` + function for retrieving content objects for a list of ``TaggedItem``s + using ``number_of_content_types + 1`` queries rather than the + ``number_of_tagged_items * 2`` queries you'd get by iterating over the + list and accessing each item's ``object`` attribute. + + * Added a ``usage`` method to ``ModelTagManager``. + + * ``TaggedItemManager``'s methods now accept a ``QuerySet`` or a + ``Model`` class. If a ``QuerySet`` is given, it will be used as the + basis for the ``QuerySet``s the methods return, so can be used to + restrict results to a subset of a model's instances. The + `tagged_object_list`` generic view and ModelTaggedItemManager`` + manager have been updated accordingly. + + * Removed ``tagging\tests\runtests.py``, as tests can be run with + ``django-admin.py test --settings=tagging.tests.settings``. + + * A ``tagging.TagDescriptor`` is now added to models when registered. + This returns a ``tagging.managers.ModelTagManager`` when accessed on a + model class, and provide access to and control over tags when used on + an instance. + + * Added ``tagging.register`` to register models with the tagging app. + Initially, a ``tagging.managers.ModelTaggedItemManager`` is added for + convenient access to tagged items. + + * Moved ``TagManager`` and ``TaggedItemManager`` to ``models.py`` - gets + rid of some import related silliness, as ``TagManager`` needs access + to ``TaggedItem``. + + Version 0.2.1, 16th Jan 2008: + ----------------------------- + + * Fixed a bug with space-delimited tag input handling - duplicates + weren't being removed and the list of tag names wasn't sorted. + + Version 0.2, 12th Jan 2008: + --------------------------- + + Packaged from revision 122 in Subversion; download at + http://django-tagging.googlecode.com/files/tagging-0.2.zip + + * Added a ``tag_cloud_for_model`` template tag. + + * Added a ``MAX_TAG_LENGTH`` setting. + + * Multi-word tags are here - simple space-delimited input still works. + Double quotes and/or commas are used to delineate multi- word tags. + As far as valid tag contents - anything goes, at least initially. + + * BACKWARDS-INCOMPATIBLE CHANGE - ``django.utils.get_tag_name_list`` and + related regular expressions have been removed in favour of a new tag + input parsing function, ``django.utils.parse_tag_input``. + + * BACKWARDS-INCOMPATIBLE CHANGE - ``Tag`` and ``TaggedItem`` no longer + declare an explicit ``db_table``. If you can't rename your tables, + you'll have to put these back in manually. + + * Fixed a bug in calculation of logarithmic tag clouds - ``font_size`` + attributes were not being set in some cases when the least used tag in + the cloud had been used more than once. + + * For consistency of return type, ``TaggedItemManager.get_by_model`` now + returns an empty ``QuerySet`` instead of an empty ``list`` if + non-existent tags were given. + + * Fixed a bug caused by ``cloud_for_model`` not passing its + ``distribution`` argument to ``calculate_cloud``. + + * Added ``TaggedItemManager.get_union_by_model`` for looking up items + tagged with any one of a list of tags. + + * Added ``TagManager.add_tag`` for adding a single extra tag to an + object. + + * Tag names can now be forced to lowercase before they are saved to the + database by adding the appropriate ``FORCE_LOWERCASE_TAGS`` setting to + your project's settings module. This feature defaults to being off. + + * Fixed a bug where passing non-existent tag names to + ``TaggedItemManager.get_by_model`` caused database errors with some + backends. + + * Added ``tagged_object_list`` generic view for displaying paginated + lists of objects for a given model which have a given tag, and + optionally related tags for that model. + + + Version 0.1, 30th May 2007: + --------------------------- + + Packaged from revision 79 in Subversion; download at + http://django-tagging.googlecode.com/files/tagging-0.1.zip + + * First packaged version using distutils. + +Keywords: django,tag,tagging +Platform: UNKNOWN +Classifier: Framework :: Django +Classifier: Environment :: Web Environment +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Utilities +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/django_tagging.egg-info/SOURCES.txt b/django_tagging.egg-info/SOURCES.txt new file mode 100644 index 0000000..15f5763 --- /dev/null +++ b/django_tagging.egg-info/SOURCES.txt @@ -0,0 +1,40 @@ +CHANGELOG.txt +LICENSE.txt +MANIFEST.in +README.rst +bootstrap.py +buildout.cfg +setup.cfg +setup.py +versions.cfg +django_tagging.egg-info/PKG-INFO +django_tagging.egg-info/SOURCES.txt +django_tagging.egg-info/dependency_links.txt +django_tagging.egg-info/not-zip-safe +django_tagging.egg-info/top_level.txt +docs/Makefile +docs/conf.py +docs/index.rst +tagging/__init__.py +tagging/admin.py +tagging/apps.py +tagging/fields.py +tagging/forms.py +tagging/generic.py +tagging/managers.py +tagging/models.py +tagging/registry.py +tagging/settings.py +tagging/utils.py +tagging/views.py +tagging/migrations/0001_initial.py +tagging/migrations/__init__.py +tagging/templatetags/__init__.py +tagging/templatetags/tagging_tags.py +tagging/tests/__init__.py +tagging/tests/models.py +tagging/tests/settings.py +tagging/tests/tags.txt +tagging/tests/tests.py +tagging/tests/urls.py +tagging/tests/utils.py \ No newline at end of file diff --git a/django_tagging.egg-info/dependency_links.txt b/django_tagging.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/django_tagging.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/django_tagging.egg-info/not-zip-safe b/django_tagging.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/django_tagging.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/django_tagging.egg-info/top_level.txt b/django_tagging.egg-info/top_level.txt new file mode 100644 index 0000000..b8acc5f --- /dev/null +++ b/django_tagging.egg-info/top_level.txt @@ -0,0 +1 @@ +tagging diff --git a/docs/._overview.txt b/docs/._overview.txt deleted file mode 100644 index c4db396..0000000 Binary files a/docs/._overview.txt and /dev/null differ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..0cddc85 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +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 " applehelp to make an Apple Help Book" + @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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of 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/django-tagging.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-tagging.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-tagging" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-tagging" + @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." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @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." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +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." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d2a7406 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# +# django-tagging documentation build configuration file, created by +# sphinx-quickstart on Thu May 14 19:31:27 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. +import os +import re +import sys + +from datetime import date +HERE = os.path.abspath(os.path.dirname(__file__)) + +sys.path.append(HERE) +sys.path.append(os.path.join(HERE, '..')) + +import tagging + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# 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', +] + +intersphinx_mapping = { + 'django': ('http://readthedocs.org/docs/django/en/latest/', None), +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Django Tagging' +copyright = '%s, %s' % (date.today().year, tagging.__maintainer__) +author = tagging.__author__ + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = tagging.__version__ +# The short X.Y version. +version = re.match(r'\d+\.\d+(?:\.\d+)?', release).group() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-taggingdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'django-tagging.tex', u'django-tagging Documentation', + u'Fantomas42', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'django-tagging', u'django-tagging Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'django-tagging', u'django-tagging Documentation', + author, 'django-tagging', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c73cbe4 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,886 @@ +============== +Django Tagging +============== + +A generic tagging application for `Django`_ projects, which allows +association of a number of tags with any Django model instance and makes +retrieval of tags simple. + +.. _`Django`: http://www.djangoproject.com + +.. contents:: + :local: + :depth: 3 + +Installation +============ + +Installing an official release +------------------------------ + +Official releases are made available from +https://pypi.python.org/pypi/django-tagging/ + +Source distribution +~~~~~~~~~~~~~~~~~~~ + +Download the a distribution file and unpack it. Inside is a script +named ``setup.py``. Enter this command:: + + $ python setup.py install + +...and the package will install automatically. + +More easily with :program:`pip`:: + + $ pip install django-tagging + +Installing the development version +---------------------------------- + +Alternatively, if you'd like to update Django Tagging occasionally to pick +up the latest bug fixes and enhancements before they make it into an +official release, clone the git repository instead. The following +command will clone the development branch to ``django-tagging`` directory:: + + git clone git@github.com:Fantomas42/django-tagging.git + +Add the resulting folder to your `PYTHONPATH`_ or symlink (`junction`_, +if you're on Windows) the ``tagging`` directory inside it into a +directory which is on your PYTHONPATH, such as your Python +installation's ``site-packages`` directory. + +You can verify that the application is available on your PYTHONPATH by +opening a Python interpreter and entering the following commands:: + + >>> import tagging + >>> tagging.__version__ + 0.4.dev0 + +When you want to update your copy of the Django Tagging source code, run +the command ``git pull`` from within the ``django-tagging`` directory. + +.. caution:: + + The development version may contain bugs which are not present in the + release version and introduce backwards-incompatible changes. + + If you're tracking git, keep an eye on the `CHANGELOG`_ + before you update your copy of the source code. + +.. _`PYTHONPATH`: http://www.python.org/doc/2.5.2/tut/node8.html#SECTION008120000000000000000 +.. _`junction`: http://www.microsoft.com/technet/sysinternals/FileAndDisk/Junction.mspx +.. _`CHANGELOG`: https://github.com/Fantomas42/django-tagging/blob/develop/CHANGELOG.txt + +Using Django Tagging in your applications +----------------------------------------- + +Once you've installed Django Tagging and want to use it in your Django +applications, do the following: + + 1. Put ``'tagging'`` in your ``INSTALLED_APPS`` setting. + 2. Run the command ``manage.py migrate``. + +The ``migrate`` command creates the necessary database tables and +creates permission objects for all installed apps that need them. + +That's it! + +Settings +======== + +Some of the application's behaviour can be configured by adding the +appropriate settings to your project's settings file. + +The following settings are available: + +FORCE_LOWERCASE_TAGS +-------------------- + +Default: ``False`` + +A boolean that turns on/off forcing of all tag names to lowercase before +they are saved to the database. + +MAX_TAG_LENGTH +-------------- + +Default: ``50`` + +An integer which specifies the maximum length which any tag is allowed +to have. This is used for validation in the ``django.contrib.admin`` +application and in any forms automatically generated using ``ModelForm``. + + +Registering your models +======================= + +Your Django models can be registered with the tagging application to +access some additional tagging-related features. + +.. note:: + + You don't *have* to register your models in order to use them with + the tagging application - many of the features added by registration + are just convenience wrappers around the tagging API provided by the + ``Tag`` and ``TaggedItem`` models and their managers, as documented + further below. + +The ``register`` function +------------------------- + +To register a model, import the ``tagging.registry`` module and call its +``register`` function, like so:: + + from django.db import models + + from tagging.registry import register + + class Widget(models.Model): + name = models.CharField(max_length=50) + + tagging.register(Widget) + +The following argument is required: + +``model`` + The model class to be registered. + + An exception will be raised if you attempt to register the same class + more than once. + +The following arguments are optional, with some recommended defaults - +take care to specify different attribute names if the defaults clash +with your model class' definition: + +``tag_descriptor_attr`` + The name of an attribute in the model class which will hold a tag + descriptor for the model. Default: ``'tags'`` + + See `TagDescriptor`_ below for details about the use of this + descriptor. + +``tagged_item_manger_attr`` + The name of an attribute in the model class which will hold a custom + manager for accessing tagged items for the model. Default: + ``'tagged'``. + + See `ModelTaggedItemManager`_ below for details about the use of this + manager. + +``TagDescriptor`` +----------------- + +When accessed through the model class itself, this descriptor will return +a ``ModelTagManager`` for the model. See `ModelTagManager`_ below for +more details about its use. + +When accessed through a model instance, this descriptor provides a handy +means of retrieving, updating and deleting the instance's tags. For +example:: + + >>> widget = Widget.objects.create(name='Testing descriptor') + >>> widget.tags + [] + >>> widget.tags = 'toast, melted cheese, butter' + >>> widget.tags + [, , ] + >>> del widget.tags + >>> widget.tags + [] + +``ModelTagManager`` +------------------- + +A manager for retrieving tags used by a particular model. + +Defines the following methods: + +* ``get_queryset()`` -- as this method is redefined, any ``QuerySets`` + created by this model will be initially restricted to contain the + distinct tags used by all the model's instances. + +* ``cloud(*args, **kwargs)`` -- creates a list of tags used by the + model's instances, with ``count`` and ``font_size`` attributes set for + use in displaying a tag cloud. + + See the documentation on ``Tag``'s manager's `cloud_for_model method`_ + for information on additional arguments which can be given. + +* ``related(self, tags, *args, **kwargs)`` -- creates a list of tags + used by the model's instances, which are also used by all instance + which have the given ``tags``. + + See the documentation on ``Tag``'s manager's + `related_for_model method`_ for information on additional arguments + which can be given. + +* ``usage(self, *args, **kwargs))`` -- creates a list of tags used by + the model's instances, with optional usages counts, restriction based + on usage counts and restriction of the model instances from which + usage and counts are determined. + + See the documentation on ``Tag``'s manager's `usage_for_model method`_ + for information on additional arguments which can be given. + +Example usage:: + + # Create a ``QuerySet`` of tags used by Widget instances + Widget.tags.all() + + # Retrieve a list of tags used by Widget instances with usage counts + Widget.tags.usage(counts=True) + + # Retrieve tags used by instances of WIdget which are also tagged with + # 'cheese' and 'toast' + Widget.tags.related(['cheese', 'toast'], counts=True, min_count=3) + +``ModelTaggedItemManager`` +-------------------------- + +A manager for retrieving model instance for a particular model, based on +their tags. + +* ``related_to(obj, queryset=None, num=None)`` -- creates a list + of model instances which are related to ``obj``, based on its tags. If + a ``queryset`` argument is provided, it will be used to restrict the + resulting list of model instances. + + If ``num`` is given, a maximum of ``num`` instances will be returned. + +* ``with_all(tags, queryset=None)`` -- creates a ``QuerySet`` containing + model instances which are tagged with *all* the given tags. If a + ``queryset`` argument is provided, it will be used as the basis for + the resulting ``QuerySet``. + +* ``with_any(tags, queryset=None)`` -- creates a ``QuerySet`` containing model + instances which are tagged with *any* the given tags. If a ``queryset`` + argument is provided, it will be used as the basis for the resulting + ``QuerySet``. + + +Tags +==== + +Tags are represented by the ``Tag`` model, which lives in the +``tagging.models`` module. + +API reference +------------- + +Fields +~~~~~~ + +``Tag`` objects have the following fields: + +* ``name`` -- The name of the tag. This is a unique value. + +Manager functions +~~~~~~~~~~~~~~~~~ + +The ``Tag`` model has a custom manager which has the following helper +methods: + +* ``update_tags(obj, tag_names)`` -- updates tags associated with an + object. + + ``tag_names`` is a string containing tag names with which ``obj`` + should be tagged. + + If ``tag_names`` is ``None`` or ``''``, the object's tags will be + cleared. + +* ``add_tag(obj, tag_name)`` -- associates a tag with an an object. + + ``tag_name`` is a string containing a tag name with which ``obj`` + should be tagged. + +* ``get_for_object(obj)`` -- returns a ``QuerySet`` containing all + ``Tag`` objects associated with ``obj``. + +.. _`usage_for_model method`: + +* ``usage_for_model(model, counts=False, min_count=None, filters=None)`` + -- returns a list of ``Tag`` objects associated with instances of + ``model``. + + If ``counts`` is ``True``, a ``count`` attribute will be added to each + tag, indicating how many times it has been associated with instances + of ``model``. + + If ``min_count`` is given, only tags which have a ``count`` greater + than or equal to ``min_count`` will be returned. Passing a value for + ``min_count`` implies ``counts=True``. + + To limit the tags (and counts, if specified) returned to those used by + a subset of the model's instances, pass a dictionary of field lookups + to be applied to ``model`` as the ``filters`` argument. + +.. _`related_for_model method`: + +* ``related_for_model(tags, Model, counts=False, min_count=None)`` + -- returns a list of tags related to a given list of tags - that is, + other tags used by items which have all the given tags. + + If ``counts`` is ``True``, a ``count`` attribute will be added to each + tag, indicating the number of items which have it in addition to the + given list of tags. + + If ``min_count`` is given, only tags which have a ``count`` greater + than or equal to ``min_count`` will be returned. Passing a value for + ``min_count`` implies ``counts=True``. + +.. _`cloud_for_model method`: + +* ``cloud_for_model(Model, steps=4, distribution=LOGARITHMIC, + filters=None, min_count=None)`` -- returns a list of the distinct + ``Tag`` objects associated with instances of ``Model``, each having a + ``count`` attribute as above and an additional ``font_size`` + attribute, for use in creation of a tag cloud (a type of weighted + list). + + ``steps`` defines the number of font sizes available - ``font_size`` + may be an integer between ``1`` and ``steps``, inclusive. + + ``distribution`` defines the type of font size distribution algorithm + which will be used - logarithmic or linear. It must be either + ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. + + To limit the tags displayed in the cloud to those associated with a + subset of the Model's instances, pass a dictionary of field lookups to + be applied to the given Model as the ``filters`` argument. + + To limit the tags displayed in the cloud to those with a ``count`` + greater than or equal to ``min_count``, pass a value for the + ``min_count`` argument. + +* ``usage_for_queryset(queryset, counts=False, min_count=None)`` -- + Obtains a list of tags associated with instances of a model contained + in the given queryset. + + If ``counts`` is True, a ``count`` attribute will be added to each tag, + indicating how many times it has been used against the Model class in + question. + + If ``min_count`` is given, only tags which have a ``count`` greater + than or equal to ``min_count`` will be returned. + + Passing a value for ``min_count`` implies ``counts=True``. + +Basic usage +----------- + +Tagging objects and retrieving an object's tags +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Objects may be tagged using the ``update_tags`` helper function:: + + >>> from shop.apps.products.models import Widget + >>> from tagging.models import Tag + >>> widget = Widget.objects.get(pk=1) + >>> Tag.objects.update_tags(widget, 'house thing') + +Retrieve tags for an object using the ``get_for_object`` helper +function:: + + >>> Tag.objects.get_for_object(widget) + [, ] + +Tags are created, associated and unassociated accordingly when you use +``update_tags`` and ``add_tag``:: + + >>> Tag.objects.update_tags(widget, 'house monkey') + >>> Tag.objects.get_for_object(widget) + [, ] + >>> Tag.objects.add_tag(widget, 'tiles') + >>> Tag.objects.get_for_object(widget) + [, , ] + +Clear an object's tags by passing ``None`` or ``''`` to +``update_tags``:: + + >>> Tag.objects.update_tags(widget, None) + >>> Tag.objects.get_for_object(widget) + [] + +Retrieving tags used by a particular model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To retrieve all tags used for a particular model, use the +``get_for_model`` helper function:: + + >>> widget1 = Widget.objects.get(pk=1) + >>> Tag.objects.update_tags(widget1, 'house thing') + >>> widget2 = Widget.objects.get(pk=2) + >>> Tag.objects.update_tags(widget2, 'cheese toast house') + >>> Tag.objects.usage_for_model(Widget) + [, , , ] + +To get a count of how many times each tag was used for a particular +model, pass in ``True`` for the ``counts`` argument:: + + >>> tags = Tag.objects.usage_for_model(Widget, counts=True) + >>> [(tag.name, tag.count) for tag in tags] + [('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)] + +To get counts and limit the tags returned to those with counts above a +certain size, pass in a ``min_count`` argument:: + + >>> tags = Tag.objects.usage_for_model(Widget, min_count=2) + >>> [(tag.name, tag.count) for tag in tags] + [('house', 2)] + +You can also specify a dictionary of `field lookups`_ to be used to +restrict the tags and counts returned based on a subset of the +model's instances. For example, the following would retrieve all tags +used on Widgets created by a user named Alan which have a size +greater than 99:: + + >>> Tag.objects.usage_for_model(Widget, filters=dict(size__gt=99, user__username='Alan')) + +.. _`field lookups`: http://docs.djangoproject.com/en/dev/topics/db/queries/#field-lookups + +The ``usage_for_queryset`` method allows you to pass a pre-filtered +queryset to be used when determining tag usage:: + + >>> Tag.objects.usage_for_queryset(Widget.objects.filter(size__gt=99, user__username='Alan')) + +Tag input +--------- + +Tag input from users is treated as follows: + +* If the input doesn't contain any commas or double quotes, it is simply + treated as a space-delimited list of tag names. + +* If the input does contain either of these characters, we parse the + input like so: + + * Groups of characters which appear between double quotes take + precedence as multi-word tags (so double quoted tag names may + contain commas). An unclosed double quote will be ignored. + + * For the remaining input, if there are any unquoted commas in the + input, the remainder will be treated as comma-delimited. Otherwise, + it will be treated as space-delimited. + +Examples: + +====================== ======================================= ================================================ +Tag input string Resulting tags Notes +====================== ======================================= ================================================ +apple ball cat [``apple``], [``ball``], [``cat``] No commas, so space delimited +apple, ball cat [``apple``], [``ball cat``] Comma present, so comma delimited +"apple, ball" cat dog [``apple, ball``], [``cat``], [``dog``] All commas are quoted, so space delimited +"apple, ball", cat dog [``apple, ball``], [``cat dog``] Contains an unquoted comma, so comma delimited +apple "ball cat" dog [``apple``], [``ball cat``], [``dog``] No commas, so space delimited +"apple" "ball dog [``apple``], [``ball``], [``dog``] Unclosed double quote is ignored +====================== ======================================= ================================================ + + +Tagged items +============ + +The relationship between a ``Tag`` and an object is represented by +the ``TaggedItem`` model, which lives in the ``tagging.models`` +module. + +API reference +------------- + +Fields +~~~~~~ + +``TaggedItem`` objects have the following fields: + +* ``tag`` -- The ``Tag`` an object is associated with. +* ``content_type`` -- The ``ContentType`` of the associated model + instance. +* ``object_id`` -- The id of the associated object. +* ``object`` -- The associated object itself, accessible via the + Generic Relations API. + +Manager functions +~~~~~~~~~~~~~~~~~ + +The ``TaggedItem`` model has a custom manager which has the following +helper methods, which accept either a ``QuerySet`` or a ``Model`` +class as one of their arguments. To restrict the objects which are +returned, pass in a filtered ``QuerySet`` for this argument: + +* ``get_by_model(queryset_or_model, tag)`` -- creates a ``QuerySet`` + containing instances of the specififed model which are tagged with + the given tag or tags. + +* ``get_intersection_by_model(queryset_or_model, tags)`` -- creates a + ``QuerySet`` containing instances of the specified model which are + tagged with every tag in a list of tags. + + ``get_by_model`` will call this function behind the scenes when you + pass it a list, so you can use ``get_by_model`` instead of calling + this method directly. + +* ``get_union_by_model(queryset_or_model, tags)`` -- creates a + ``QuerySet`` containing instances of the specified model which are + tagged with any tag in a list of tags. + +.. _`get_related method`: + +* ``get_related(obj, queryset_or_model, num=None)`` - returns a list of + instances of the specified model which share tags with the model + instance ``obj``, ordered by the number of shared tags in descending + order. + + If ``num`` is given, a maximum of ``num`` instances will be returned. + +Basic usage +----------- + +Retrieving tagged objects +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Objects may be retrieved based on their tags using the ``get_by_model`` +manager method:: + + >>> from shop.apps.products.models import Widget + >>> from tagging.models import Tag + >>> house_tag = Tag.objects.get(name='house') + >>> TaggedItem.objects.get_by_model(Widget, house_tag) + [, ] + +Passing a list of tags to ``get_by_model`` returns an intersection of +objects which have those tags, i.e. tag1 AND tag2 ... AND tagN:: + + >>> thing_tag = Tag.objects.get(name='thing') + >>> TaggedItem.objects.get_by_model(Widget, [house_tag, thing_tag]) + [] + +Functions which take tags are flexible when it comes to tag input:: + + >>> TaggedItem.objects.get_by_model(Widget, Tag.objects.filter(name__in=['house', 'thing'])) + [] + >>> TaggedItem.objects.get_by_model(Widget, 'house thing') + [] + >>> TaggedItem.objects.get_by_model(Widget, ['house', 'thing']) + [] + +Restricting objects returned +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pass in a ``QuerySet`` to restrict the objects returned:: + + # Retrieve all Widgets which have a price less than 50, tagged with 'house' + TaggedItem.objects.get_by_model(Widget.objects.filter(price__lt=50), 'house') + + # Retrieve all Widgets which have a name starting with 'a', tagged with any + # of 'house', 'garden' or 'water'. + TaggedItem.objects.get_union_by_model(Widget.objects.filter(name__startswith='a'), + ['house', 'garden', 'water']) + + +Utilities +========= + +Tag-related utility functions are defined in the ``tagging.utils`` +module: + +``parse_tag_input(input)`` +-------------------------- + +Parses tag input, with multiple word input being activated and +delineated by commas and double quotes. Quotes take precedence, so they +may contain commas. + +Returns a sorted list of unique tag names. + +See `tag input`_ for more details. + +``edit_string_for_tags(tags)`` +------------------------------ +Given list of ``Tag`` instances, creates a string representation of the +list suitable for editing by the user, such that submitting the given +string representation back without changing it will give the same list +of tags. + +Tag names which contain commas will be double quoted. + +If any tag name which isn't being quoted contains whitespace, the +resulting string of tag names will be comma-delimited, otherwise it will +be space-delimited. + +``get_tag_list(tags)`` +---------------------- + +Utility function for accepting tag input in a flexible manner. + +If a ``Tag`` object is given, it will be returned in a list as its +single occupant. + +If given, the tag names in the following will be used to create a +``Tag`` ``QuerySet``: + + * A string, which may contain multiple tag names. + * A list or tuple of strings corresponding to tag names. + * A list or tuple of integers corresponding to tag ids. + +If given, the following will be returned as-is: + + * A list or tuple of ``Tag`` objects. + * A ``Tag`` ``QuerySet``. + +``calculate_cloud(tags, steps=4, distribution=tagging.utils.LOGARITHMIC)`` +-------------------------------------------------------------------------- + +Add a ``font_size`` attribute to each tag according to the frequency of +its use, as indicated by its ``count`` attribute. + +``steps`` defines the range of font sizes - ``font_size`` will be an +integer between 1 and ``steps`` (inclusive). + +``distribution`` defines the type of font size distribution algorithm +which will be used - logarithmic or linear. It must be one of +``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. + + +Model Fields +============ + +The ``tagging.fields`` module contains fields which make it easy to +integrate tagging into your models and into the +``django.contrib.admin`` application. + +Field types +----------- + +``TagField`` +~~~~~~~~~~~~ + +A ``CharField`` that actually works as a relationship to tags "under +the hood". + +Using this example model:: + + class Link(models.Model): + ... + tags = TagField() + +Setting tags:: + + >>> l = Link.objects.get(...) + >>> l.tags = 'tag1 tag2 tag3' + +Getting tags for an instance:: + + >>> l.tags + 'tag1 tag2 tag3' + +Getting tags for a model - i.e. all tags used by all instances of the +model:: + + >>> Link.tags + 'tag1 tag2 tag3 tag4 tag5' + +This field will also validate that it has been given a valid list of +tag names, separated by a single comma, a single space or a comma +followed by a space. + + +Form fields +=========== + +The ``tagging.forms`` module contains a ``Field`` for use with +Django's `forms library`_ which takes care of validating tag name +input when used in your forms. + +.. _`forms library`: http://docs.djangoproject.com/en/dev/topics/forms/ + +Field types +----------- + +``TagField`` +~~~~~~~~~~~~ + +A form ``Field`` which is displayed as a single-line text input, which +validates that the input it receives is a valid list of tag names. + +When you generate a form for one of your models automatically, using +the ``ModelForm`` class, any ``tagging.fields.TagField`` fields in your +model will automatically be represented by a ``tagging.forms.TagField`` +in the generated form. + + +Generic views +============= + +The ``tagging.views`` module contains views to handle simple cases of +common display logic related to tagging. + +``tagging.views.TaggedObjectList`` +---------------------------------- + +**Description:** + +A view that displays a list of objects for a given model which have a +given tag. This is a thin wrapper around the +``django.views.generic.list.ListView`` view, which takes a +model and a tag as its arguments (in addition to the other optional +arguments supported by ``ListView``), building the appropriate +``QuerySet`` for you instead of expecting one to be passed in. + +**Required arguments:** + + * ``tag``: The tag which objects of the given model must have in + order to be listed. + +**Optional arguments:** + +Please refer to the `ListView documentation`_ for additional optional +arguments which may be given. + + * ``related_tags``: If ``True``, a ``related_tags`` context variable + will also contain tags related to the given tag for the given + model. + + * ``related_tag_counts``: If ``True`` and ``related_tags`` is + ``True``, each related tag will have a ``count`` attribute + indicating the number of items which have it in addition to the + given tag. + +**Template context:** + +Please refer to the `ListView documentation`_ for additional +template context variables which may be provided. + + * ``tag``: The ``Tag`` instance for the given tag. + +.. _`ListView documentation`: https://docs.djangoproject.com/en/1.8/ref/class-based-views/generic-display/#listview + +Example usage +~~~~~~~~~~~~~ + +The following sample URLconf demonstrates using this generic view to +list items of a particular model class which have a given tag:: + + from django.conf.urls.defaults import * + + from tagging.views import TaggedObjectList + + from shop.apps.products.models import Widget + + urlpatterns = patterns('', + url(r'^widgets/tag/(?P[^/]+(?u))/$', + TaggedObjectList.as_view(model=Widget, paginate_by=10, allow_empty=True), + name='widget_tag_detail'), + ) + +The following sample view demonstrates wrapping this generic view to +perform filtering of the objects which are listed:: + + from myapp.models import People + + from tagging.views import TaggedObjectList + + class TaggedPeopleFilteredList(TaggedObjectList): + queryset = People.objects.filter(country__code=country_code) + +Template tags +============= + +The ``tagging.templatetags.tagging_tags`` module defines a number of +template tags which may be used to work with tags. + +Tag reference +------------- + +tags_for_model +~~~~~~~~~~~~~~ + +Retrieves a list of ``Tag`` objects associated with a given model and +stores them in a context variable. + +Usage:: + + {% tags_for_model [model] as [varname] %} + +The model is specified in ``[appname].[modelname]`` format. + +Extended usage:: + + {% tags_for_model [model] as [varname] with counts %} + +If specified - by providing extra ``with counts`` arguments - adds a +``count`` attribute to each tag containing the number of instances of +the given model which have been tagged with it. + +Examples:: + + {% tags_for_model products.Widget as widget_tags %} + {% tags_for_model products.Widget as widget_tags with counts %} + +tag_cloud_for_model +~~~~~~~~~~~~~~~~~~~ + +Retrieves a list of ``Tag`` objects for a given model, with tag cloud +attributes set, and stores them in a context variable. + +Usage:: + + {% tag_cloud_for_model [model] as [varname] %} + +The model is specified in ``[appname].[modelname]`` format. + +Extended usage:: + + {% tag_cloud_for_model [model] as [varname] with [options] %} + +Extra options can be provided after an optional ``with`` argument, with +each option being specified in ``[name]=[value]`` format. Valid extra +options are: + + ``steps`` + Integer. Defines the range of font sizes. + + ``min_count`` + Integer. Defines the minimum number of times a tag must have + been used to appear in the cloud. + + ``distribution`` + One of ``linear`` or ``log``. Defines the font-size + distribution algorithm to use when generating the tag cloud. + +Examples:: + + {% tag_cloud_for_model products.Widget as widget_tags %} + {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %} + +tags_for_object +~~~~~~~~~~~~~~~ + +Retrieves a list of ``Tag`` objects associated with an object and stores +them in a context variable. + +Usage:: + + {% tags_for_object [object] as [varname] %} + +Example:: + + {% tags_for_object foo_object as tag_list %} + +tagged_objects +~~~~~~~~~~~~~~ + +Retrieves a list of instances of a given model which are tagged with a +given ``Tag`` and stores them in a context variable. + +Usage:: + + {% tagged_objects [tag] in [model] as [varname] %} + +The model is specified in ``[appname].[modelname]`` format. + +The tag must be an instance of a ``Tag``, not the name of a tag. + +Example:: + + {% tagged_objects comedy_tag in tv.Show as comedies %} diff --git a/docs/overview.txt b/docs/overview.txt deleted file mode 100644 index feb08c9..0000000 --- a/docs/overview.txt +++ /dev/null @@ -1,904 +0,0 @@ -============== -Django Tagging -============== - -A generic tagging application for `Django`_ projects, which allows -association of a number of tags with any Django model instance and makes -retrieval of tags simple. - -.. _`Django`: http://www.djangoproject.com - -.. contents:: - :depth: 3 - - -Installation -============ - -Installing an official release ------------------------------- - -Official releases are made available from -http://code.google.com/p/django-tagging/ - -Source distribution -~~~~~~~~~~~~~~~~~~~ - -Download the .zip distribution file and unpack it. Inside is a script -named ``setup.py``. Enter this command:: - - python setup.py install - -...and the package will install automatically. - -Windows installer -~~~~~~~~~~~~~~~~~ - -A Windows installer is also made available - download the .exe -distribution file and launch it to install the application. - -An uninstaller will also be created, accessible through Add/Remove -Programs in your Control Panel. - -Installing the development version ----------------------------------- - -Alternatively, if you'd like to update Django Tagging occasionally to pick -up the latest bug fixes and enhancements before they make it into an -official release, perform a `Subversion`_ checkout instead. The following -command will check the application's development branch out to an -``tagging-trunk`` directory:: - - svn checkout http://django-tagging.googlecode.com/svn/trunk/ tagging-trunk - -Add the resulting folder to your `PYTHONPATH`_ or symlink (`junction`_, -if you're on Windows) the ``tagging`` directory inside it into a -directory which is on your PYTHONPATH, such as your Python -installation's ``site-packages`` directory. - -You can verify that the application is available on your PYTHONPATH by -opening a Python interpreter and entering the following commands:: - - >>> import tagging - >>> tagging.VERSION - (0, 3, 'pre') - -When you want to update your copy of the Django Tagging source code, run -the command ``svn update`` from within the ``tagging-trunk`` directory. - -.. caution:: - - The development version may contain bugs which are not present in the - release version and introduce backwards-incompatible changes. - - If you're tracking trunk, keep an eye on the `CHANGELOG`_ and the - `backwards-incompatible changes wiki page`_ before you update your - copy of the source code. - -.. _`Subversion`: http://subversion.tigris.org -.. _`PYTHONPATH`: http://www.python.org/doc/2.5.2/tut/node8.html#SECTION008120000000000000000 -.. _`junction`: http://www.microsoft.com/technet/sysinternals/FileAndDisk/Junction.mspx -.. _`CHANGELOG`: http://django-tagging.googlecode.com/svn/trunk/CHANGELOG.txt -.. _`backwards-incompatible changes wiki page`: http://code.google.com/p/django-tagging/wiki/BackwardsIncompatibleChanges - -Using Django Tagging in your applications ------------------------------------------ - -Once you've installed Django Tagging and want to use it in your Django -applications, do the following: - - 1. Put ``'tagging'`` in your ``INSTALLED_APPS`` setting. - 2. Run the command ``manage.py syncdb``. - -The ``syncdb`` command creates the necessary database tables and -creates permission objects for all installed apps that need them. - -That's it! - - -Settings -======== - -Some of the application's behaviour can be configured by adding the -appropriate settings to your project's settings file. - -The following settings are available: - -FORCE_LOWERCASE_TAGS --------------------- - -Default: ``False`` - -A boolean that turns on/off forcing of all tag names to lowercase before -they are saved to the database. - -MAX_TAG_LENGTH --------------- - -Default: ``50`` - -An integer which specifies the maximum length which any tag is allowed -to have. This is used for validation in the ``django.contrib.admin`` -application and in any forms automatically generated using ``ModelForm``. - - -Registering your models -======================= - -Your Django models can be registered with the tagging application to -access some additional tagging-related features. - -.. note:: - - You don't *have* to register your models in order to use them with - the tagging application - many of the features added by registration - are just convenience wrappers around the tagging API provided by the - ``Tag`` and ``TaggedItem`` models and their managers, as documented - further below. - -The ``register`` function -------------------------- - -To register a model, import the ``tagging`` module and call its -``register`` function, like so:: - - from django.db import models - - import tagging - - class Widget(models.Model): - name = models.CharField(max_length=50) - - tagging.register(Widget) - -The following argument is required: - -``model`` - The model class to be registered. - - An exception will be raised if you attempt to register the same class - more than once. - -The following arguments are optional, with some recommended defaults - -take care to specify different attribute names if the defaults clash -with your model class' definition: - -``tag_descriptor_attr`` - The name of an attribute in the model class which will hold a tag - descriptor for the model. Default: ``'tags'`` - - See `TagDescriptor`_ below for details about the use of this - descriptor. - -``tagged_item_manger_attr`` - The name of an attribute in the model class which will hold a custom - manager for accessing tagged items for the model. Default: - ``'tagged'``. - - See `ModelTaggedItemManager`_ below for details about the use of this - manager. - -``TagDescriptor`` ------------------ - -When accessed through the model class itself, this descriptor will return -a ``ModelTagManager`` for the model. See `ModelTagManager`_ below for -more details about its use. - -When accessed through a model instance, this descriptor provides a handy -means of retrieving, updating and deleting the instance's tags. For -example:: - - >>> widget = Widget.objects.create(name='Testing descriptor') - >>> widget.tags - [] - >>> widget.tags = 'toast, melted cheese, butter' - >>> widget.tags - [, , ] - >>> del widget.tags - >>> widget.tags - [] - -``ModelTagManager`` -------------------- - -A manager for retrieving tags used by a particular model. - -Defines the following methods: - -* ``get_query_set()`` -- as this method is redefined, any ``QuerySets`` - created by this model will be initially restricted to contain the - distinct tags used by all the model's instances. - -* ``cloud(*args, **kwargs)`` -- creates a list of tags used by the - model's instances, with ``count`` and ``font_size`` attributes set for - use in displaying a tag cloud. - - See the documentation on ``Tag``'s manager's `cloud_for_model method`_ - for information on additional arguments which can be given. - -* ``related(self, tags, *args, **kwargs)`` -- creates a list of tags - used by the model's instances, which are also used by all instance - which have the given ``tags``. - - See the documentation on ``Tag``'s manager's - `related_for_model method`_ for information on additional arguments - which can be given. - -* ``usage(self, *args, **kwargs))`` -- creates a list of tags used by - the model's instances, with optional usages counts, restriction based - on usage counts and restriction of the model instances from which - usage and counts are determined. - - See the documentation on ``Tag``'s manager's `usage_for_model method`_ - for information on additional arguments which can be given. - -Example usage:: - - # Create a ``QuerySet`` of tags used by Widget instances - Widget.tags.all() - - # Retrieve a list of tags used by Widget instances with usage counts - Widget.tags.usage(counts=True) - - # Retrieve tags used by instances of WIdget which are also tagged with - # 'cheese' and 'toast' - Widget.tags.related(['cheese', 'toast'], counts=True, min_count=3) - -``ModelTaggedItemManager`` --------------------------- - -A manager for retrieving model instance for a particular model, based on -their tags. - -* ``related_to(obj, queryset=None, num=None)`` -- creates a list - of model instances which are related to ``obj``, based on its tags. If - a ``queryset`` argument is provided, it will be used to restrict the - resulting list of model instances. - - If ``num`` is given, a maximum of ``num`` instances will be returned. - -* ``with_all(tags, queryset=None)`` -- creates a ``QuerySet`` containing - model instances which are tagged with *all* the given tags. If a - ``queryset`` argument is provided, it will be used as the basis for - the resulting ``QuerySet``. - -* ``with_any(tags, queryset=None)`` -- creates a ``QuerySet`` containing model - instances which are tagged with *any* the given tags. If a ``queryset`` - argument is provided, it will be used as the basis for the resulting - ``QuerySet``. - - -Tags -==== - -Tags are represented by the ``Tag`` model, which lives in the -``tagging.models`` module. - -API reference -------------- - -Fields -~~~~~~ - -``Tag`` objects have the following fields: - -* ``name`` -- The name of the tag. This is a unique value. - -Manager functions -~~~~~~~~~~~~~~~~~ - -The ``Tag`` model has a custom manager which has the following helper -methods: - -* ``update_tags(obj, tag_names)`` -- updates tags associated with an - object. - - ``tag_names`` is a string containing tag names with which ``obj`` - should be tagged. - - If ``tag_names`` is ``None`` or ``''``, the object's tags will be - cleared. - -* ``add_tag(obj, tag_name)`` -- associates a tag with an an object. - - ``tag_name`` is a string containing a tag name with which ``obj`` - should be tagged. - -* ``get_for_object(obj)`` -- returns a ``QuerySet`` containing all - ``Tag`` objects associated with ``obj``. - -.. _`usage_for_model method`: - -* ``usage_for_model(model, counts=False, min_count=None, filters=None)`` - -- returns a list of ``Tag`` objects associated with instances of - ``model``. - - If ``counts`` is ``True``, a ``count`` attribute will be added to each - tag, indicating how many times it has been associated with instances - of ``model``. - - If ``min_count`` is given, only tags which have a ``count`` greater - than or equal to ``min_count`` will be returned. Passing a value for - ``min_count`` implies ``counts=True``. - - To limit the tags (and counts, if specified) returned to those used by - a subset of the model's instances, pass a dictionary of field lookups - to be applied to ``model`` as the ``filters`` argument. - -.. _`related_for_model method`: - -* ``related_for_model(tags, Model, counts=False, min_count=None)`` - -- returns a list of tags related to a given list of tags - that is, - other tags used by items which have all the given tags. - - If ``counts`` is ``True``, a ``count`` attribute will be added to each - tag, indicating the number of items which have it in addition to the - given list of tags. - - If ``min_count`` is given, only tags which have a ``count`` greater - than or equal to ``min_count`` will be returned. Passing a value for - ``min_count`` implies ``counts=True``. - -.. _`cloud_for_model method`: - -* ``cloud_for_model(Model, steps=4, distribution=LOGARITHMIC, - filters=None, min_count=None)`` -- returns a list of the distinct - ``Tag`` objects associated with instances of ``Model``, each having a - ``count`` attribute as above and an additional ``font_size`` - attribute, for use in creation of a tag cloud (a type of weighted - list). - - ``steps`` defines the number of font sizes available - ``font_size`` - may be an integer between ``1`` and ``steps``, inclusive. - - ``distribution`` defines the type of font size distribution algorithm - which will be used - logarithmic or linear. It must be either - ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. - - To limit the tags displayed in the cloud to those associated with a - subset of the Model's instances, pass a dictionary of field lookups to - be applied to the given Model as the ``filters`` argument. - - To limit the tags displayed in the cloud to those with a ``count`` - greater than or equal to ``min_count``, pass a value for the - ``min_count`` argument. - -* ``usage_for_queryset(queryset, counts=False, min_count=None)`` -- - Obtains a list of tags associated with instances of a model contained - in the given queryset. - - If ``counts`` is True, a ``count`` attribute will be added to each tag, - indicating how many times it has been used against the Model class in - question. - - If ``min_count`` is given, only tags which have a ``count`` greater - than or equal to ``min_count`` will be returned. - - Passing a value for ``min_count`` implies ``counts=True``. - -Basic usage ------------ - -Tagging objects and retrieving an object's tags -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Objects may be tagged using the ``update_tags`` helper function:: - - >>> from shop.apps.products.models import Widget - >>> from tagging.models import Tag - >>> widget = Widget.objects.get(pk=1) - >>> Tag.objects.update_tags(widget, 'house thing') - -Retrieve tags for an object using the ``get_for_object`` helper -function:: - - >>> Tag.objects.get_for_object(widget) - [, ] - -Tags are created, associated and unassociated accordingly when you use -``update_tags`` and ``add_tag``:: - - >>> Tag.objects.update_tags(widget, 'house monkey') - >>> Tag.objects.get_for_object(widget) - [, ] - >>> Tag.objects.add_tag(widget, 'tiles') - >>> Tag.objects.get_for_object(widget) - [, , ] - -Clear an object's tags by passing ``None`` or ``''`` to -``update_tags``:: - - >>> Tag.objects.update_tags(widget, None) - >>> Tag.objects.get_for_object(widget) - [] - -Retrieving tags used by a particular model -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To retrieve all tags used for a particular model, use the -``get_for_model`` helper function:: - - >>> widget1 = Widget.objects.get(pk=1) - >>> Tag.objects.update_tags(widget1, 'house thing') - >>> widget2 = Widget.objects.get(pk=2) - >>> Tag.objects.update_tags(widget2, 'cheese toast house') - >>> Tag.objects.usage_for_model(Widget) - [, , , ] - -To get a count of how many times each tag was used for a particular -model, pass in ``True`` for the ``counts`` argument:: - - >>> tags = Tag.objects.usage_for_model(Widget, counts=True) - >>> [(tag.name, tag.count) for tag in tags] - [('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)] - -To get counts and limit the tags returned to those with counts above a -certain size, pass in a ``min_count`` argument:: - - >>> tags = Tag.objects.usage_for_model(Widget, min_count=2) - >>> [(tag.name, tag.count) for tag in tags] - [('house', 2)] - -You can also specify a dictionary of `field lookups`_ to be used to -restrict the tags and counts returned based on a subset of the -model's instances. For example, the following would retrieve all tags -used on Widgets created by a user named Alan which have a size -greater than 99:: - - >>> Tag.objects.usage_for_model(Widget, filters=dict(size__gt=99, user__username='Alan')) - -.. _`field lookups`: http://docs.djangoproject.com/en/dev/topics/db/queries/#field-lookups - -The ``usage_for_queryset`` method allows you to pass a pre-filtered -queryset to be used when determining tag usage:: - - >>> Tag.objects.usage_for_queryset(Widget.objects.filter(size__gt=99, user__username='Alan')) - -Tag input ---------- - -Tag input from users is treated as follows: - -* If the input doesn't contain any commas or double quotes, it is simply - treated as a space-delimited list of tag names. - -* If the input does contain either of these characters, we parse the - input like so: - - * Groups of characters which appear between double quotes take - precedence as multi-word tags (so double quoted tag names may - contain commas). An unclosed double quote will be ignored. - - * For the remaining input, if there are any unquoted commas in the - input, the remainder will be treated as comma-delimited. Otherwise, - it will be treated as space-delimited. - -Examples: - -====================== ======================================= ================================================ -Tag input string Resulting tags Notes -====================== ======================================= ================================================ -apple ball cat [``apple``], [``ball``], [``cat``] No commas, so space delimited -apple, ball cat [``apple``], [``ball cat``] Comma present, so comma delimited -"apple, ball" cat dog [``apple, ball``], [``cat``], [``dog``] All commas are quoted, so space delimited -"apple, ball", cat dog [``apple, ball``], [``cat dog``] Contains an unquoted comma, so comma delimited -apple "ball cat" dog [``apple``], [``ball cat``], [``dog``] No commas, so space delimited -"apple" "ball dog [``apple``], [``ball``], [``dog``] Unclosed double quote is ignored -====================== ======================================= ================================================ - - -Tagged items -============ - -The relationship between a ``Tag`` and an object is represented by -the ``TaggedItem`` model, which lives in the ``tagging.models`` -module. - -API reference -------------- - -Fields -~~~~~~ - -``TaggedItem`` objects have the following fields: - -* ``tag`` -- The ``Tag`` an object is associated with. -* ``content_type`` -- The ``ContentType`` of the associated model - instance. -* ``object_id`` -- The id of the associated object. -* ``object`` -- The associated object itself, accessible via the - Generic Relations API. - -Manager functions -~~~~~~~~~~~~~~~~~ - -The ``TaggedItem`` model has a custom manager which has the following -helper methods, which accept either a ``QuerySet`` or a ``Model`` -class as one of their arguments. To restrict the objects which are -returned, pass in a filtered ``QuerySet`` for this argument: - -* ``get_by_model(queryset_or_model, tag)`` -- creates a ``QuerySet`` - containing instances of the specififed model which are tagged with - the given tag or tags. - -* ``get_intersection_by_model(queryset_or_model, tags)`` -- creates a - ``QuerySet`` containing instances of the specified model which are - tagged with every tag in a list of tags. - - ``get_by_model`` will call this function behind the scenes when you - pass it a list, so you can use ``get_by_model`` instead of calling - this method directly. - -* ``get_union_by_model(queryset_or_model, tags)`` -- creates a - ``QuerySet`` containing instances of the specified model which are - tagged with any tag in a list of tags. - -.. _`get_related method`: - -* ``get_related(obj, queryset_or_model, num=None)`` - returns a list of - instances of the specified model which share tags with the model - instance ``obj``, ordered by the number of shared tags in descending - order. - - If ``num`` is given, a maximum of ``num`` instances will be returned. - -Basic usage ------------ - -Retrieving tagged objects -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Objects may be retrieved based on their tags using the ``get_by_model`` -manager method:: - - >>> from shop.apps.products.models import Widget - >>> from tagging.models import Tag - >>> house_tag = Tag.objects.get(name='house') - >>> TaggedItem.objects.get_by_model(Widget, house_tag) - [, ] - -Passing a list of tags to ``get_by_model`` returns an intersection of -objects which have those tags, i.e. tag1 AND tag2 ... AND tagN:: - - >>> thing_tag = Tag.objects.get(name='thing') - >>> TaggedItem.objects.get_by_model(Widget, [house_tag, thing_tag]) - [] - -Functions which take tags are flexible when it comes to tag input:: - - >>> TaggedItem.objects.get_by_model(Widget, Tag.objects.filter(name__in=['house', 'thing'])) - [] - >>> TaggedItem.objects.get_by_model(Widget, 'house thing') - [] - >>> TaggedItem.objects.get_by_model(Widget, ['house', 'thing']) - [] - -Restricting objects returned -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Pass in a ``QuerySet`` to restrict the objects returned:: - - # Retrieve all Widgets which have a price less than 50, tagged with 'house' - TaggedItem.objects.get_by_model(Widget.objects.filter(price__lt=50), 'house') - - # Retrieve all Widgets which have a name starting with 'a', tagged with any - # of 'house', 'garden' or 'water'. - TaggedItem.objects.get_union_by_model(Widget.objects.filter(name__startswith='a'), - ['house', 'garden', 'water']) - - -Utilities -========= - -Tag-related utility functions are defined in the ``tagging.utils`` -module: - -``parse_tag_input(input)`` --------------------------- - -Parses tag input, with multiple word input being activated and -delineated by commas and double quotes. Quotes take precedence, so they -may contain commas. - -Returns a sorted list of unique tag names. - -See `tag input`_ for more details. - -``edit_string_for_tags(tags)`` ------------------------------- -Given list of ``Tag`` instances, creates a string representation of the -list suitable for editing by the user, such that submitting the given -string representation back without changing it will give the same list -of tags. - -Tag names which contain commas will be double quoted. - -If any tag name which isn't being quoted contains whitespace, the -resulting string of tag names will be comma-delimited, otherwise it will -be space-delimited. - -``get_tag_list(tags)`` ----------------------- - -Utility function for accepting tag input in a flexible manner. - -If a ``Tag`` object is given, it will be returned in a list as its -single occupant. - -If given, the tag names in the following will be used to create a -``Tag`` ``QuerySet``: - - * A string, which may contain multiple tag names. - * A list or tuple of strings corresponding to tag names. - * A list or tuple of integers corresponding to tag ids. - -If given, the following will be returned as-is: - - * A list or tuple of ``Tag`` objects. - * A ``Tag`` ``QuerySet``. - -``calculate_cloud(tags, steps=4, distribution=tagging.utils.LOGARITHMIC)`` --------------------------------------------------------------------------- - -Add a ``font_size`` attribute to each tag according to the frequency of -its use, as indicated by its ``count`` attribute. - -``steps`` defines the range of font sizes - ``font_size`` will be an -integer between 1 and ``steps`` (inclusive). - -``distribution`` defines the type of font size distribution algorithm -which will be used - logarithmic or linear. It must be one of -``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. - - -Model Fields -============ - -The ``tagging.fields`` module contains fields which make it easy to -integrate tagging into your models and into the -``django.contrib.admin`` application. - -Field types ------------ - -``TagField`` -~~~~~~~~~~~~ - -A ``CharField`` that actually works as a relationship to tags "under -the hood". - -Using this example model:: - - class Link(models.Model): - ... - tags = TagField() - -Setting tags:: - - >>> l = Link.objects.get(...) - >>> l.tags = 'tag1 tag2 tag3' - -Getting tags for an instance:: - - >>> l.tags - 'tag1 tag2 tag3' - -Getting tags for a model - i.e. all tags used by all instances of the -model:: - - >>> Link.tags - 'tag1 tag2 tag3 tag4 tag5' - -This field will also validate that it has been given a valid list of -tag names, separated by a single comma, a single space or a comma -followed by a space. - - -Form fields -=========== - -The ``tagging.forms`` module contains a ``Field`` for use with -Django's `forms library`_ which takes care of validating tag name -input when used in your forms. - -.. _`forms library`: http://docs.djangoproject.com/en/dev/topics/forms/ - -Field types ------------ - -``TagField`` -~~~~~~~~~~~~ - -A form ``Field`` which is displayed as a single-line text input, which -validates that the input it receives is a valid list of tag names. - -When you generate a form for one of your models automatically, using -the ``ModelForm`` class, any ``tagging.fields.TagField`` fields in your -model will automatically be represented by a ``tagging.forms.TagField`` -in the generated form. - - -Generic views -============= - -The ``tagging.views`` module contains views to handle simple cases of -common display logic related to tagging. - -``tagging.views.tagged_object_list`` ------------------------------------- - -**Description:** - -A view that displays a list of objects for a given model which have a -given tag. This is a thin wrapper around the -``django.views.generic.list_detail.object_list`` view, which takes a -model and a tag as its arguments (in addition to the other optional -arguments supported by ``object_list``), building the appropriate -``QuerySet`` for you instead of expecting one to be passed in. - -**Required arguments:** - - * ``queryset_or_model``: A ``QuerySet`` or Django model class for the - object which will be listed. - - * ``tag``: The tag which objects of the given model must have in - order to be listed. - -**Optional arguments:** - -Please refer to the `object_list documentation`_ for additional optional -arguments which may be given. - - * ``related_tags``: If ``True``, a ``related_tags`` context variable - will also contain tags related to the given tag for the given - model. - - * ``related_tag_counts``: If ``True`` and ``related_tags`` is - ``True``, each related tag will have a ``count`` attribute - indicating the number of items which have it in addition to the - given tag. - -**Template context:** - -Please refer to the `object_list documentation`_ for additional -template context variables which may be provided. - - * ``tag``: The ``Tag`` instance for the given tag. - -.. _`object_list documentation`: http://docs.djangoproject.com/en/dev/ref/generic-views/#django-views-generic-list-detail-object-list - -Example usage -~~~~~~~~~~~~~ - -The following sample URLconf demonstrates using this generic view to -list items of a particular model class which have a given tag:: - - from django.conf.urls.defaults import * - - from tagging.views import tagged_object_list - - from shop.apps.products.models import Widget - - urlpatterns = patterns('', - url(r'^widgets/tag/(?P[^/]+)/$', - tagged_object_list, - dict(queryset_or_model=Widget, paginate_by=10, allow_empty=True, - template_object_name='widget'), - name='widget_tag_detail'), - ) - -The following sample view demonstrates wrapping this generic view to -perform filtering of the objects which are listed:: - - from myapp.models import People - - from tagging.views import tagged_object_list - - def tagged_people(request, country_code, tag): - queryset = People.objects.filter(country__code=country_code) - return tagged_object_list(request, queryset, tag, paginate_by=25, - allow_empty=True, template_object_name='people') - - -Template tags -============= - -The ``tagging.templatetags.tagging_tags`` module defines a number of -template tags which may be used to work with tags. - -Tag reference -------------- - -tags_for_model -~~~~~~~~~~~~~~ - -Retrieves a list of ``Tag`` objects associated with a given model and -stores them in a context variable. - -Usage:: - - {% tags_for_model [model] as [varname] %} - -The model is specified in ``[appname].[modelname]`` format. - -Extended usage:: - - {% tags_for_model [model] as [varname] with counts %} - -If specified - by providing extra ``with counts`` arguments - adds a -``count`` attribute to each tag containing the number of instances of -the given model which have been tagged with it. - -Examples:: - - {% tags_for_model products.Widget as widget_tags %} - {% tags_for_model products.Widget as widget_tags with counts %} - -tag_cloud_for_model -~~~~~~~~~~~~~~~~~~~ - -Retrieves a list of ``Tag`` objects for a given model, with tag cloud -attributes set, and stores them in a context variable. - -Usage:: - - {% tag_cloud_for_model [model] as [varname] %} - -The model is specified in ``[appname].[modelname]`` format. - -Extended usage:: - - {% tag_cloud_for_model [model] as [varname] with [options] %} - -Extra options can be provided after an optional ``with`` argument, with -each option being specified in ``[name]=[value]`` format. Valid extra -options are: - - ``steps`` - Integer. Defines the range of font sizes. - - ``min_count`` - Integer. Defines the minimum number of times a tag must have - been used to appear in the cloud. - - ``distribution`` - One of ``linear`` or ``log``. Defines the font-size - distribution algorithm to use when generating the tag cloud. - -Examples:: - - {% tag_cloud_for_model products.Widget as widget_tags %} - {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %} - -tags_for_object -~~~~~~~~~~~~~~~ - -Retrieves a list of ``Tag`` objects associated with an object and stores -them in a context variable. - -Usage:: - - {% tags_for_object [object] as [varname] %} - -Example:: - - {% tags_for_object foo_object as tag_list %} - -tagged_objects -~~~~~~~~~~~~~~ - -Retrieves a list of instances of a given model which are tagged with a -given ``Tag`` and stores them in a context variable. - -Usage:: - - {% tagged_objects [tag] in [model] as [varname] %} - -The model is specified in ``[appname].[modelname]`` format. - -The tag must be an instance of a ``Tag``, not the name of a tag. - -Example:: - - {% tagged_objects comedy_tag in tv.Show as comedies %} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6c71b61 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[wheel] +universal = 1 + +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py index 0670dfe..3e7ccf0 100644 --- a/setup.py +++ b/setup.py @@ -1,70 +1,42 @@ """ Based entirely on Django's own ``setup.py``. """ -import os -from distutils.command.install import INSTALL_SCHEMES -from distutils.core import setup +from setuptools import setup +from setuptools import find_packages import tagging - - -def fullsplit(path, result=None): - """ - Split a pathname into components (the opposite of os.path.join) in a - platform-neutral way. - """ - if result is None: - result = [] - head, tail = os.path.split(path) - if head == '': - return [tail] + result - if head == path: - return result - return fullsplit(head, [tail] + result) - -# Tell distutils to put the data_files in platform-specific installation -# locations. See here for an explanation: -# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb -for scheme in INSTALL_SCHEMES.values(): - scheme['data'] = scheme['purelib'] - -# Compile the list of packages available, because distutils doesn't have -# an easy way to do this. -packages, data_files = [], [] -root_dir = os.path.dirname(__file__) -tagging_dir = os.path.join(root_dir, 'tagging') -pieces = fullsplit(root_dir) -if pieces[-1] == '': - len_root_dir = len(pieces) - 1 -else: - len_root_dir = len(pieces) - -for dirpath, dirnames, filenames in os.walk(tagging_dir): - # Ignore dirnames that start with '.' - for i, dirname in enumerate(dirnames): - if dirname.startswith('.'): del dirnames[i] - if '__init__.py' in filenames: - packages.append('.'.join(fullsplit(dirpath)[len_root_dir:])) - elif filenames: - data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) - - setup( - name = 'django-tagging', - version = tagging.get_version(), - description = 'Generic tagging application for Django', - author = 'Jonathan Buchanan', - author_email = 'jonathan.buchanan@gmail.com', - url = 'http://code.google.com/p/django-tagging/', - packages = packages, - data_files = data_files, - classifiers = ['Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Utilities'], + name='django-tagging', + version=tagging.__version__, + + description='Generic tagging application for Django', + long_description='\n'.join([open('README.rst').read(), + open('CHANGELOG.txt').read()]), + keywords='django, tag, tagging', + + author=tagging.__author__, + author_email=tagging.__author_email__, + maintainer=tagging.__maintainer__, + maintainer_email=tagging.__maintainer_email__, + url=tagging.__url__, + license=tagging.__license__, + + packages=find_packages(), + include_package_data=True, + zip_safe=False, + + classifiers=[ + 'Framework :: Django', + 'Environment :: Web Environment', + 'Operating System :: OS Independent', + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Topic :: Utilities', + 'Topic :: Software Development :: Libraries :: Python Modules'] ) diff --git a/tagging/._models.py b/tagging/._models.py deleted file mode 100644 index bdd7231..0000000 Binary files a/tagging/._models.py and /dev/null differ diff --git a/tagging/._settings.py b/tagging/._settings.py deleted file mode 100644 index 3c4ee38..0000000 Binary files a/tagging/._settings.py and /dev/null differ diff --git a/tagging/__init__.py b/tagging/__init__.py index 03d6c03..735691d 100644 --- a/tagging/__init__.py +++ b/tagging/__init__.py @@ -1,62 +1,15 @@ -VERSION = (0, 3, 1, "final", 0) +""" +Django-tagging +""" +__version__ = '0.4' +__license__ = 'BSD License' +__author__ = 'Jonathan Buchanan' +__author_email__ = 'jonathan.buchanan@gmail.com' +__maintainer__ = 'Fantomas42' +__maintainer_email__ = 'fantomas42@gmail.com' -def get_version(): - if VERSION[3] == "final": - return "%s.%s.%s" % (VERSION[0], VERSION[1], VERSION[2]) - elif VERSION[3] == "dev": - if VERSION[2] == 0: - return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[3], VERSION[4]) - return "%s.%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3], VERSION[4]) - else: - return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3]) +__url__ = 'https://github.com/Fantomas42/django-tagging' - -__version__ = get_version() - - -class AlreadyRegistered(Exception): - """ - An attempt was made to register a model more than once. - """ - pass - - -registry = [] - - -def register(model, tag_descriptor_attr='tags', - tagged_item_manager_attr='tagged'): - """ - Sets the given model class up for working with tags. - """ - - from tagging.managers import ModelTaggedItemManager, TagDescriptor - - if model in registry: - raise AlreadyRegistered("The model '%s' has already been " - "registered." % model._meta.object_name) - if hasattr(model, tag_descriptor_attr): - raise AttributeError("'%s' already has an attribute '%s'. You must " - "provide a custom tag_descriptor_attr to register." % ( - model._meta.object_name, - tag_descriptor_attr, - ) - ) - if hasattr(model, tagged_item_manager_attr): - raise AttributeError("'%s' already has an attribute '%s'. You must " - "provide a custom tagged_item_manager_attr to register." % ( - model._meta.object_name, - tagged_item_manager_attr, - ) - ) - - # Add tag descriptor - setattr(model, tag_descriptor_attr, TagDescriptor()) - - # Add custom manager - ModelTaggedItemManager().contribute_to_class(model, tagged_item_manager_attr) - - # Finally register in registry - registry.append(model) +default_app_config = 'tagging.apps.TaggingConfig' diff --git a/tagging/admin.py b/tagging/admin.py index bec3922..8eb9833 100644 --- a/tagging/admin.py +++ b/tagging/admin.py @@ -1,13 +1,16 @@ +""" +Admin components for tagging. +""" from django.contrib import admin -from tagging.models import Tag, TaggedItem + +from tagging.models import Tag +from tagging.models import TaggedItem from tagging.forms import TagAdminForm + class TagAdmin(admin.ModelAdmin): form = TagAdminForm + admin.site.register(TaggedItem) admin.site.register(Tag, TagAdmin) - - - - diff --git a/tagging/apps.py b/tagging/apps.py new file mode 100644 index 0000000..492318c --- /dev/null +++ b/tagging/apps.py @@ -0,0 +1,14 @@ +""" +Apps for tagging. +""" +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class TaggingConfig(AppConfig): + """ + Config for Tagging application. + """ + name = 'tagging' + label = 'tagging' + verbose_name = _('Tagging') diff --git a/tagging/fields.py b/tagging/fields.py index f52daff..dd4a936 100644 --- a/tagging/fields.py +++ b/tagging/fields.py @@ -8,6 +8,8 @@ from django.utils.translation import ugettext_lazy as _ from tagging import settings from tagging.models import Tag from tagging.utils import edit_string_for_tags +from tagging.forms import TagField as TagFormField + class TagField(CharField): """ @@ -58,7 +60,8 @@ class TagField(CharField): self._set_instance_tag_cache(instance, '') else: self._set_instance_tag_cache( - instance, edit_string_for_tags(Tag.objects.get_for_object(instance))) + instance, edit_string_for_tags( + Tag.objects.get_for_object(instance))) return self._get_instance_tag_cache(instance) def __set__(self, instance, value): @@ -66,12 +69,13 @@ class TagField(CharField): Set an object's tags. """ if instance is None: - raise AttributeError(_('%s can only be set on instances.') % self.name) + raise AttributeError( + _('%s can only be set on instances.') % self.name) if settings.FORCE_LOWERCASE_TAGS and value is not None: value = value.lower() self._set_instance_tag_cache(instance, value) - def _save(self, **kwargs): #signal, sender, instance): + def _save(self, **kwargs): # signal, sender, instance): """ Save tags back to the database """ @@ -101,7 +105,6 @@ class TagField(CharField): return 'CharField' def formfield(self, **kwargs): - from tagging import forms - defaults = {'form_class': forms.TagField} + defaults = {'form_class': TagFormField} defaults.update(kwargs) return super(TagField, self).formfield(**defaults) diff --git a/tagging/forms.py b/tagging/forms.py index a2d9fd9..e597f2d 100644 --- a/tagging/forms.py +++ b/tagging/forms.py @@ -1,5 +1,5 @@ """ -Tagging components for Django's form library. +Form components for tagging. """ from django import forms from django.utils.translation import ugettext as _ @@ -8,21 +8,20 @@ from tagging import settings from tagging.models import Tag from tagging.utils import parse_tag_input + class TagAdminForm(forms.ModelForm): class Meta: model = Tag + fields = ('name',) def clean_name(self): value = self.cleaned_data['name'] tag_names = parse_tag_input(value) if len(tag_names) > 1: raise forms.ValidationError(_('Multiple tags were given.')) - elif len(tag_names[0]) > settings.MAX_TAG_LENGTH: - raise forms.ValidationError( - _('A tag may be no more than %s characters long.') % - settings.MAX_TAG_LENGTH) return value + class TagField(forms.CharField): """ A ``CharField`` which validates that its input is a valid list of @@ -30,11 +29,9 @@ class TagField(forms.CharField): """ def clean(self, value): value = super(TagField, self).clean(value) - if value == u'': - return value for tag_name in parse_tag_input(value): if len(tag_name) > settings.MAX_TAG_LENGTH: raise forms.ValidationError( _('Each tag may be no more than %s characters long.') % - settings.MAX_TAG_LENGTH) + settings.MAX_TAG_LENGTH) return value diff --git a/tagging/generic.py b/tagging/generic.py index 75d1b8e..770e928 100644 --- a/tagging/generic.py +++ b/tagging/generic.py @@ -1,5 +1,9 @@ +""" +Generic components for tagging. +""" from django.contrib.contenttypes.models import ContentType + def fetch_content_objects(tagged_items, select_related_for=None): """ Retrieves ``ContentType`` and content objects for the given list of @@ -15,7 +19,8 @@ def fetch_content_objects(tagged_items, select_related_for=None): ``ContentType``) for which ``select_related`` should be used when retrieving model instances. """ - if select_related_for is None: select_related_for = [] + if select_related_for is None: + select_related_for = [] # Group content object pks by their content type pks objects = {} @@ -27,9 +32,11 @@ def fetch_content_objects(tagged_items, select_related_for=None): for content_type_pk, object_pks in objects.iteritems(): model = content_types[content_type_pk].model_class() if content_types[content_type_pk].model in select_related_for: - objects[content_type_pk] = model._default_manager.select_related().in_bulk(object_pks) + objects[content_type_pk] = model._default_manager.select_related( + ).in_bulk(object_pks) else: - objects[content_type_pk] = model._default_manager.in_bulk(object_pks) + objects[content_type_pk] = model._default_manager.in_bulk( + object_pks) # Set content types and content objects in the appropriate cache # attributes, so accessing the 'content_type' and 'object' diff --git a/tagging/managers.py b/tagging/managers.py index 02cd1c2..d85c260 100644 --- a/tagging/managers.py +++ b/tagging/managers.py @@ -1,17 +1,18 @@ """ -Custom managers for Django models registered with the tagging -application. +Custom managers for tagging. """ -from django.contrib.contenttypes.models import ContentType from django.db import models +from django.contrib.contenttypes.models import ContentType + +from tagging.models import Tag +from tagging.models import TaggedItem -from tagging.models import Tag, TaggedItem class ModelTagManager(models.Manager): """ A manager for retrieving tags for a particular model. """ - def get_query_set(self): + def get_queryset(self): ctype = ContentType.objects.get_for_model(self.model) return Tag.objects.filter( items__content_type__pk=ctype.pk).distinct() @@ -25,6 +26,7 @@ class ModelTagManager(models.Manager): def usage(self, *args, **kwargs): return Tag.objects.usage_for_model(self.model, *args, **kwargs) + class ModelTaggedItemManager(models.Manager): """ A manager for retrieving model instances based on their tags. @@ -47,6 +49,7 @@ class ModelTaggedItemManager(models.Manager): else: return TaggedItem.objects.get_union_by_model(queryset, tags) + class TagDescriptor(object): """ A descriptor which provides access to a ``ModelTagManager`` for diff --git a/tagging/migrations/0001_initial.py b/tagging/migrations/0001_initial.py new file mode 100644 index 0000000..d784916 --- /dev/null +++ b/tagging/migrations/0001_initial.py @@ -0,0 +1,52 @@ +from django.db import models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField( + unique=True, max_length=50, + verbose_name='name', db_index=True)), + ], + options={ + 'ordering': ('name',), + 'verbose_name': 'tag', + 'verbose_name_plural': 'tags', + }, + ), + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField( + verbose_name='object id', db_index=True)), + ('content_type', models.ForeignKey( + verbose_name='content type', + to='contenttypes.ContentType')), + ('tag', models.ForeignKey( + related_name='items', verbose_name='tag', + to='tagging.Tag')), + ], + options={ + 'verbose_name': 'tagged item', + 'verbose_name_plural': 'tagged items', + }, + ), + migrations.AlterUniqueTogether( + name='taggeditem', + unique_together=set([('tag', 'content_type', 'object_id')]), + ), + ] diff --git a/tagging/migrations/__init__.py b/tagging/migrations/__init__.py new file mode 100644 index 0000000..805bae9 --- /dev/null +++ b/tagging/migrations/__init__.py @@ -0,0 +1,3 @@ +""" +Migrations for tagging. +""" diff --git a/tagging/models.py b/tagging/models.py index 860cf81..9e89e43 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -1,29 +1,31 @@ """ -Models and managers for generic tagging. +Models and managers for tagging. """ -# Python 2.3 compatibility -try: - set -except NameError: - from sets import Set as set - -from django.contrib.contenttypes import generic -from django.contrib.contenttypes.models import ContentType -from django.db import connection, models -from django.db.models.query import QuerySet +from django.db import models +from django.db import connection +from django.utils.encoding import smart_text +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey from tagging import settings -from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input from tagging.utils import LOGARITHMIC +from tagging.utils import get_tag_list +from tagging.utils import calculate_cloud +from tagging.utils import parse_tag_input +from tagging.utils import get_queryset_and_model + qn = connection.ops.quote_name + ############ # Managers # ############ class TagManager(models.Manager): + def update_tags(self, obj, tag_names): """ Update tags associated with an object. @@ -36,12 +38,13 @@ class TagManager(models.Manager): updated_tag_names = [t.lower() for t in updated_tag_names] # Remove tags which no longer apply - tags_for_removal = [tag for tag in current_tags \ + tags_for_removal = [tag for tag in current_tags if tag.name not in updated_tag_names] if len(tags_for_removal): - TaggedItem._default_manager.filter(content_type__pk=ctype.pk, - object_id=obj.pk, - tag__in=tags_for_removal).delete() + TaggedItem._default_manager.filter( + content_type__pk=ctype.pk, + object_id=obj.pk, + tag__in=tags_for_removal).delete() # Add new tags current_tag_names = [tag.name for tag in current_tags] for tag_name in updated_tag_names: @@ -55,9 +58,11 @@ class TagManager(models.Manager): """ tag_names = parse_tag_input(tag_name) if not len(tag_names): - raise AttributeError(_('No tags were given: "%s".') % tag_name) + raise AttributeError( + _('No tags were given: "%s".') % tag_name) if len(tag_names) > 1: - raise AttributeError(_('Multiple tags were given: "%s".') % tag_name) + raise AttributeError( + _('Multiple tags were given: "%s".') % tag_name) tag_name = tag_names[0] if settings.FORCE_LOWERCASE_TAGS: tag_name = tag_name.lower() @@ -75,12 +80,14 @@ class TagManager(models.Manager): return self.filter(items__content_type__pk=ctype.pk, items__object_id=obj.pk) - def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None): + def _get_usage(self, model, counts=False, min_count=None, + extra_joins=None, extra_criteria=None, params=None): """ Perform the custom SQL query for ``usage_for_model`` and ``usage_for_queryset``. """ - if min_count is not None: counts = True + if min_count is not None: + counts = True model_table = qn(model._meta.db_table) model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column)) @@ -112,7 +119,8 @@ class TagManager(models.Manager): params.append(min_count) cursor = connection.cursor() - cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params) + cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), + params) tags = [] for row in cursor.fetchall(): t = self.model(*row[:2]) @@ -121,7 +129,8 @@ class TagManager(models.Manager): tags.append(t) return tags - def usage_for_model(self, model, counts=False, min_count=None, filters=None): + def usage_for_model(self, model, counts=False, min_count=None, + filters=None): """ Obtain a list of tags associated with instances of the given Model class. @@ -139,7 +148,8 @@ class TagManager(models.Manager): of field lookups to be applied to the given Model as the ``filters`` argument. """ - if filters is None: filters = {} + if filters is None: + filters = {} queryset = model._default_manager.filter() for f in filters.items(): @@ -161,24 +171,16 @@ class TagManager(models.Manager): greater than or equal to ``min_count`` will be returned. Passing a value for ``min_count`` implies ``counts=True``. """ - - if getattr(queryset.query, 'get_compiler', None): - # Django 1.2+ - compiler = queryset.query.get_compiler(using='default') - extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) - where, params = queryset.query.where.as_sql( - compiler.quote_name_unless_alias, compiler.connection - ) - else: - # Django pre-1.2 - extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:]) - where, params = queryset.query.where.as_sql() + compiler = queryset.query.get_compiler(using=queryset.db) + where, params = compiler.compile(queryset.query.where) + extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) if where: extra_criteria = 'AND %s' % where else: extra_criteria = '' - return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params) + return self._get_usage(queryset.model, counts, min_count, + extra_joins, extra_criteria, params) def related_for_model(self, tags, model, counts=False, min_count=None): """ @@ -193,13 +195,16 @@ class TagManager(models.Manager): greater than or equal to ``min_count`` will be returned. Passing a value for ``min_count`` implies ``counts=True``. """ - if min_count is not None: counts = True + if min_count is not None: + counts = True + tags = get_tag_list(tags) tag_count = len(tags) tagged_item_table = qn(TaggedItem._meta.db_table) query = """ SELECT %(tag)s.id, %(tag)s.name%(count_sql)s - FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id + FROM %(tagged_item)s INNER JOIN %(tag)s ON + %(tagged_item)s.tag_id = %(tag)s.id WHERE %(tagged_item)s.content_type_id = %(content_type_id)s AND %(tagged_item)s.object_id IN ( @@ -216,12 +221,14 @@ class TagManager(models.Manager): %(min_count_sql)s ORDER BY %(tag)s.name ASC""" % { 'tag': qn(self.model._meta.db_table), - 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '', + 'count_sql': counts and ', COUNT(%s.object_id)' % + tagged_item_table or '', 'tagged_item': tagged_item_table, 'content_type_id': ContentType.objects.get_for_model(model).pk, 'tag_id_placeholders': ','.join(['%s'] * tag_count), 'tag_count': tag_count, - 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '', + 'min_count_sql': min_count is not None and ( + 'HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '', } params = [tag.pk for tag in tags] * 2 @@ -267,6 +274,7 @@ class TagManager(models.Manager): min_count=min_count)) return calculate_cloud(tags, steps, distribution) + class TaggedItemManager(models.Manager): """ FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING`` @@ -280,6 +288,7 @@ class TaggedItemManager(models.Manager): Now that the queryset-refactor branch is in the trunk, this can be tidied up significantly. """ + def get_by_model(self, queryset_or_model, tags): """ Create a ``QuerySet`` containing instances of the specified @@ -405,7 +414,8 @@ class TaggedItemManager(models.Manager): related_content_type = ContentType.objects.get_for_model(model) query = """ SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s - FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item + FROM %(model)s, %(tagged_item)s, %(tag)s, + %(tagged_item)s related_tagged_item WHERE %(tagged_item)s.object_id = %%s AND %(tagged_item)s.content_type_id = %(content_type_id)s AND %(tag)s.id = %(tagged_item)s.tag_id @@ -441,23 +451,27 @@ class TaggedItemManager(models.Manager): cursor.execute(query, params) object_ids = [row[0] for row in cursor.fetchall()] if len(object_ids) > 0: - # Use in_bulk here instead of an id__in lookup, because id__in would - # clobber the ordering. + # Use in_bulk here instead of an id__in lookup, + # because id__in would clobber the ordering. object_dict = queryset.in_bulk(object_ids) - return [object_dict[object_id] for object_id in object_ids \ + return [object_dict[object_id] for object_id in object_ids if object_id in object_dict] else: return [] + ########## # Models # ########## +@python_2_unicode_compatible class Tag(models.Model): """ A tag. """ - name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) + name = models.CharField( + _('name'), max_length=settings.MAX_TAG_LENGTH, + unique=True, db_index=True) objects = TagManager() @@ -466,17 +480,30 @@ class Tag(models.Model): verbose_name = _('tag') verbose_name_plural = _('tags') - def __unicode__(self): + def __str__(self): return self.name + +@python_2_unicode_compatible class TaggedItem(models.Model): """ Holds the relationship between a tag and the item being tagged. """ - tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items') - content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) - object_id = models.PositiveIntegerField(_('object id'), db_index=True) - object = generic.GenericForeignKey('content_type', 'object_id') + tag = models.ForeignKey( + Tag, + verbose_name=_('tag'), + related_name='items') + + content_type = models.ForeignKey( + ContentType, + verbose_name=_('content type')) + + object_id = models.PositiveIntegerField( + _('object id'), + db_index=True) + + object = GenericForeignKey( + 'content_type', 'object_id') objects = TaggedItemManager() @@ -486,5 +513,5 @@ class TaggedItem(models.Model): verbose_name = _('tagged item') verbose_name_plural = _('tagged items') - def __unicode__(self): - return u'%s [%s]' % (self.object, self.tag) + def __str__(self): + return '%s [%s]' % (smart_text(self.object), smart_text(self.tag)) diff --git a/tagging/registry.py b/tagging/registry.py new file mode 100644 index 0000000..30c00ae --- /dev/null +++ b/tagging/registry.py @@ -0,0 +1,51 @@ +""" +Registery for tagging. +""" +from tagging.managers import TagDescriptor +from tagging.managers import ModelTaggedItemManager + +registry = [] + + +class AlreadyRegistered(Exception): + """ + An attempt was made to register a model more than once. + """ + pass + + +def register(model, tag_descriptor_attr='tags', + tagged_item_manager_attr='tagged'): + """ + Sets the given model class up for working with tags. + """ + if model in registry: + raise AlreadyRegistered( + "The model '%s' has already been registered." % + model._meta.object_name) + if hasattr(model, tag_descriptor_attr): + raise AttributeError( + "'%s' already has an attribute '%s'. You must " + "provide a custom tag_descriptor_attr to register." % ( + model._meta.object_name, + tag_descriptor_attr, + ) + ) + if hasattr(model, tagged_item_manager_attr): + raise AttributeError( + "'%s' already has an attribute '%s'. You must " + "provide a custom tagged_item_manager_attr to register." % ( + model._meta.object_name, + tagged_item_manager_attr, + ) + ) + + # Add tag descriptor + setattr(model, tag_descriptor_attr, TagDescriptor()) + + # Add custom manager + ModelTaggedItemManager().contribute_to_class( + model, tagged_item_manager_attr) + + # Finally register in registry + registry.append(model) diff --git a/tagging/settings.py b/tagging/settings.py index 1d6224c..558349c 100644 --- a/tagging/settings.py +++ b/tagging/settings.py @@ -8,6 +8,6 @@ from django.conf import settings # The maximum length of a tag's name. MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50) -# Whether to force all tags to lowercase before they are saved to the -# database. +# Whether to force all tags to lowercase +# before they are saved to the database. FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False) diff --git a/tagging/templatetags/__init__.py b/tagging/templatetags/__init__.py index e69de29..ab524e2 100644 --- a/tagging/templatetags/__init__.py +++ b/tagging/templatetags/__init__.py @@ -0,0 +1,3 @@ +""" +Templatetags module for tagging. +""" diff --git a/tagging/templatetags/tagging_tags.py b/tagging/templatetags/tagging_tags.py index 11d31cc..bd38b30 100644 --- a/tagging/templatetags/tagging_tags.py +++ b/tagging/templatetags/tagging_tags.py @@ -1,12 +1,22 @@ +""" +Templatetags for tagging. +""" +from django.template import Node +from django.template import Library +from django.template import Variable +from django.template import TemplateSyntaxError from django.db.models import get_model -from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable from django.utils.translation import ugettext as _ -from tagging.models import Tag, TaggedItem -from tagging.utils import LINEAR, LOGARITHMIC +from tagging.utils import LINEAR +from tagging.utils import LOGARITHMIC +from tagging.models import Tag +from tagging.models import TaggedItem + register = Library() + class TagsForModelNode(Node): def __init__(self, model, context_var, counts): self.model = model @@ -16,10 +26,14 @@ class TagsForModelNode(Node): def render(self, context): model = get_model(*self.model.split('.')) if model is None: - raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model) - context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts) + raise TemplateSyntaxError( + _('tags_for_model tag was given an invalid model: %s') % + self.model) + context[self.context_var] = Tag.objects.usage_for_model( + model, counts=self.counts) return '' + class TagCloudForModelNode(Node): def __init__(self, model, context_var, **kwargs): self.model = model @@ -29,11 +43,14 @@ class TagCloudForModelNode(Node): def render(self, context): model = get_model(*self.model.split('.')) if model is None: - raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model) - context[self.context_var] = \ - Tag.objects.cloud_for_model(model, **self.kwargs) + raise TemplateSyntaxError( + _('tag_cloud_for_model tag was given an invalid model: %s') % + self.model) + context[self.context_var] = Tag.objects.cloud_for_model( + model, **self.kwargs) return '' + class TagsForObjectNode(Node): def __init__(self, obj, context_var): self.obj = Variable(obj) @@ -44,6 +61,7 @@ class TagsForObjectNode(Node): Tag.objects.get_for_object(self.obj.resolve(context)) return '' + class TaggedObjectsNode(Node): def __init__(self, tag, model, context_var): self.tag = Variable(tag) @@ -53,11 +71,14 @@ class TaggedObjectsNode(Node): def render(self, context): model = get_model(*self.model.split('.')) if model is None: - raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model) - context[self.context_var] = \ - TaggedItem.objects.get_by_model(model, self.tag.resolve(context)) + raise TemplateSyntaxError( + _('tagged_objects tag was given an invalid model: %s') % + self.model) + context[self.context_var] = TaggedItem.objects.get_by_model( + model, self.tag.resolve(context)) return '' + def do_tags_for_model(parser, token): """ Retrieves a list of ``Tag`` objects associated with a given model @@ -86,19 +107,26 @@ def do_tags_for_model(parser, token): bits = token.contents.split() len_bits = len(bits) if len_bits not in (4, 6): - raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0]) + raise TemplateSyntaxError( + _('%s tag requires either three or five arguments') % bits[0]) if bits[2] != 'as': - raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + raise TemplateSyntaxError( + _("second argument to %s tag must be 'as'") % bits[0]) if len_bits == 6: if bits[4] != 'with': - raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0]) + raise TemplateSyntaxError( + _("if given, fourth argument to %s tag must be 'with'") % + bits[0]) if bits[5] != 'counts': - raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0]) + raise TemplateSyntaxError( + _("if given, fifth argument to %s tag must be 'counts'") % + bits[0]) if len_bits == 4: return TagsForModelNode(bits[1], bits[3], counts=False) else: return TagsForModelNode(bits[1], bits[3], counts=True) + def do_tag_cloud_for_model(parser, token): """ Retrieves a list of ``Tag`` objects for a given model, with tag @@ -132,19 +160,25 @@ def do_tag_cloud_for_model(parser, token): Examples:: {% tag_cloud_for_model products.Widget as widget_tags %} - {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %} + {% tag_cloud_for_model products.Widget as widget_tags + with steps=9 min_count=3 distribution=log %} """ bits = token.contents.split() len_bits = len(bits) if len_bits != 4 and len_bits not in range(6, 9): - raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0]) + raise TemplateSyntaxError( + _('%s tag requires either three or between five ' + 'and seven arguments') % bits[0]) if bits[2] != 'as': - raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + raise TemplateSyntaxError( + _("second argument to %s tag must be 'as'") % bits[0]) kwargs = {} if len_bits > 5: if bits[4] != 'with': - raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0]) + raise TemplateSyntaxError( + _("if given, fourth argument to %s tag must be 'with'") % + bits[0]) for i in range(5, len_bits): try: name, value = bits[i].split('=') @@ -152,32 +186,42 @@ def do_tag_cloud_for_model(parser, token): try: kwargs[str(name)] = int(value) except ValueError: - raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % { - 'tag': bits[0], - 'option': name, - 'value': value, - }) + raise TemplateSyntaxError( + _("%(tag)s tag's '%(option)s' option was not " + "a valid integer: '%(value)s'") % { + 'tag': bits[0], + 'option': name, + 'value': value, + }) elif name == 'distribution': if value in ['linear', 'log']: - kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value] + kwargs[str(name)] = {'linear': LINEAR, + 'log': LOGARITHMIC}[value] else: - raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % { + raise TemplateSyntaxError( + _("%(tag)s tag's '%(option)s' option was not " + "a valid choice: '%(value)s'") % { + 'tag': bits[0], + 'option': name, + 'value': value, + }) + else: + raise TemplateSyntaxError( + _("%(tag)s tag was given an " + "invalid option: '%(option)s'") % { 'tag': bits[0], 'option': name, - 'value': value, }) - else: - raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % { + except ValueError: + raise TemplateSyntaxError( + _("%(tag)s tag was given a badly " + "formatted option: '%(option)s'") % { 'tag': bits[0], - 'option': name, + 'option': bits[i], }) - except ValueError: - raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % { - 'tag': bits[0], - 'option': bits[i], - }) return TagCloudForModelNode(bits[1], bits[3], **kwargs) + def do_tags_for_object(parser, token): """ Retrieves a list of ``Tag`` objects associated with an object and @@ -193,11 +237,14 @@ def do_tags_for_object(parser, token): """ bits = token.contents.split() if len(bits) != 4: - raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0]) + raise TemplateSyntaxError( + _('%s tag requires exactly three arguments') % bits[0]) if bits[2] != 'as': - raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + raise TemplateSyntaxError( + _("second argument to %s tag must be 'as'") % bits[0]) return TagsForObjectNode(bits[1], bits[3]) + def do_tagged_objects(parser, token): """ Retrieves a list of instances of a given model which are tagged with @@ -218,13 +265,17 @@ def do_tagged_objects(parser, token): """ bits = token.contents.split() if len(bits) != 6: - raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0]) + raise TemplateSyntaxError( + _('%s tag requires exactly five arguments') % bits[0]) if bits[2] != 'in': - raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0]) + raise TemplateSyntaxError( + _("second argument to %s tag must be 'in'") % bits[0]) if bits[4] != 'as': - raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0]) + raise TemplateSyntaxError( + _("fourth argument to %s tag must be 'as'") % bits[0]) return TaggedObjectsNode(bits[1], bits[3], bits[5]) + register.tag('tags_for_model', do_tags_for_model) register.tag('tag_cloud_for_model', do_tag_cloud_for_model) register.tag('tags_for_object', do_tags_for_object) diff --git a/tagging/tests/._settings.py b/tagging/tests/._settings.py deleted file mode 100644 index 6092bc4..0000000 Binary files a/tagging/tests/._settings.py and /dev/null differ diff --git a/tagging/tests/._tags.txt b/tagging/tests/._tags.txt deleted file mode 100644 index 3348eaa..0000000 Binary files a/tagging/tests/._tags.txt and /dev/null differ diff --git a/tagging/tests/__init__.py b/tagging/tests/__init__.py index e69de29..f020378 100644 --- a/tagging/tests/__init__.py +++ b/tagging/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for tagging. +""" diff --git a/tagging/tests/models.py b/tagging/tests/models.py index 0708686..1babd58 100644 --- a/tagging/tests/models.py +++ b/tagging/tests/models.py @@ -1,38 +1,51 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible from tagging.fields import TagField + class Perch(models.Model): size = models.IntegerField() smelly = models.BooleanField(default=True) + +@python_2_unicode_compatible class Parrot(models.Model): state = models.CharField(max_length=50) perch = models.ForeignKey(Perch, null=True) - def __unicode__(self): + def __str__(self): return self.state class Meta: ordering = ['state'] + +@python_2_unicode_compatible class Link(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name class Meta: ordering = ['name'] + +@python_2_unicode_compatible class Article(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name class Meta: ordering = ['name'] + class FormTest(models.Model): tags = TagField('Test', help_text='Test') + + +class FormTestNull(models.Model): + tags = TagField(null=True) diff --git a/tagging/tests/settings.py b/tagging/tests/settings.py index 74eb909..3648f2e 100644 --- a/tagging/tests/settings.py +++ b/tagging/tests/settings.py @@ -1,27 +1,39 @@ +"""Tests settings""" import os -DIRNAME = os.path.dirname(__file__) -DEFAULT_CHARSET = 'utf-8' +SECRET_KEY = 'secret-key' -test_engine = os.environ.get("TAGGING_TEST_ENGINE", "sqlite3") +DATABASES = { + 'default': { + 'NAME': 'tagging.db', + 'ENGINE': 'django.db.backends.sqlite3' + } +} -DATABASE_ENGINE = test_engine -DATABASE_NAME = os.environ.get("TAGGING_DATABASE_NAME", "tagging_test") -DATABASE_USER = os.environ.get("TAGGING_DATABASE_USER", "") -DATABASE_PASSWORD = os.environ.get("TAGGING_DATABASE_PASSWORD", "") -DATABASE_HOST = os.environ.get("TAGGING_DATABASE_HOST", "localhost") +DATABASE_ENGINE = os.environ.get('DATABASE_ENGINE') +if DATABASE_ENGINE == 'postgres': + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'tagging', + 'USER': 'postgres', + 'HOST': 'localhost' + } + } +elif DATABASE_ENGINE == 'mysql': + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'zinnia', + 'USER': 'root', + 'HOST': 'localhost' + } + } -if test_engine == "sqlite": - DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db') - DATABASE_HOST = "" -elif test_engine == "mysql": - DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 3306) -elif test_engine == "postgresql_psycopg2": - DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 5432) - - -INSTALLED_APPS = ( +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.sessions', 'django.contrib.contenttypes', 'tagging', 'tagging.tests', -) +] diff --git a/tagging/tests/tests.py b/tagging/tests/tests.py index f5c9e37..c3ff3b6 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -1,434 +1,509 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import os + from django import forms +from django.utils import six from django.db.models import Q from django.test import TestCase -from tagging.forms import TagField +from django.test.utils import override_settings +from django.core.exceptions import ImproperlyConfigured + from tagging import settings -from tagging.models import Tag, TaggedItem -from tagging.tests.models import Article, Link, Perch, Parrot, FormTest -from tagging.utils import calculate_cloud, edit_string_for_tags, get_tag_list, get_tag, parse_tag_input +from tagging.forms import TagField +from tagging.forms import TagAdminForm +from tagging.models import Tag +from tagging.models import TaggedItem +from tagging.tests.models import Article +from tagging.tests.models import Link +from tagging.tests.models import Perch +from tagging.tests.models import Parrot +from tagging.tests.models import FormTest +from tagging.tests.models import FormTestNull from tagging.utils import LINEAR +from tagging.utils import get_tag +from tagging.utils import get_tag_list +from tagging.utils import calculate_cloud +from tagging.utils import parse_tag_input +from tagging.utils import edit_string_for_tags ############# # Utilities # ############# + class TestParseTagInput(TestCase): def test_with_simple_space_delimited_tags(self): """ Test with simple space-delimited tags. """ - - self.assertEquals(parse_tag_input('one'), [u'one']) - self.assertEquals(parse_tag_input('one two'), [u'one', u'two']) - self.assertEquals(parse_tag_input('one two three'), [u'one', u'three', u'two']) - self.assertEquals(parse_tag_input('one one two two'), [u'one', u'two']) - + + self.assertEqual(parse_tag_input('one'), ['one']) + self.assertEqual(parse_tag_input('one two'), ['one', 'two']) + self.assertEqual(parse_tag_input('one one two two'), ['one', 'two']) + self.assertEqual(parse_tag_input('one two three'), + ['one', 'three', 'two']) + def test_with_comma_delimited_multiple_words(self): """ Test with comma-delimited multiple words. An unquoted comma in the input will trigger this. """ - - self.assertEquals(parse_tag_input(',one'), [u'one']) - self.assertEquals(parse_tag_input(',one two'), [u'one two']) - self.assertEquals(parse_tag_input(',one two three'), [u'one two three']) - self.assertEquals(parse_tag_input('a-one, a-two and a-three'), - [u'a-one', u'a-two and a-three']) - + + self.assertEqual(parse_tag_input(',one'), ['one']) + self.assertEqual(parse_tag_input(',one two'), ['one two']) + self.assertEqual(parse_tag_input(',one two three'), ['one two three']) + self.assertEqual(parse_tag_input('a-one, a-two and a-three'), + ['a-one', 'a-two and a-three']) + def test_with_double_quoted_multiple_words(self): """ Test with double-quoted multiple words. - A completed quote will trigger this. Unclosed quotes are ignored. """ - - self.assertEquals(parse_tag_input('"one'), [u'one']) - self.assertEquals(parse_tag_input('"one two'), [u'one', u'two']) - self.assertEquals(parse_tag_input('"one two three'), [u'one', u'three', u'two']) - self.assertEquals(parse_tag_input('"one two"'), [u'one two']) - self.assertEquals(parse_tag_input('a-one "a-two and a-three"'), - [u'a-one', u'a-two and a-three']) - + A completed quote will trigger this. Unclosed quotes are ignored. + """ + + self.assertEqual(parse_tag_input('"one'), ['one']) + self.assertEqual(parse_tag_input('"one two'), ['one', 'two']) + self.assertEqual(parse_tag_input('"one two three'), + ['one', 'three', 'two']) + self.assertEqual(parse_tag_input('"one two"'), ['one two']) + self.assertEqual(parse_tag_input('a-one "a-two and a-three"'), + ['a-one', 'a-two and a-three']) + def test_with_no_loose_commas(self): """ Test with no loose commas -- split on spaces. """ - self.assertEquals(parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) - + self.assertEqual(parse_tag_input('one two "thr,ee"'), + ['one', 'thr,ee', 'two']) + def test_with_loose_commas(self): """ Loose commas - split on commas """ - self.assertEquals(parse_tag_input('"one", two three'), [u'one', u'two three']) - + self.assertEqual(parse_tag_input('"one", two three'), + ['one', 'two three']) + def test_tags_with_double_quotes_can_contain_commas(self): """ Double quotes can contain commas """ - self.assertEquals(parse_tag_input('a-one "a-two, and a-three"'), - [u'a-one', u'a-two, and a-three']) - self.assertEquals(parse_tag_input('"two", one, one, two, "one"'), - [u'one', u'two']) - + self.assertEqual(parse_tag_input('a-one "a-two, and a-three"'), + ['a-one', 'a-two, and a-three']) + self.assertEqual(parse_tag_input('"two", one, one, two, "one"'), + ['one', 'two']) + self.assertEqual(parse_tag_input('two", one'), + ['one', 'two']) + def test_with_naughty_input(self): """ Test with naughty input. """ - # Bad users! Naughty users! - self.assertEquals(parse_tag_input(None), []) - self.assertEquals(parse_tag_input(''), []) - self.assertEquals(parse_tag_input('"'), []) - self.assertEquals(parse_tag_input('""'), []) - self.assertEquals(parse_tag_input('"' * 7), []) - self.assertEquals(parse_tag_input(',,,,,,'), []) - self.assertEquals(parse_tag_input('",",",",",",","'), [u',']) - self.assertEquals(parse_tag_input('a-one "a-two" and "a-three'), - [u'a-one', u'a-three', u'a-two', u'and']) - + self.assertEqual(parse_tag_input(None), []) + self.assertEqual(parse_tag_input(''), []) + self.assertEqual(parse_tag_input('"'), []) + self.assertEqual(parse_tag_input('""'), []) + self.assertEqual(parse_tag_input('"' * 7), []) + self.assertEqual(parse_tag_input(',,,,,,'), []) + self.assertEqual(parse_tag_input('",",",",",",","'), [',']) + self.assertEqual(parse_tag_input('a-one "a-two" and "a-three'), + ['a-one', 'a-three', 'a-two', 'and']) + + class TestNormalisedTagListInput(TestCase): def setUp(self): - self.cheese = Tag.objects.create(name='cheese') self.toast = Tag.objects.create(name='toast') - + self.cheese = Tag.objects.create(name='cheese') + def test_single_tag_object_as_input(self): - self.assertEquals(get_tag_list(self.cheese), [self.cheese]) - + self.assertEqual(get_tag_list(self.cheese), [self.cheese]) + def test_space_delimeted_string_as_input(self): ret = get_tag_list('cheese toast') - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_comma_delimeted_string_as_input(self): ret = get_tag_list('cheese,toast') - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_with_empty_list(self): - self.assertEquals(get_tag_list([]), []) - + self.assertEqual(get_tag_list([]), []) + def test_list_of_two_strings(self): ret = get_tag_list(['cheese', 'toast']) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_list_of_tag_primary_keys(self): ret = get_tag_list([self.cheese.id, self.toast.id]) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_list_of_strings_with_strange_nontag_string(self): ret = get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_list_of_tag_instances(self): ret = get_tag_list([self.cheese, self.toast]) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_tuple_of_instances(self): ret = get_tag_list((self.cheese, self.toast)) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_with_tag_filter(self): ret = get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast'])) - self.assertEquals(len(ret), 2) - self.failUnless(self.cheese in ret) - self.failUnless(self.toast in ret) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_with_invalid_input_mix_of_string_and_instance(self): try: get_tag_list(['cheese', self.toast]) - except ValueError, ve: - self.assertEquals(str(ve), - 'If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.') - except Exception, e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%]' %\ + except ValueError as ve: + self.assertEqual( + str(ve), + 'If a list or tuple of tags is provided, they must all ' + 'be tag names, Tag objects or Tag ids.') + except Exception as e: + raise self.failureException( + 'the wrong type of exception was raised: type [%s] value [%]' % (str(type(e)), str(e))) else: - raise self.failureException('a ValueError exception was supposed to be raised!') - + raise self.failureException( + 'a ValueError exception was supposed to be raised!') + def test_with_invalid_input(self): try: get_tag_list(29) - except ValueError, ve: - self.assertEquals(str(ve), 'The tag input given was invalid.') - except Exception, e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ - (str(type(e)), str(e))) + except ValueError as ve: + self.assertEqual(str(ve), 'The tag input given was invalid.') + except Exception as e: + print('--', e) + raise self.failureException( + 'the wrong type of exception was raised: ' + 'type [%s] value [%s]' % (str(type(e)), str(e))) else: - raise self.failureException('a ValueError exception was supposed to be raised!') + raise self.failureException( + 'a ValueError exception was supposed to be raised!') def test_with_tag_instance(self): - self.assertEquals(get_tag(self.cheese), self.cheese) - + self.assertEqual(get_tag(self.cheese), self.cheese) + def test_with_string(self): - self.assertEquals(get_tag('cheese'), self.cheese) - + self.assertEqual(get_tag('cheese'), self.cheese) + def test_with_primary_key(self): - self.assertEquals(get_tag(self.cheese.id), self.cheese) - + self.assertEqual(get_tag(self.cheese.id), self.cheese) + def test_nonexistent_tag(self): - self.assertEquals(get_tag('mouse'), None) + self.assertEqual(get_tag('mouse'), None) + class TestCalculateCloud(TestCase): def setUp(self): self.tags = [] - for line in open(os.path.join(os.path.dirname(__file__), 'tags.txt')).readlines(): + for line in open(os.path.join(os.path.dirname(__file__), + 'tags.txt')).readlines(): name, count = line.rstrip().split() tag = Tag(name=name) tag.count = int(count) self.tags.append(tag) - + def test_default_distribution(self): sizes = {} for tag in calculate_cloud(self.tags, steps=5): sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1 - + # This isn't a pre-calculated test, just making sure it's consistent - self.assertEquals(sizes[1], 48) - self.assertEquals(sizes[2], 30) - self.assertEquals(sizes[3], 19) - self.assertEquals(sizes[4], 15) - self.assertEquals(sizes[5], 10) - + self.assertEqual(sizes[1], 48) + self.assertEqual(sizes[2], 30) + self.assertEqual(sizes[3], 19) + self.assertEqual(sizes[4], 15) + self.assertEqual(sizes[5], 10) + def test_linear_distribution(self): sizes = {} for tag in calculate_cloud(self.tags, steps=5, distribution=LINEAR): sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1 - + # This isn't a pre-calculated test, just making sure it's consistent - self.assertEquals(sizes[1], 97) - self.assertEquals(sizes[2], 12) - self.assertEquals(sizes[3], 7) - self.assertEquals(sizes[4], 2) - self.assertEquals(sizes[5], 4) - + self.assertEqual(sizes[1], 97) + self.assertEqual(sizes[2], 12) + self.assertEqual(sizes[3], 7) + self.assertEqual(sizes[4], 2) + self.assertEqual(sizes[5], 4) + def test_invalid_distribution(self): try: calculate_cloud(self.tags, steps=5, distribution='cheese') - except ValueError, ve: - self.assertEquals(str(ve), 'Invalid distribution algorithm specified: cheese.') - except Exception, e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ - (str(type(e)), str(e))) + except ValueError as ve: + self.assertEqual( + str(ve), 'Invalid distribution algorithm specified: cheese.') + except Exception as e: + raise self.failureException( + 'the wrong type of exception was raised: ' + 'type [%s] value [%s]' % (str(type(e)), str(e))) else: - raise self.failureException('a ValueError exception was supposed to be raised!') - + raise self.failureException( + 'a ValueError exception was supposed to be raised!') + ########### # Tagging # ########### + class TestBasicTagging(TestCase): def setUp(self): self.dead_parrot = Parrot.objects.create(state='dead') - + def test_update_tags(self): Tag.objects.update_tags(self.dead_parrot, 'foo,bar,"ter"') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('foo') in tags) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('ter') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('foo') in tags) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('ter') in tags) + Tag.objects.update_tags(self.dead_parrot, '"foo" bar "baz"') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + def test_add_tag(self): # start off in a known, mildly interesting state Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + # try to add a tag that already exists Tag.objects.add_tag(self.dead_parrot, 'foo') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + # now add a tag that doesn't already exist Tag.objects.add_tag(self.dead_parrot, 'zip') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 4) - self.failUnless(get_tag('zip') in tags) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 4) + self.assertTrue(get_tag('zip') in tags) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + def test_add_tag_invalid_input_no_tags_specified(self): # start off in a known, mildly interesting state Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + try: Tag.objects.add_tag(self.dead_parrot, ' ') - except AttributeError, ae: - self.assertEquals(str(ae), 'No tags were given: " ".') - except Exception, e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ - (str(type(e)), str(e))) + except AttributeError as ae: + self.assertEqual(str(ae), 'No tags were given: " ".') + except Exception as e: + raise self.failureException( + 'the wrong type of exception was raised: ' + 'type [%s] value [%s]' % (str(type(e)), str(e))) else: - raise self.failureException('an AttributeError exception was supposed to be raised!') - + raise self.failureException( + 'an AttributeError exception was supposed to be raised!') + def test_add_tag_invalid_input_multiple_tags_specified(self): # start off in a known, mildly interesting state Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + try: Tag.objects.add_tag(self.dead_parrot, 'one two') - except AttributeError, ae: - self.assertEquals(str(ae), 'Multiple tags were given: "one two".') - except Exception, e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ - (str(type(e)), str(e))) + except AttributeError as ae: + self.assertEqual(str(ae), 'Multiple tags were given: "one two".') + except Exception as e: + raise self.failureException( + 'the wrong type of exception was raised: ' + 'type [%s] value [%s]' % (str(type(e)), str(e))) else: - raise self.failureException('an AttributeError exception was supposed to be raised!') - + raise self.failureException( + 'an AttributeError exception was supposed to be raised!') + def test_update_tags_exotic_characters(self): # start off in a known, mildly interesting state Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - - Tag.objects.update_tags(self.dead_parrot, u'ŠĐĆŽćžšđ') + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, 'ŠĐĆŽćžšđ') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 1) - self.assertEquals(tags[0].name, u'ŠĐĆŽćžšđ') - - Tag.objects.update_tags(self.dead_parrot, u'你好') + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0].name, 'ŠĐĆŽćžšđ') + + Tag.objects.update_tags(self.dead_parrot, '你好') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 1) - self.assertEquals(tags[0].name, u'你好') - + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0].name, '你好') + + def test_unicode_tagged_object(self): + self.dead_parrot.state = "dëad" + self.dead_parrot.save() + Tag.objects.update_tags(self.dead_parrot, 'föo') + items = TaggedItem.objects.all() + self.assertEqual(len(items), 1) + self.assertEqual(six.text_type(items[0]), "dëad [föo]") + def test_update_tags_with_none(self): # start off in a known, mildly interesting state Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(get_tag('bar') in tags) - self.failUnless(get_tag('baz') in tags) - self.failUnless(get_tag('foo') in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + Tag.objects.update_tags(self.dead_parrot, None) tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 0) + self.assertEqual(len(tags), 0) + class TestModelTagField(TestCase): """ Test the 'tags' field on models. """ - + def test_create_with_tags_specified(self): - f1 = FormTest.objects.create(tags=u'test3 test2 test1') + f1 = FormTest.objects.create(tags='test3 test2 test1') tags = Tag.objects.get_for_object(f1) test1_tag = get_tag('test1') test2_tag = get_tag('test2') test3_tag = get_tag('test3') - self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) - self.assertEquals(len(tags), 3) - self.failUnless(test1_tag in tags) - self.failUnless(test2_tag in tags) - self.failUnless(test3_tag in tags) - + self.assertTrue(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEqual(len(tags), 3) + self.assertTrue(test1_tag in tags) + self.assertTrue(test2_tag in tags) + self.assertTrue(test3_tag in tags) + def test_update_via_tags_field(self): - f1 = FormTest.objects.create(tags=u'test3 test2 test1') + f1 = FormTest.objects.create(tags='test3 test2 test1') tags = Tag.objects.get_for_object(f1) test1_tag = get_tag('test1') test2_tag = get_tag('test2') test3_tag = get_tag('test3') - self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) - self.assertEquals(len(tags), 3) - self.failUnless(test1_tag in tags) - self.failUnless(test2_tag in tags) - self.failUnless(test3_tag in tags) - - f1.tags = u'test4' + self.assertTrue(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEqual(len(tags), 3) + self.assertTrue(test1_tag in tags) + self.assertTrue(test2_tag in tags) + self.assertTrue(test3_tag in tags) + + f1.tags = 'test4' f1.save() tags = Tag.objects.get_for_object(f1) test4_tag = get_tag('test4') - self.assertEquals(len(tags), 1) - self.assertEquals(tags[0], test4_tag) - + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0], test4_tag) + f1.tags = '' f1.save() tags = Tag.objects.get_for_object(f1) - self.assertEquals(len(tags), 0) - + self.assertEqual(len(tags), 0) + + def disabledtest_update_via_tags(self): + # TODO: make this test working by reverting + # https://github.com/Fantomas42/django-tagging/commit/bbc7f25ea471dd903f39e08684d84ce59081bdef + f1 = FormTest.objects.create(tags='one two three') + Tag.objects.get(name='three').delete() + t2 = Tag.objects.get(name='two') + t2.name = 'new' + t2.save() + f1again = FormTest.objects.get(pk=f1.pk) + self.assertFalse('three' in f1again.tags) + self.assertFalse('two' in f1again.tags) + self.assertTrue('new' in f1again.tags) + + def test_creation_without_specifying_tags(self): + f1 = FormTest() + self.assertEqual(f1.tags, '') + + def test_creation_with_nullable_tags_field(self): + f1 = FormTestNull() + self.assertEqual(f1.tags, '') + + class TestSettings(TestCase): def setUp(self): self.original_force_lower_case_tags = settings.FORCE_LOWERCASE_TAGS self.dead_parrot = Parrot.objects.create(state='dead') - + def tearDown(self): settings.FORCE_LOWERCASE_TAGS = self.original_force_lower_case_tags - + def test_force_lowercase_tags(self): """ Test forcing tags to lowercase. """ - + settings.FORCE_LOWERCASE_TAGS = True - + Tag.objects.update_tags(self.dead_parrot, 'foO bAr Ter') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) + self.assertEqual(len(tags), 3) foo_tag = get_tag('foo') bar_tag = get_tag('bar') ter_tag = get_tag('ter') - self.failUnless(foo_tag in tags) - self.failUnless(bar_tag in tags) - self.failUnless(ter_tag in tags) - + self.assertTrue(foo_tag in tags) + self.assertTrue(bar_tag in tags) + self.assertTrue(ter_tag in tags) + Tag.objects.update_tags(self.dead_parrot, 'foO bAr baZ') tags = Tag.objects.get_for_object(self.dead_parrot) baz_tag = get_tag('baz') - self.assertEquals(len(tags), 3) - self.failUnless(bar_tag in tags) - self.failUnless(baz_tag in tags) - self.failUnless(foo_tag in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(foo_tag in tags) + Tag.objects.add_tag(self.dead_parrot, 'FOO') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 3) - self.failUnless(bar_tag in tags) - self.failUnless(baz_tag in tags) - self.failUnless(foo_tag in tags) - + self.assertEqual(len(tags), 3) + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(foo_tag in tags) + Tag.objects.add_tag(self.dead_parrot, 'Zip') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 4) + self.assertEqual(len(tags), 4) zip_tag = get_tag('zip') - self.failUnless(bar_tag in tags) - self.failUnless(baz_tag in tags) - self.failUnless(foo_tag in tags) - self.failUnless(zip_tag in tags) - + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(foo_tag in tags) + self.assertTrue(zip_tag in tags) + f1 = FormTest.objects.create() - f1.tags = u'TEST5' + f1.tags = 'TEST5' f1.save() tags = Tag.objects.get_for_object(f1) test5_tag = get_tag('test5') - self.assertEquals(len(tags), 1) - self.failUnless(test5_tag in tags) - self.assertEquals(f1.tags, u'test5') + self.assertEqual(len(tags), 1) + self.assertTrue(test5_tag in tags) + self.assertEqual(f1.tags, 'test5') + class TestTagUsageForModelBaseCase(TestCase): def test_tag_usage_for_model_empty(self): - self.assertEquals(Tag.objects.usage_for_model(Parrot), []) + self.assertEqual(Tag.objects.usage_for_model(Parrot), []) + class TestTagUsageForModel(TestCase): def setUp(self): @@ -438,75 +513,85 @@ class TestTagUsageForModel(TestCase): ('no more', 4, True, 'foo ter'), ('late', 2, False, 'bar ter'), ) - + for state, perch_size, perch_smelly, tags in parrot_details: perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) parrot = Parrot.objects.create(state=state, perch=perch) Tag.objects.update_tags(parrot, tags) - + def test_tag_usage_for_model(self): tag_usage = Tag.objects.usage_for_model(Parrot, counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', 3) in relevant_attribute_list) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 2) in relevant_attribute_list) - self.failUnless((u'ter', 3) in relevant_attribute_list) - + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 3) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('ter', 3) in relevant_attribute_list) + def test_tag_usage_for_model_with_min_count(self): - tag_usage = Tag.objects.usage_for_model(Parrot, min_count = 2) + tag_usage = Tag.objects.usage_for_model(Parrot, min_count=2) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', 3) in relevant_attribute_list) - self.failUnless((u'foo', 2) in relevant_attribute_list) - self.failUnless((u'ter', 3) in relevant_attribute_list) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 3) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('ter', 3) in relevant_attribute_list) + def test_tag_usage_with_filter_on_model_objects(self): - tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more')) + tag_usage = Tag.objects.usage_for_model( + Parrot, counts=True, filters=dict(state='no more')) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 2) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p')) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, counts=True, filters=dict(state__startswith='p')) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4)) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, counts=True, filters=dict(perch__size__gt=4)) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True)) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, counts=True, filters=dict(perch__smelly=True)) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', 1) in relevant_attribute_list) - self.failUnless((u'foo', 2) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True)) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, min_count=2, filters=dict(perch__smelly=True)) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'foo', 2) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4)) - relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', False) in relevant_attribute_list) - self.failUnless((u'baz', False) in relevant_attribute_list) - self.failUnless((u'foo', False) in relevant_attribute_list) - self.failUnless((u'ter', False) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99)) - relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 0) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, filters=dict(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) + for tag in tag_usage] + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('baz', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model( + Parrot, filters=dict(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) + for tag in tag_usage] + self.assertEqual(len(relevant_attribute_list), 0) + class TestTagsRelatedForModel(TestCase): def setUp(self): @@ -516,71 +601,134 @@ class TestTagsRelatedForModel(TestCase): ('no more', 4, True, 'foo ter'), ('late', 2, False, 'bar ter'), ) - + for state, perch_size, perch_smelly, tags in parrot_details: perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) parrot = Parrot.objects.create(state=state, perch=perch) Tag.objects.update_tags(parrot, tags) - + def test_related_for_model_with_tag_query_sets_as_input(self): - related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False) + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar']), Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar']), Parrot, counts=False) relevant_attribute_list = [tag.name for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless(u'baz' in relevant_attribute_list) - self.failUnless(u'foo' in relevant_attribute_list) - self.failUnless(u'ter' in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'baz', 1) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 0) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue('baz' in relevant_attribute_list) + self.assertTrue('foo' in relevant_attribute_list) + self.assertTrue('ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar', 'ter', 'baz']), + Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 0) + def test_related_for_model_with_tag_strings_as_input(self): # Once again, with feeling (strings) - related_tags = Tag.objects.related_for_model('bar', Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model('bar', Parrot, min_count=2) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model('bar', Parrot, counts=False) + related_tags = Tag.objects.related_for_model( + 'bar', Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + 'bar', Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + 'bar', Parrot, counts=False) relevant_attribute_list = [tag.name for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless(u'baz' in relevant_attribute_list) - self.failUnless(u'foo' in relevant_attribute_list) - self.failUnless(u'ter' in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'baz', 1) in relevant_attribute_list) - - related_tags = Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] - self.assertEquals(len(relevant_attribute_list), 0) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue('baz' in relevant_attribute_list) + self.assertTrue('foo' in relevant_attribute_list) + self.assertTrue('ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + ['bar', 'ter'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model( + ['bar', 'ter', 'baz'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 0) + + +class TestTagCloudForModel(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + def test_tag_cloud_for_model(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 3, 4) in relevant_attribute_list) + self.assertTrue(('baz', 1, 1) in relevant_attribute_list) + self.assertTrue(('foo', 2, 2) in relevant_attribute_list) + self.assertTrue(('ter', 3, 4) in relevant_attribute_list) + + def test_tag_cloud_for_model_filters(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot, + filters={'state': 'no more'}) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1, 1) in relevant_attribute_list) + self.assertTrue(('ter', 1, 1) in relevant_attribute_list) + + def test_tag_cloud_for_model_min_count(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 3, 4) in relevant_attribute_list) + self.assertTrue(('foo', 2, 1) in relevant_attribute_list) + self.assertTrue(('ter', 3, 4) in relevant_attribute_list) + + class TestGetTaggedObjectsByModel(TestCase): def setUp(self): parrot_details = ( @@ -589,112 +737,123 @@ class TestGetTaggedObjectsByModel(TestCase): ('no more', 4, True, 'foo ter'), ('late', 2, False, 'bar ter'), ) - + for state, perch_size, perch_smelly, tags in parrot_details: perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) parrot = Parrot.objects.create(state=state, perch=perch) Tag.objects.update_tags(parrot, tags) - + self.foo = Tag.objects.get(name='foo') self.bar = Tag.objects.get(name='bar') self.baz = Tag.objects.get(name='baz') self.ter = Tag.objects.get(name='ter') - - self.pining_for_the_fjords_parrot = Parrot.objects.get(state='pining for the fjords') + + self.pining_for_the_fjords_parrot = Parrot.objects.get( + state='pining for the fjords') self.passed_on_parrot = Parrot.objects.get(state='passed on') self.no_more_parrot = Parrot.objects.get(state='no more') self.late_parrot = Parrot.objects.get(state='late') - + def test_get_by_model_simple(self): parrots = TaggedItem.objects.get_by_model(Parrot, self.foo) - self.assertEquals(len(parrots), 2) - self.failUnless(self.no_more_parrot in parrots) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.no_more_parrot in parrots) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + parrots = TaggedItem.objects.get_by_model(Parrot, self.bar) - self.assertEquals(len(parrots), 3) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 3) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + def test_get_by_model_intersection(self): parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.baz]) - self.assertEquals(len(parrots), 0) - + self.assertEqual(len(parrots), 0) + parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.bar]) - self.assertEquals(len(parrots), 1) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 1) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + parrots = TaggedItem.objects.get_by_model(Parrot, [self.bar, self.ter]) - self.assertEquals(len(parrots), 2) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + # Issue 114 - Intersection with non-existant tags parrots = TaggedItem.objects.get_intersection_by_model(Parrot, []) - self.assertEquals(len(parrots), 0) - + self.assertEqual(len(parrots), 0) + def test_get_by_model_with_tag_querysets_as_input(self): - parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) - self.assertEquals(len(parrots), 0) - - parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar'])) - self.assertEquals(len(parrots), 1) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - - parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) - self.assertEquals(len(parrots), 2) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) + self.assertEqual(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['foo', 'bar'])) + self.assertEqual(len(parrots), 1) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + def test_get_by_model_with_strings_as_input(self): parrots = TaggedItem.objects.get_by_model(Parrot, 'foo baz') - self.assertEquals(len(parrots), 0) - + self.assertEqual(len(parrots), 0) + parrots = TaggedItem.objects.get_by_model(Parrot, 'foo bar') - self.assertEquals(len(parrots), 1) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 1) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + parrots = TaggedItem.objects.get_by_model(Parrot, 'bar ter') - self.assertEquals(len(parrots), 2) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + def test_get_by_model_with_lists_of_strings_as_input(self): parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) - self.assertEquals(len(parrots), 0) - + self.assertEqual(len(parrots), 0) + parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) - self.assertEquals(len(parrots), 1) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 1) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + parrots = TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) - self.assertEquals(len(parrots), 2) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + def test_get_by_nonexistent_tag(self): # Issue 50 - Get by non-existent tag parrots = TaggedItem.objects.get_by_model(Parrot, 'argatrons') - self.assertEquals(len(parrots), 0) - + self.assertEqual(len(parrots), 0) + def test_get_union_by_model(self): parrots = TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) - self.assertEquals(len(parrots), 4) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.no_more_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 4) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.no_more_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) - self.assertEquals(len(parrots), 3) - self.failUnless(self.late_parrot in parrots) - self.failUnless(self.passed_on_parrot in parrots) - self.failUnless(self.pining_for_the_fjords_parrot in parrots) - + self.assertEqual(len(parrots), 3) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + # Issue 114 - Union with non-existant tags parrots = TaggedItem.objects.get_union_by_model(Parrot, []) - self.assertEquals(len(parrots), 0) + self.assertEqual(len(parrots), 0) + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['albert']) + self.assertEqual(len(parrots), 0) + + Tag.objects.create(name='titi') + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['titi']) + self.assertEqual(len(parrots), 0) + class TestGetRelatedTaggedItems(TestCase): def setUp(self): @@ -704,12 +863,12 @@ class TestGetRelatedTaggedItems(TestCase): ('no more', 4, True, 'foo ter'), ('late', 2, False, 'bar ter'), ) - + for state, perch_size, perch_smelly, tags in parrot_details: perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) parrot = Parrot.objects.create(state=state, perch=perch) Tag.objects.update_tags(parrot, tags) - + self.l1 = Link.objects.create(name='link 1') Tag.objects.update_tags(self.l1, 'tag1 tag2 tag3 tag4 tag5') self.l2 = Link.objects.create(name='link 2') @@ -717,44 +876,46 @@ class TestGetRelatedTaggedItems(TestCase): self.l3 = Link.objects.create(name='link 3') Tag.objects.update_tags(self.l3, 'tag1') self.l4 = Link.objects.create(name='link 4') - + self.a1 = Article.objects.create(name='article 1') Tag.objects.update_tags(self.a1, 'tag1 tag2 tag3 tag4') - + def test_get_related_objects_of_same_model(self): related_objects = TaggedItem.objects.get_related(self.l1, Link) - self.assertEquals(len(related_objects), 2) - self.failUnless(self.l2 in related_objects) - self.failUnless(self.l3 in related_objects) - + self.assertEqual(len(related_objects), 2) + self.assertTrue(self.l2 in related_objects) + self.assertTrue(self.l3 in related_objects) + related_objects = TaggedItem.objects.get_related(self.l4, Link) - self.assertEquals(len(related_objects), 0) - + self.assertEqual(len(related_objects), 0) + def test_get_related_objects_of_same_model_limited_number_of_results(self): # This fails on Oracle because it has no support for a 'LIMIT' clause. - # See http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:127412348064 - + # See http://bit.ly/1AYNEsa + # ask for no more than 1 result related_objects = TaggedItem.objects.get_related(self.l1, Link, num=1) - self.assertEquals(len(related_objects), 1) - self.failUnless(self.l2 in related_objects) - + self.assertEqual(len(related_objects), 1) + self.assertTrue(self.l2 in related_objects) + def test_get_related_objects_of_same_model_limit_related_items(self): - related_objects = TaggedItem.objects.get_related(self.l1, Link.objects.exclude(name='link 3')) - self.assertEquals(len(related_objects), 1) - self.failUnless(self.l2 in related_objects) - + related_objects = TaggedItem.objects.get_related( + self.l1, Link.objects.exclude(name='link 3')) + self.assertEqual(len(related_objects), 1) + self.assertTrue(self.l2 in related_objects) + def test_get_related_objects_of_different_model(self): related_objects = TaggedItem.objects.get_related(self.a1, Link) - self.assertEquals(len(related_objects), 3) - self.failUnless(self.l1 in related_objects) - self.failUnless(self.l2 in related_objects) - self.failUnless(self.l3 in related_objects) - + self.assertEqual(len(related_objects), 3) + self.assertTrue(self.l1 in related_objects) + self.assertTrue(self.l2 in related_objects) + self.assertTrue(self.l3 in related_objects) + Tag.objects.update_tags(self.a1, 'tag6') related_objects = TaggedItem.objects.get_related(self.a1, Link) - self.assertEquals(len(related_objects), 0) - + self.assertEqual(len(related_objects), 0) + + class TestTagUsageForQuerySet(TestCase): def setUp(self): parrot_details = ( @@ -763,102 +924,125 @@ class TestTagUsageForQuerySet(TestCase): ('no more', 4, True, 'foo ter'), ('late', 2, False, 'bar ter'), ) - + for state, perch_size, perch_smelly, tags in parrot_details: perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) parrot = Parrot.objects.create(state=state, perch=perch) Tag.objects.update_tags(parrot, tags) - + def test_tag_usage_for_queryset(self): - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state='no more'), counts=True) + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(state='no more'), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 2) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state__startswith='p'), counts=True) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(state__startswith='p'), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4), counts=True) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(perch__size__gt=4), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'baz', 1) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), counts=True) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(perch__smelly=True), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', 1) in relevant_attribute_list) - self.failUnless((u'foo', 2) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), min_count=2) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(perch__smelly=True), min_count=2) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'foo', 2) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4)) - relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 4) - self.failUnless((u'bar', False) in relevant_attribute_list) - self.failUnless((u'baz', False) in relevant_attribute_list) - self.failUnless((u'foo', False) in relevant_attribute_list) - self.failUnless((u'ter', False) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=99)) - relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 0) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), counts=True) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) + for tag in tag_usage] + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('baz', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) + for tag in tag_usage] + self.assertEqual(len(relevant_attribute_list), 0) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(Q(perch__size__gt=6) | + Q(state__startswith='l')), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), min_count=2) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(Q(perch__size__gt=6) | + Q(state__startswith='l')), min_count=2) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'bar', 2) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l'))) - relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', False) in relevant_attribute_list) - self.failUnless((u'foo', False) in relevant_attribute_list) - self.failUnless((u'ter', False) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state='passed on'), counts=True) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('bar', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.filter(Q(perch__size__gt=6) | + Q(state__startswith='l'))) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) + for tag in tag_usage] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.exclude(state='passed on'), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 3) - self.failUnless((u'bar', 2) in relevant_attribute_list) - self.failUnless((u'foo', 2) in relevant_attribute_list) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state__startswith='p'), min_count=2) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.exclude(state__startswith='p'), min_count=2) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 1) - self.failUnless((u'ter', 2) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(Q(perch__size__gt=6) | Q(perch__smelly=False)), counts=True) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.exclude(Q(perch__size__gt=6) | + Q(perch__smelly=False)), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 2) - self.failUnless((u'foo', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - - tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(perch__smelly=True).filter(state__startswith='l'), counts=True) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset( + Parrot.objects.exclude(perch__smelly=True).filter( + state__startswith='l'), counts=True) relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] - self.assertEquals(len(relevant_attribute_list), 2) - self.failUnless((u'bar', 1) in relevant_attribute_list) - self.failUnless((u'ter', 1) in relevant_attribute_list) - + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + ################ # Model Fields # ################ @@ -869,33 +1053,121 @@ class TestTagFieldInForms(TestCase): class TestForm(forms.ModelForm): class Meta: model = FormTest - + fields = forms.ALL_FIELDS + form = TestForm() - self.assertEquals(form.fields['tags'].__class__.__name__, 'TagField') - + self.assertEqual(form.fields['tags'].__class__.__name__, 'TagField') + def test_recreation_of_tag_list_string_representations(self): plain = Tag.objects.create(name='plain') spaces = Tag.objects.create(name='spa ces') comma = Tag.objects.create(name='com,ma') - self.assertEquals(edit_string_for_tags([plain]), u'plain') - self.assertEquals(edit_string_for_tags([plain, spaces]), u'plain, spa ces') - self.assertEquals(edit_string_for_tags([plain, spaces, comma]), u'plain, spa ces, "com,ma"') - self.assertEquals(edit_string_for_tags([plain, comma]), u'plain "com,ma"') - self.assertEquals(edit_string_for_tags([comma, spaces]), u'"com,ma", spa ces') - + self.assertEqual(edit_string_for_tags([plain]), 'plain') + self.assertEqual(edit_string_for_tags([plain, spaces]), + 'plain, spa ces') + self.assertEqual(edit_string_for_tags([plain, spaces, comma]), + 'plain, spa ces, "com,ma"') + self.assertEqual(edit_string_for_tags([plain, comma]), + 'plain "com,ma"') + self.assertEqual(edit_string_for_tags([comma, spaces]), + '"com,ma", spa ces') + def test_tag_d_validation(self): - t = TagField() - self.assertEquals(t.clean('foo'), u'foo') - self.assertEquals(t.clean('foo bar baz'), u'foo bar baz') - self.assertEquals(t.clean('foo,bar,baz'), u'foo,bar,baz') - self.assertEquals(t.clean('foo, bar, baz'), u'foo, bar, baz') - self.assertEquals(t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'), - u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') - try: - t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') - except forms.ValidationError, ve: - self.assertEquals(str(ve), "[u'Each tag may be no more than 50 characters long.']") - except Exception, e: - raise e - else: - raise self.failureException('a ValidationError exception was supposed to have been raised.') + t = TagField(required=False) + self.assertEqual(t.clean(''), '') + self.assertEqual(t.clean('foo'), 'foo') + self.assertEqual(t.clean('foo bar baz'), 'foo bar baz') + self.assertEqual(t.clean('foo,bar,baz'), 'foo,bar,baz') + self.assertEqual(t.clean('foo, bar, baz'), 'foo, bar, baz') + self.assertEqual( + t.clean('foo qwertyuiopasdfghjklzxcvbnm' + 'qwertyuiopasdfghjklzxcvb bar'), + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') + self.assertRaises( + forms.ValidationError, t.clean, + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') + + def test_tag_get_from_model(self): + FormTest.objects.create(tags='test3 test2 test1') + FormTest.objects.create(tags='toto titi') + self.assertEquals(FormTest.tags, 'test1 test2 test3 titi toto') + + +######### +# Forms # +######### + + +class TestTagAdminForm(TestCase): + + def test_clean_name(self): + datas = {'name': 'tag'} + form = TagAdminForm(datas) + self.assertTrue(form.is_valid()) + + def test_clean_name_multi(self): + datas = {'name': 'tag error'} + form = TagAdminForm(datas) + self.assertFalse(form.is_valid()) + + def test_clean_name_too_long(self): + datas = {'name': 't' * (settings.MAX_TAG_LENGTH + 1)} + form = TagAdminForm(datas) + self.assertFalse(form.is_valid()) + +######### +# Views # +######### + + +@override_settings( + ROOT_URLCONF='tagging.tests.urls', + TEMPLATE_LOADERS=( + 'tagging.tests.utils.VoidLoader', + ), +) +class TestTaggedObjectList(TestCase): + + def setUp(self): + self.a1 = Article.objects.create(name='article 1') + self.a2 = Article.objects.create(name='article 2') + Tag.objects.update_tags(self.a1, 'static tag test') + Tag.objects.update_tags(self.a2, 'static test') + + def get_view(self, url, queries=1, code=200, + expected_items=1, + friendly_context='article_list', + template='tests/article_list.html'): + with self.assertNumQueries(queries): + response = self.client.get(url) + self.assertEquals(response.status_code, code) + + if code == 200: + self.assertTrue(isinstance(response.context['tag'], Tag)) + self.assertEqual(len(response.context['object_list']), + expected_items) + self.assertEqual(response.context['object_list'], + response.context[friendly_context]) + self.assertTemplateUsed(response, template) + return response + + def test_view_static(self): + self.get_view('/static/', expected_items=2) + + def test_view_dynamic(self): + self.get_view('/tag/', expected_items=1) + + def test_view_related(self): + response = self.get_view('/static/related/', + queries=2, expected_items=2) + self.assertEquals(len(response.context['related_tags']), 2) + + def test_view_no_queryset_no_model(self): + self.assertRaises(ImproperlyConfigured, self.get_view, + '/no-query-no-model/') + + def test_view_no_tag(self): + self.assertRaises(AttributeError, self.get_view, '/no-tag/') + + def test_view_404(self): + self.get_view('/unavailable/', code=404) diff --git a/tagging/tests/urls.py b/tagging/tests/urls.py new file mode 100644 index 0000000..a78dc8b --- /dev/null +++ b/tagging/tests/urls.py @@ -0,0 +1,20 @@ +"""Test urls for tagging.""" +from django.conf.urls import url + +from tagging.views import TaggedObjectList +from tagging.tests.models import Article + + +class StaticTaggedObjectList(TaggedObjectList): + tag = 'static' + queryset = Article.objects.all() + + +urlpatterns = [ + url(r'^static/$', StaticTaggedObjectList.as_view()), + url(r'^static/related/$', StaticTaggedObjectList.as_view( + related_tags=True)), + url(r'^no-tag/$', TaggedObjectList.as_view(model=Article)), + url(r'^no-query-no-model/$', TaggedObjectList.as_view()), + url(r'^(?P[^/]+(?u))/$', TaggedObjectList.as_view(model=Article)), +] diff --git a/tagging/tests/utils.py b/tagging/tests/utils.py new file mode 100644 index 0000000..2f041d9 --- /dev/null +++ b/tagging/tests/utils.py @@ -0,0 +1,16 @@ +""" +Tests utils for tagging. +""" +from django.template.loader import BaseLoader + + +class VoidLoader(BaseLoader): + """ + Template loader which is always returning + an empty template. + """ + is_usable = True + _accepts_engine_in_init = True + + def load_template_source(self, template_name, template_dirs=None): + return ('', 'voidloader:%s' % template_name) diff --git a/tagging/utils.py b/tagging/utils.py index e89bab0..6c1492f 100644 --- a/tagging/utils.py +++ b/tagging/utils.py @@ -3,17 +3,15 @@ Tagging utilities - from user tag input parsing to tag cloud calculation. """ import math -import types +from django.utils import six from django.db.models.query import QuerySet -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.translation import ugettext as _ -# Python 2.3 compatibility -try: - set -except NameError: - from sets import Set as set +# Font size distribution algorithms +LOGARITHMIC, LINEAR = 1, 2 + def parse_tag_input(input): """ @@ -26,13 +24,13 @@ def parse_tag_input(input): if not input: return [] - input = force_unicode(input) + input = force_text(input) # Special case - if there are no commas or double quotes in the # input, we don't *do* a recall... I mean, we know we only need to # split on spaces. - if u',' not in input and u'"' not in input: - words = list(set(split_strip(input, u' '))) + if ',' not in input and '"' not in input: + words = list(set(split_strip(input, ' '))) words.sort() return words @@ -46,56 +44,55 @@ def parse_tag_input(input): i = iter(input) try: while 1: - c = i.next() - if c == u'"': + c = next(i) + if c == '"': if buffer: - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) buffer = [] # Find the matching quote open_quote = True - c = i.next() - while c != u'"': + c = next(i) + while c != '"': buffer.append(c) - c = i.next() + c = next(i) if buffer: - word = u''.join(buffer).strip() + word = ''.join(buffer).strip() if word: words.append(word) buffer = [] open_quote = False else: - if not saw_loose_comma and c == u',': + if not saw_loose_comma and c == ',': saw_loose_comma = True buffer.append(c) except StopIteration: # If we were parsing an open quote which was never closed treat # the buffer as unquoted. if buffer: - if open_quote and u',' in buffer: + if open_quote and ',' in buffer: saw_loose_comma = True - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) if to_be_split: if saw_loose_comma: - delimiter = u',' + delimiter = ',' else: - delimiter = u' ' + delimiter = ' ' for chunk in to_be_split: words.extend(split_strip(chunk, delimiter)) words = list(set(words)) words.sort() return words -def split_strip(input, delimiter=u','): + +def split_strip(input, delimiter=','): """ Splits ``input`` on ``delimiter``, stripping each resulting string and returning a list of non-empty strings. """ - if not input: - return [] - words = [w.strip() for w in input.split(delimiter)] return [w for w in words if w] + def edit_string_for_tags(tags): """ Given list of ``Tag`` instances, creates a string representation of @@ -113,19 +110,20 @@ def edit_string_for_tags(tags): use_commas = False for tag in tags: name = tag.name - if u',' in name: + if ',' in name: names.append('"%s"' % name) continue - elif u' ' in name: + elif ' ' in name: if not use_commas: use_commas = True names.append(name) if use_commas: - glue = u', ' + glue = ', ' else: - glue = u' ' + glue = ' ' return glue.join(names) + def get_queryset_and_model(queryset_or_model): """ Given a ``QuerySet`` or a ``Model``, returns a two-tuple of @@ -139,6 +137,7 @@ def get_queryset_and_model(queryset_or_model): except AttributeError: return queryset_or_model._default_manager.all(), queryset_or_model + def get_tag_list(tags): """ Utility function for accepting tag input in a flexible manner. @@ -164,32 +163,35 @@ def get_tag_list(tags): return [tags] elif isinstance(tags, QuerySet) and tags.model is Tag: return tags - elif isinstance(tags, types.StringTypes): + elif isinstance(tags, six.string_types): return Tag.objects.filter(name__in=parse_tag_input(tags)) - elif isinstance(tags, (types.ListType, types.TupleType)): + elif isinstance(tags, (list, tuple)): if len(tags) == 0: return tags contents = set() for item in tags: - if isinstance(item, types.StringTypes): + if isinstance(item, six.string_types): contents.add('string') elif isinstance(item, Tag): contents.add('tag') - elif isinstance(item, (types.IntType, types.LongType)): + elif isinstance(item, six.integer_types): contents.add('int') if len(contents) == 1: if 'string' in contents: - return Tag.objects.filter(name__in=[force_unicode(tag) \ + return Tag.objects.filter(name__in=[force_text(tag) for tag in tags]) elif 'tag' in contents: return tags elif 'int' in contents: return Tag.objects.filter(id__in=tags) else: - raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.')) + raise ValueError( + _('If a list or tuple of tags is provided, ' + 'they must all be tag names, Tag objects or Tag ids.')) else: raise ValueError(_('The tag input given was invalid.')) + def get_tag(tag): """ Utility function for accepting single tag input in a flexible @@ -206,34 +208,35 @@ def get_tag(tag): return tag try: - if isinstance(tag, types.StringTypes): + if isinstance(tag, six.string_types): return Tag.objects.get(name=tag) - elif isinstance(tag, (types.IntType, types.LongType)): + elif isinstance(tag, six.integer_types): return Tag.objects.get(id=tag) except Tag.DoesNotExist: pass return None -# Font size distribution algorithms -LOGARITHMIC, LINEAR = 1, 2 def _calculate_thresholds(min_weight, max_weight, steps): delta = (max_weight - min_weight) / float(steps) return [min_weight + i * delta for i in range(1, steps + 1)] + def _calculate_tag_weight(weight, max_weight, distribution): """ Logarithmic tag weight calculation is based on code from the - `Tag Cloud`_ plugin for Mephisto, by Sven Fuchs. + *Tag Cloud* plugin for Mephisto, by Sven Fuchs. - .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud + http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud """ if distribution == LINEAR or max_weight == 1: return weight elif distribution == LOGARITHMIC: return math.log(weight) * max_weight / math.log(max_weight) - raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution) + raise ValueError( + _('Invalid distribution algorithm specified: %s.') % distribution) + def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC): """ @@ -255,7 +258,8 @@ def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC): thresholds = _calculate_thresholds(min_weight, max_weight, steps) for tag in tags: font_set = False - tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution) + tag_weight = _calculate_tag_weight( + tag.count, max_weight, distribution) for i in range(steps): if not font_set and tag_weight <= thresholds[i]: tag.font_size = i + 1 diff --git a/tagging/views.py b/tagging/views.py index 9e7e2f5..527bdd7 100644 --- a/tagging/views.py +++ b/tagging/views.py @@ -2,17 +2,20 @@ Tagging related views. """ from django.http import Http404 +from django.views.generic.list import ListView from django.utils.translation import ugettext as _ -from django.views.generic.list_detail import object_list +from django.core.exceptions import ImproperlyConfigured -from tagging.models import Tag, TaggedItem -from tagging.utils import get_tag, get_queryset_and_model +from tagging.models import Tag +from tagging.models import TaggedItem +from tagging.utils import get_tag +from tagging.utils import get_queryset_and_model -def tagged_object_list(request, queryset_or_model=None, tag=None, - related_tags=False, related_tag_counts=True, **kwargs): + +class TaggedObjectList(ListView): """ A thin wrapper around - ``django.views.generic.list_detail.object_list`` which creates a + ``django.views.generic.list.ListView`` which creates a ``QuerySet`` containing instances of the given queryset or model tagged with the given tag. @@ -26,27 +29,50 @@ def tagged_object_list(request, queryset_or_model=None, tag=None, tag will have a ``count`` attribute indicating the number of items which have it in addition to the given tag. """ - if queryset_or_model is None: - try: - queryset_or_model = kwargs.pop('queryset_or_model') - except KeyError: - raise AttributeError(_('tagged_object_list must be called with a queryset or a model.')) - - if tag is None: - try: - tag = kwargs.pop('tag') - except KeyError: - raise AttributeError(_('tagged_object_list must be called with a tag.')) - - tag_instance = get_tag(tag) - if tag_instance is None: - raise Http404(_('No Tag found matching "%s".') % tag) - queryset = TaggedItem.objects.get_by_model(queryset_or_model, tag_instance) - if not kwargs.has_key('extra_context'): - kwargs['extra_context'] = {} - kwargs['extra_context']['tag'] = tag_instance - if related_tags: - kwargs['extra_context']['related_tags'] = \ - Tag.objects.related_for_model(tag_instance, queryset_or_model, - counts=related_tag_counts) - return object_list(request, queryset, **kwargs) + tag = None + related_tags = False + related_tag_counts = True + + def get_tag(self): + if self.tag is None: + try: + self.tag = self.kwargs.pop('tag') + except KeyError: + raise AttributeError( + _('TaggedObjectList must be called with a tag.')) + + tag_instance = get_tag(self.tag) + if tag_instance is None: + raise Http404(_('No Tag found matching "%s".') % self.tag) + + return tag_instance + + def get_queryset_or_model(self): + if self.queryset is not None: + return self.queryset + elif self.model is not None: + return self.model + else: + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset_or_model()." % { + 'cls': self.__class__.__name__ + } + ) + + def get_queryset(self): + self.queryset_or_model = self.get_queryset_or_model() + self.tag_instance = self.get_tag() + return TaggedItem.objects.get_by_model( + self.queryset_or_model, self.tag_instance) + + def get_context_data(self, **kwargs): + context = super(TaggedObjectList, self).get_context_data(**kwargs) + context['tag'] = self.tag_instance + + if self.related_tags: + queryset, model = get_queryset_and_model(self.queryset_or_model) + context['related_tags'] = Tag.objects.related_for_model( + self.tag_instance, model, counts=self.related_tag_counts) + return context diff --git a/versions.cfg b/versions.cfg new file mode 100644 index 0000000..49ece04 --- /dev/null +++ b/versions.cfg @@ -0,0 +1,23 @@ +[versions] +blessings = 1.6 +buildout-versions-checker = 1.9.2 +coverage = 3.7.1 +django = 1.8.1 +flake8 = 2.4.0 +futures = 3.0.2 +mccabe = 0.3 +nose = 1.3.6 +nose-progressive = 1.5.1 +nose-sfd = 0.4 +packaging = 15.1 +pbp.recipe.noserunner = 0.2.6 +pep8 = 1.5.7 +pyflakes = 0.8.1 +python-coveralls = 2.5.0 +pyyaml = 3.11 +requests = 2.7.0 +setuptools = 15.2 +sh = 1.11 +six = 1.9.0 +zc.buildout = 2.3.1 +zc.recipe.egg = 2.0.1 -- cgit v1.2.3