aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--._CHANGELOG.txtbin187 -> 0 bytes
-rw-r--r--._MANIFEST.inbin184 -> 0 bytes
-rw-r--r--._README.txtbin184 -> 0 bytes
-rw-r--r--CHANGELOG.txt37
-rw-r--r--INSTALL.txt14
-rw-r--r--LICENSE.txt4
-rw-r--r--MANIFEST.in9
-rw-r--r--PKG-INFO188
-rw-r--r--README.rst20
-rw-r--r--README.txt10
-rw-r--r--bootstrap.py189
-rw-r--r--buildout.cfg50
-rw-r--r--django_tagging.egg-info/PKG-INFO186
-rw-r--r--django_tagging.egg-info/SOURCES.txt40
-rw-r--r--django_tagging.egg-info/dependency_links.txt1
-rw-r--r--django_tagging.egg-info/not-zip-safe1
-rw-r--r--django_tagging.egg-info/top_level.txt1
-rw-r--r--docs/._overview.txtbin188 -> 0 bytes
-rw-r--r--docs/Makefile192
-rw-r--r--docs/conf.py293
-rw-r--r--docs/index.rst (renamed from docs/overview.txt)84
-rw-r--r--setup.cfg8
-rw-r--r--setup.py96
-rw-r--r--tagging/._models.pybin187 -> 0 bytes
-rw-r--r--tagging/._settings.pybin184 -> 0 bytes
-rw-r--r--tagging/__init__.py69
-rw-r--r--tagging/admin.py13
-rw-r--r--tagging/apps.py14
-rw-r--r--tagging/fields.py13
-rw-r--r--tagging/forms.py13
-rw-r--r--tagging/generic.py13
-rw-r--r--tagging/managers.py13
-rw-r--r--tagging/migrations/0001_initial.py52
-rw-r--r--tagging/migrations/__init__.py3
-rw-r--r--tagging/models.py131
-rw-r--r--tagging/registry.py51
-rw-r--r--tagging/settings.py4
-rw-r--r--tagging/templatetags/__init__.py3
-rw-r--r--tagging/templatetags/tagging_tags.py131
-rw-r--r--tagging/tests/._settings.pybin184 -> 0 bytes
-rw-r--r--tagging/tests/._tags.txtbin184 -> 0 bytes
-rw-r--r--tagging/tests/__init__.py3
-rw-r--r--tagging/tests/models.py19
-rw-r--r--tagging/tests/settings.py50
-rw-r--r--tagging/tests/tests.py1386
-rw-r--r--tagging/tests/urls.py20
-rw-r--r--tagging/tests/utils.py16
-rw-r--r--tagging/utils.py92
-rw-r--r--tagging/views.py86
-rw-r--r--versions.cfg23
50 files changed, 2655 insertions, 986 deletions
diff --git a/._CHANGELOG.txt b/._CHANGELOG.txt
deleted file mode 100644
index 9bf97be..0000000
--- a/._CHANGELOG.txt
+++ /dev/null
Binary files differ
diff --git a/._MANIFEST.in b/._MANIFEST.in
deleted file mode 100644
index 8af97db..0000000
--- a/._MANIFEST.in
+++ /dev/null
Binary files differ
diff --git a/._README.txt b/._README.txt
deleted file mode 100644
index 8645f1c..0000000
--- a/._README.txt
+++ /dev/null
Binary files 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
--- a/docs/._overview.txt
+++ /dev/null
Binary files 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 <target>' where <target> 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
+# "<project> v<release> 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 <link> 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/overview.txt b/docs/index.rst
index feb08c9..c73cbe4 100644
--- a/docs/overview.txt
+++ b/docs/index.rst
@@ -9,8 +9,8 @@ retrieval of tags simple.
.. _`Django`: http://www.djangoproject.com
.. contents::
- :depth: 3
-
+ :local:
+ :depth: 3
Installation
============
@@ -19,37 +19,31 @@ Installing an official release
------------------------------
Official releases are made available from
-http://code.google.com/p/django-tagging/
+https://pypi.python.org/pypi/django-tagging/
Source distribution
~~~~~~~~~~~~~~~~~~~
-Download the .zip distribution file and unpack it. Inside is a script
+Download the a distribution file and unpack it. Inside is a script
named ``setup.py``. Enter this command::
- python setup.py install
+ $ 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.
+More easily with :program:`pip`::
-An uninstaller will also be created, accessible through Add/Remove
-Programs in your Control Panel.
+ $ 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, perform a `Subversion`_ checkout instead. The following
-command will check the application's development branch out to an
-``tagging-trunk`` directory::
+official release, clone the git repository instead. The following
+command will clone the development branch to ``django-tagging`` directory::
- svn checkout http://django-tagging.googlecode.com/svn/trunk/ tagging-trunk
+ 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
@@ -60,26 +54,23 @@ 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')
+ >>> tagging.__version__
+ 0.4.dev0
When you want to update your copy of the Django Tagging source code, run
-the command ``svn update`` from within the ``tagging-trunk`` directory.
+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 trunk, keep an eye on the `CHANGELOG`_ and the
- `backwards-incompatible changes wiki page`_ before you update your
- copy of the source code.
+ If you're tracking git, keep an eye on the `CHANGELOG`_
+ 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
+.. _`CHANGELOG`: https://github.com/Fantomas42/django-tagging/blob/develop/CHANGELOG.txt
Using Django Tagging in your applications
-----------------------------------------
@@ -88,14 +79,13 @@ 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``.
+ 2. Run the command ``manage.py migrate``.
-The ``syncdb`` command creates the necessary database tables and
+The ``migrate`` command creates the necessary database tables and
creates permission objects for all installed apps that need them.
That's it!
-
Settings
========
@@ -139,12 +129,12 @@ access some additional tagging-related features.
The ``register`` function
-------------------------
-To register a model, import the ``tagging`` module and call its
+To register a model, import the ``tagging.registry`` module and call its
``register`` function, like so::
from django.db import models
- import tagging
+ from tagging.registry import register
class Widget(models.Model):
name = models.CharField(max_length=50)
@@ -206,7 +196,7 @@ A manager for retrieving tags used by a particular model.
Defines the following methods:
-* ``get_query_set()`` -- as this method is redefined, any ``QuerySets``
+* ``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.
@@ -725,29 +715,26 @@ Generic views
The ``tagging.views`` module contains views to handle simple cases of
common display logic related to tagging.
-``tagging.views.tagged_object_list``
-------------------------------------
+``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_detail.object_list`` view, which takes a
+``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 ``object_list``), building the appropriate
+arguments supported by ``ListView``), 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
+Please refer to the `ListView documentation`_ for additional optional
arguments which may be given.
* ``related_tags``: If ``True``, a ``related_tags`` context variable
@@ -761,12 +748,12 @@ arguments which may be given.
**Template context:**
-Please refer to the `object_list documentation`_ for additional
+Please refer to the `ListView 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
+.. _`ListView documentation`: https://docs.djangoproject.com/en/1.8/ref/class-based-views/generic-display/#listview
Example usage
~~~~~~~~~~~~~
@@ -776,15 +763,13 @@ 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 tagging.views import TaggedObjectList
from shop.apps.products.models import Widget
urlpatterns = patterns('',
- url(r'^widgets/tag/(?P<tag>[^/]+)/$',
- tagged_object_list,
- dict(queryset_or_model=Widget, paginate_by=10, allow_empty=True,
- template_object_name='widget'),
+ url(r'^widgets/tag/(?P<tag>[^/]+(?u))/$',
+ TaggedObjectList.as_view(model=Widget, paginate_by=10, allow_empty=True),
name='widget_tag_detail'),
)
@@ -793,13 +778,10 @@ perform filtering of the objects which are listed::
from myapp.models import People
- from tagging.views import tagged_object_list
+ from tagging.views import TaggedObjectList
- def tagged_people(request, country_code, tag):
+ class TaggedPeopleFilteredList(TaggedObjectList):
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
=============
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
--- a/tagging/._models.py
+++ /dev/null
Binary files differ
diff --git a/tagging/._settings.py b/tagging/._settings.py
deleted file mode 100644
index 3c4ee38..0000000
--- a/tagging/._settings.py
+++ /dev/null
Binary files 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
--- a/tagging/tests/._settings.py
+++ /dev/null
Binary files differ
diff --git a/tagging/tests/._tags.txt b/tagging/tests/._tags.txt
deleted file mode 100644
index 3348eaa..0000000
--- a/tagging/tests/._tags.txt
+++ /dev/null
Binary files 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<tag>[^/]+(?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