summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichal Čihař <nijel@debian.org>2015-11-27 07:37:05 (GMT)
committerMichal Čihař <nijel@debian.org>2015-11-27 07:37:05 (GMT)
commit0c6b364e5ec3befdfdc6f2fa02fe4fd1bc52027f (patch)
tree45264725d9c18c99d4e96f03f2d446a7bd4727cf
downloaddjango-taggit-0c6b364e5ec3befdfdc6f2fa02fe4fd1bc52027f.zip
django-taggit-0c6b364e5ec3befdfdc6f2fa02fe4fd1bc52027f.tar.gz
django-taggit-0c6b364e5ec3befdfdc6f2fa02fe4fd1bc52027f.tar.bz2
django-taggit (0.17.4-1) unstable; urgency=mediumHEADmaster
* New upstream release. # imported from the archive
-rw-r--r--AUTHORS15
-rw-r--r--CHANGELOG.txt163
-rw-r--r--LICENSE27
-rw-r--r--MANIFEST.in9
-rw-r--r--PKG-INFO62
-rw-r--r--README.rst39
-rw-r--r--debian/changelog97
-rw-r--r--debian/compat1
-rw-r--r--debian/control41
-rw-r--r--debian/copyright54
-rw-r--r--debian/gbp.conf5
-rwxr-xr-xdebian/rules9
-rw-r--r--debian/source/format1
-rw-r--r--debian/watch2
-rw-r--r--django_taggit.egg-info/PKG-INFO62
-rw-r--r--django_taggit.egg-info/SOURCES.txt64
-rw-r--r--django_taggit.egg-info/dependency_links.txt1
-rw-r--r--django_taggit.egg-info/not-zip-safe1
-rw-r--r--django_taggit.egg-info/top_level.txt1
-rw-r--r--docs/Makefile89
-rw-r--r--docs/admin.txt14
-rw-r--r--docs/api.txt114
-rw-r--r--docs/changelog.txt1
-rw-r--r--docs/conf.py198
-rw-r--r--docs/custom_tagging.txt167
-rw-r--r--docs/external_apps.txt34
-rw-r--r--docs/forms.txt51
-rw-r--r--docs/getting_started.txt39
-rw-r--r--docs/index.txt45
-rwxr-xr-xruntests.py31
-rw-r--r--setup.cfg18
-rw-r--r--setup.py41
-rw-r--r--taggit/__init__.py1
-rw-r--r--taggit/admin.py21
-rw-r--r--taggit/forms.py27
-rw-r--r--taggit/locale/cs/LC_MESSAGES/django.mobin0 -> 1061 bytes
-rw-r--r--taggit/locale/cs/LC_MESSAGES/django.po64
-rw-r--r--taggit/locale/de/LC_MESSAGES/django.mobin0 -> 1331 bytes
-rw-r--r--taggit/locale/de/LC_MESSAGES/django.po67
-rw-r--r--taggit/locale/en/LC_MESSAGES/django.po68
-rw-r--r--taggit/locale/eo/LC_MESSAGES/django.mobin0 -> 1322 bytes
-rw-r--r--taggit/locale/eo/LC_MESSAGES/django.po67
-rw-r--r--taggit/locale/he/LC_MESSAGES/django.mobin0 -> 847 bytes
-rw-r--r--taggit/locale/he/LC_MESSAGES/django.po68
-rw-r--r--taggit/locale/it/LC_MESSAGES/django.mobin0 -> 1195 bytes
-rw-r--r--taggit/locale/it/LC_MESSAGES/django.po70
-rw-r--r--taggit/locale/ja/LC_MESSAGES/django.mobin0 -> 1437 bytes
-rw-r--r--taggit/locale/ja/LC_MESSAGES/django.po67
-rw-r--r--taggit/locale/nb/LC_MESSAGES/django.mobin0 -> 1278 bytes
-rw-r--r--taggit/locale/nb/LC_MESSAGES/django.po72
-rw-r--r--taggit/locale/nl/LC_MESSAGES/django.mobin0 -> 1217 bytes
-rw-r--r--taggit/locale/nl/LC_MESSAGES/django.po63
-rw-r--r--taggit/locale/pt_BR/LC_MESSAGES/django.mobin0 -> 1064 bytes
-rw-r--r--taggit/locale/pt_BR/LC_MESSAGES/django.po62
-rw-r--r--taggit/locale/ru/LC_MESSAGES/django.mobin0 -> 1513 bytes
-rw-r--r--taggit/locale/ru/LC_MESSAGES/django.po69
-rw-r--r--taggit/managers.py592
-rw-r--r--taggit/migrations/0001_initial.py41
-rw-r--r--taggit/migrations/0002_auto_20150616_2121.py18
-rw-r--r--taggit/migrations/__init__.py21
-rw-r--r--taggit/models.py225
-rw-r--r--taggit/south_migrations/0001_initial.py60
-rw-r--r--taggit/south_migrations/0002_unique_tagnames.py43
-rw-r--r--taggit/south_migrations/__init__.py0
-rw-r--r--taggit/utils.py137
-rw-r--r--taggit/views.py20
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/forms.py36
-rw-r--r--tests/migrations/0001_initial.py468
-rw-r--r--tests/migrations/__init__.py0
-rw-r--r--tests/models.py228
-rw-r--r--tests/tests.py659
-rw-r--r--tox.ini129
73 files changed, 4959 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..2950197
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,15 @@
+django-taggit was originally created by Alex Gaynor.
+
+The following is a list of much appreciated contributors:
+
+Nathan Borror <nathan@playgroundblues.com>
+fakeempire <adam@fakeempire.com>
+Ben Firshman <ben@firshman.co.uk>
+Alex Gaynor <alex.gaynor@gmail.com>
+Rob Hudson <rob@cogit8.org>
+Carl Meyer <carl@oddbird.net>
+Frank Wiles
+Jonathan Buchanan
+idle sign <idlesign@yandex.ru>
+Charles Leifer
+Florian Apolloner <apollo13@apolloner.eu>
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
new file mode 100644
index 0000000..f9520c3
--- /dev/null
+++ b/CHANGELOG.txt
@@ -0,0 +1,163 @@
+Changelog
+=========
+
+0.17.4 (2015-11-25)
+~~~~~~~~~~~~~~~~~~~
+ * Allows custom Through Model with GenericForeignKey
+ * https://github.com/alex/django-taggit/pull/359
+
+0.17.3 (2015-10-26)
+~~~~~~~~~~~~~~~~~~~
+ * Silence Django 1.9 warning about on_delete
+ * https://github.com/alex/django-taggit/pull/338
+
+0.17.2 (2015-10-25)
+~~~~~~~~~~~~~~~~~~~
+ * Django 1.9 beta compatibility
+ * https://github.com/alex/django-taggit/pull/352
+
+0.17.1 (2015-09-10)
+~~~~~~~~~~~~~~~~~~~
+ * Fix unknown column `object_id` issue with Django 1.6+
+ * https://github.com/alex/django-taggit/pull/305
+
+0.17.0 (2015-08-14)
+~~~~~~~~~~~~~~~~~~~
+ * Database index added on TaggedItem fields content_type & object_id
+ * https://github.com/alex/django-taggit/pull/319
+
+0.16.4 (2015-08-13)
+~~~~~~~~~~~~~~~~~~~
+ * Access default manager via class instead of instance
+ * https://github.com/alex/django-taggit/pull/335
+
+0.16.3 (2015-08-08)
+~~~~~~~~~~~~~~~~~~~
+ * Prevent IntegrityError with custom TagBase classes
+ * https://github.com/alex/django-taggit/pull/334
+
+0.16.2 (2015-07-13)
+~~~~~~~~~~~~~~~~~~~
+ * Fix an admin bug related to the `Manager` property `through_fields`
+ * https://github.com/alex/django-taggit/pull/328
+
+0.16.1 (2015-07-09)
+~~~~~~~~~~~~~~~~~~~
+ * Fix bug that assumed all primary keys are named 'id'
+ * https://github.com/alex/django-taggit/pull/322
+
+0.16.0 (2015-07-04)
+~~~~~~~~~~~~~~~~~~~
+ * Add option to allow case-insensitive tags
+ * https://github.com/alex/django-taggit/pull/325
+
+0.15.0 (2015-06-23)
+~~~~~~~~~~~~~~~~~~~
+ * Fix wrong slugs for non-latin chars
+ * Only works if optional GPL dependency (unidecode) is installed
+ * https://github.com/alex/django-taggit/pull/315
+ * https://github.com/alex/django-taggit/pull/273
+
+0.14.0 (2015-04-26)
+~~~~~~~~~~~~~~~~~~~
+ * Prevent extra JOIN when prefetching
+ * https://github.com/alex/django-taggit/pull/275
+ * Prevent _meta warnings with Django 1.8
+ * https://github.com/alex/django-taggit/pull/299
+
+0.13.0 (2015-04-02)
+~~~~~~~~~~~~~~~~~~~
+ * Django 1.8 support
+ * https://github.com/alex/django-taggit/pull/297
+
+0.12.3 (2015-03-03)
+~~~~~~~~~~~~~~~~~~~
+ * Specify that the internal type of the TaggitManager is a ManyToManyField
+
+0.12.2 (2014-21-09)
+~~~~~~~~~~~~~~~~~~~
+ * Fixed 1.7 migrations.
+
+0.12.1 (2014-10-08)
+~~~~~~~~~~~~~~~~~~~
+ * Final (hopefully) fixes for the upcoming Django 1.7 release.
+ * Added Japanese translation.
+
+0.12.0 (2014-20-04)
+~~~~~~~~~~~~~~~~~~~
+ * **Backwards incompatible:** Support for Django 1.7 migrations. South users
+ have to set ``SOUTH_MIGRATION_MODULES`` to use ``taggit.south_migrations``
+ for taggit.
+ * **Backwards incompatible:** Django's new transaction handling is used on
+ Django 1.6 and newer.
+ * **Backwards incompatible:** ``Tag.save`` got changed to opportunistically
+ try to save the tag and if that fails fall back to selecting existing
+ similar tags and retry -- if that fails too an ``IntegrityError`` is
+ raised by the database, your app will have to handle that.
+ * Added Italian and Esperanto translations.
+
+0.11.2 (2013-13-12)
+~~~~~~~~~~~~~~~~~~~
+ * Forbid multiple TaggableManagers via generic foreign keys.
+
+0.11.1 (2013-25-11)
+~~~~~~~~~~~~~~~~~~~
+ * Fixed support for Django 1.4 and 1.5.
+
+0.11.0 (2013-25-11)
+~~~~~~~~~~~~~~~~~~~
+ * Added support for prefetch_related on tags fields.
+ * Fixed support for Django 1.7.
+ * Made the tagging relations unserializeable again.
+ * Allow more than one TaggableManager on models (assuming concrete FKs are
+ used for the relations).
+
+0.10.0 (2013-17-08)
+~~~~~~~~~~~~~~~~~~~
+
+ * Support for Django 1.6 and 1.7.
+ * Python3 support
+ * **Backwards incompatible:** Dropped support for Django < 1.4.5.
+ * Tag names are unique now, use the provided South migrations to upgrade.
+
+0.9.2 (2011-01-17)
+~~~~~~~~~~~~~~~~~~
+
+ * **Backwards incompatible:** Forms containing a :class:`TaggableManager` by
+ default now require tags, to change this provide ``blank=True`` to the
+ :class:`TaggableManager`.
+ * Now works with Django 1.3 (as of beta-1).
+
+0.9.0 (2010-09-22)
+~~~~~~~~~~~~~~~~~~
+
+ * Added a Hebrew locale.
+ * Added an index on the ``object_id`` field of ``TaggedItem``.
+ * When displaying tags always join them with commas, never spaces.
+ * The docs are now available `online <http://django-taggit.readthedocs.org/>`_.
+ * Custom ``Tag`` models are now allowed.
+ * **Backwards incompatible:** Filtering on tags is no longer
+ ``filter(tags__in=["foo"])``, it is written
+ ``filter(tags__name__in=["foo"])``.
+ * Added a German locale.
+ * Added a Dutch locale.
+ * Removed ``taggit.contrib.suggest``, it now lives in an external application,
+ see :doc:`external_apps` for more information.
+
+0.8.0 (2010-06-22)
+~~~~~~~~~~~~~~~~~~
+
+ * Fixed querying for objects using ``exclude(tags__in=tags)``.
+ * Marked strings as translatable.
+
+ * Added a Russian translation.
+ * Created a `mailing list <http://groups.google.com/group/django-taggit>`_.
+ * Smarter tagstring parsing for form field; ported from Jonathan
+ Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com>`_. Now supports tags
+ containing commas. See :ref:`tags-in-forms` for details.
+ * Switched to using savepoints around the slug generation for tags. This
+ ensures that it works fine on databases (such as Postgres) which dirty a
+ transaction with an ``IntegrityError``.
+ * Added Python 2.4 compatibility.
+ * Added Django 1.1 compatibility.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d2b0157
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) Alex Gaynor and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of django-taggit nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT 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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1908386
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,9 @@
+include AUTHORS
+include CHANGELOG.txt
+include LICENSE
+include README.rst
+include tox.ini
+include runtests.py
+recursive-include docs *
+recursive-include taggit/locale *
+recursive-include tests *
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..dfe9886
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,62 @@
+Metadata-Version: 1.1
+Name: django-taggit
+Version: 0.17.4
+Summary: django-taggit is a reusable Django application for simple tagging.
+Home-page: http://github.com/alex/django-taggit/tree/master
+Author: Alex Gaynor
+Author-email: alex.gaynor@gmail.com
+License: BSD
+Description: django-taggit
+ =============
+
+ ``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
+ ``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
+
+ .. code:: python
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager()
+
+
+ Then you can use the API like so:
+
+ .. code:: python
+
+ >>> apple = Food.objects.create(name="apple")
+ >>> apple.tags.add("red", "green", "delicious")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: green>, <Tag: delicious>]
+ >>> apple.tags.remove("green")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: delicious>]
+ >>> Food.objects.filter(tags__name__in=["red"])
+ [<Food: apple>, <Food: cherry>]
+
+ Tags will show up for you automatically in forms and the admin.
+
+ ``django-taggit`` requires Django 1.4.5 or greater.
+
+ For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
+ development you can contact the
+ `mailinglist <http://groups.google.com/group/django-taggit>`_.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Framework :: Django
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..c70f655
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,39 @@
+django-taggit
+=============
+
+``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
+``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
+
+.. code:: python
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager()
+
+
+Then you can use the API like so:
+
+.. code:: python
+
+ >>> apple = Food.objects.create(name="apple")
+ >>> apple.tags.add("red", "green", "delicious")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: green>, <Tag: delicious>]
+ >>> apple.tags.remove("green")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: delicious>]
+ >>> Food.objects.filter(tags__name__in=["red"])
+ [<Food: apple>, <Food: cherry>]
+
+Tags will show up for you automatically in forms and the admin.
+
+``django-taggit`` requires Django 1.4.5 or greater.
+
+For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
+development you can contact the
+`mailinglist <http://groups.google.com/group/django-taggit>`_.
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..c6f0cb2
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,97 @@
+django-taggit (0.17.4-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Michal Čihař <nijel@debian.org> Fri, 27 Nov 2015 08:37:05 +0100
+
+django-taggit (0.17.3-1) unstable; urgency=medium
+
+ * New upstream release.
+ - Django 1.9 compatibility.
+
+ -- Michal Čihař <nijel@debian.org> Thu, 29 Oct 2015 14:44:36 +0100
+
+django-taggit (0.17.1-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Michal Čihař <nijel@debian.org> Thu, 17 Sep 2015 09:47:25 +0200
+
+django-taggit (0.17.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Update debian/watch.
+ * Add python 3 package.
+ * Run tests during build.
+
+ -- Michal Čihař <nijel@debian.org> Mon, 24 Aug 2015 13:44:42 +0200
+
+django-taggit (0.12.2-1) unstable; urgency=medium
+
+ * New upstream release.
+ - Fixed 1.7 migrations.
+ * Bump standards to 3.9.6.
+
+ -- Michal Čihař <nijel@debian.org> Thu, 25 Sep 2014 09:20:46 +0200
+
+django-taggit (0.12.1-1) unstable; urgency=medium
+
+ * New upstream release.
+ - Complete support for Django 1.7 (Closes: #755614).
+
+ -- Michal Čihař <nijel@debian.org> Mon, 11 Aug 2014 09:55:45 +0200
+
+django-taggit (0.12-1) unstable; urgency=low
+
+ * New upstream release.
+ - Backwards incompatible: Support for Django 1.7 migrations. South users
+ have to set SOUTH_MIGRATION_MODULES to use taggit.south_migrations
+ for taggit.
+ - Backwards incompatible: Django's new transaction handling is used on
+ Django 1.6 and newer.
+ - Backwards incompatible: Tag.save got changed to opportunistically
+ try to save the tag and if that fails fall back to selecting existing
+ similar tags and retry -- if that fails too an IntegrityError is
+ raised by the database, your app will have to handle that.
+ - Added Italian and Esperanto translations.
+
+ -- Michal Čihař <nijel@debian.org> Tue, 29 Apr 2014 10:05:29 +0200
+
+django-taggit (0.11.2-2) unstable; urgency=medium
+
+ * Bump standards to 3.9.5.
+ * Really do not ship tests in module without namespace (Closes: #738210).
+
+ -- Michal Čihař <nijel@debian.org> Mon, 10 Feb 2014 09:36:44 +0100
+
+django-taggit (0.11.2-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Do not ship tests in module without namespace (Closes: #733729).
+
+ -- Michal Čihař <nijel@debian.org> Fri, 03 Jan 2014 10:20:12 +0100
+
+django-taggit (0.11.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * Bump standards to 3.9.5.
+
+ -- Michal Čihař <nijel@debian.org> Mon, 09 Dec 2013 14:00:12 +0100
+
+django-taggit (0.10a1-3) unstable; urgency=low
+
+ * Fixed Vcs-* fields in debian/control (Closes: #721524).
+
+ -- Michal Čihař <nijel@debian.org> Mon, 09 Sep 2013 09:36:36 +0200
+
+django-taggit (0.10a1-2) unstable; urgency=low
+
+ * Fix typo in package name.
+
+ -- Michal Čihař <nijel@debian.org> Thu, 15 Aug 2013 16:48:21 +0200
+
+django-taggit (0.10a1-1) unstable; urgency=low
+
+ * Initial release. (Closes: #719809)
+
+ -- Michal Čihař <nijel@debian.org> Thu, 15 Aug 2013 16:31:05 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..d971ea6
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,41 @@
+Source: django-taggit
+Section: python
+Priority: optional
+Maintainer: Michal Čihař <nijel@debian.org>
+Uploaders: Python Applications Packaging Team <python-apps-team@lists.alioth.debian.org>
+Build-Depends: debhelper (>= 9)
+Build-Depends-Indep: python (>= 2.6.6-3), python-setuptools, python3-setuptools, python3-all, dh-python, python-django, python3-django
+Standards-Version: 3.9.6
+Vcs-Browser: http://anonscm.debian.org/gitweb/?p=collab-maint/django-taggit.git
+Vcs-Git: git://anonscm.debian.org/collab-maint/django-taggit.git
+Homepage: https://github.com/alex/django-taggit
+X-Python-Version: >= 2.6
+X-Python3-Version: >= 3.2
+
+Package: python-django-taggit
+Architecture: all
+Depends: ${python:Depends}, ${misc:Depends}, python-django
+Breaks: ${python:Breaks}
+Description: simple tagging for Django (Python 2)
+ This is a generic tagging application for Django, which allows
+ association of a number of tags with any Model instance and makes
+ retrieval of tags simple.
+ .
+ django-taggit a simpler approach to tagging with Django. Add "taggit" to your
+ INSTALLED_APPS then just add a TaggableManager to your model.
+ .
+ This package installs the library for Python 2.
+
+Package: python3-django-taggit
+Architecture: all
+Depends: ${python3:Depends}, ${misc:Depends}, python3-django
+Breaks: ${python3:Breaks}
+Description: simple tagging for Django (Python 3)
+ This is a generic tagging application for Django, which allows
+ association of a number of tags with any Model instance and makes
+ retrieval of tags simple.
+ .
+ django-taggit a simpler approach to tagging with Django. Add "taggit" to your
+ INSTALLED_APPS then just add a TaggableManager to your model.
+ .
+ This package installs the library for Python 3.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..5f2fcd6
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,54 @@
+This package was debianized by Michal Čihař <nijel@debian.org> on
+Thu, 15 Aug 2013 16:18:02 +0200.
+
+It was downloaded from <https://pypi.python.org/pypi/django-taggit>.
+
+Upstream Authors:
+
+ Nathan Borror <nathan@playgroundblues.com>
+ fakeempire <adam@fakeempire.com>
+ Ben Firshman <ben@firshman.co.uk>
+ Alex Gaynor <alex.gaynor@gmail.com>
+ Rob Hudson <rob@cogit8.org>
+ Carl Meyer <carl@oddbird.net>
+ Frank Wiles
+ Jonathan Buchanan
+ idle sign <idlesign@yandex.ru>
+ Charles Leifer
+ Florian Apolloner <apollo13@apolloner.eu>
+
+Copyright:
+
+ Copyright (c) Alex Gaynor and individual contributors.
+
+License:
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of django-taggit nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT 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.
+
+On Debian systems, the complete text of the GNU General
+Public License can be found in `/usr/share/common-licenses/GPL-2'.
+
+The Debian packaging is Copyright (C) 2013, Michal Čihař
+<nijel@debian.org> and is licensed under the GPL, see above.
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000..41f4f8c
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,5 @@
+# Configuration file for git-buildpackage and friends
+
+[DEFAULT]
+sign-tags = True
+pristine-tar = True
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..1d71897
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,9 @@
+#!/usr/bin/make -f
+
+export PYBUILD_NAME=django-taggit
+%:
+ dh $@ --fail-missing --with python2,python3 --buildsystem=pybuild
+
+override_dh_auto_test:
+ python2 runtests.py
+ python3 runtests.py
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..c65eef5
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,2 @@
+version=3
+http://pypi.debian.net/django-taggit/django-taggit-(.*)\.tar\.gz
diff --git a/django_taggit.egg-info/PKG-INFO b/django_taggit.egg-info/PKG-INFO
new file mode 100644
index 0000000..dfe9886
--- /dev/null
+++ b/django_taggit.egg-info/PKG-INFO
@@ -0,0 +1,62 @@
+Metadata-Version: 1.1
+Name: django-taggit
+Version: 0.17.4
+Summary: django-taggit is a reusable Django application for simple tagging.
+Home-page: http://github.com/alex/django-taggit/tree/master
+Author: Alex Gaynor
+Author-email: alex.gaynor@gmail.com
+License: BSD
+Description: django-taggit
+ =============
+
+ ``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
+ ``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
+
+ .. code:: python
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager()
+
+
+ Then you can use the API like so:
+
+ .. code:: python
+
+ >>> apple = Food.objects.create(name="apple")
+ >>> apple.tags.add("red", "green", "delicious")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: green>, <Tag: delicious>]
+ >>> apple.tags.remove("green")
+ >>> apple.tags.all()
+ [<Tag: red>, <Tag: delicious>]
+ >>> Food.objects.filter(tags__name__in=["red"])
+ [<Food: apple>, <Food: cherry>]
+
+ Tags will show up for you automatically in forms and the admin.
+
+ ``django-taggit`` requires Django 1.4.5 or greater.
+
+ For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
+ development you can contact the
+ `mailinglist <http://groups.google.com/group/django-taggit>`_.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Framework :: Django
diff --git a/django_taggit.egg-info/SOURCES.txt b/django_taggit.egg-info/SOURCES.txt
new file mode 100644
index 0000000..559aef3
--- /dev/null
+++ b/django_taggit.egg-info/SOURCES.txt
@@ -0,0 +1,64 @@
+AUTHORS
+CHANGELOG.txt
+LICENSE
+MANIFEST.in
+README.rst
+runtests.py
+setup.cfg
+setup.py
+tox.ini
+django_taggit.egg-info/PKG-INFO
+django_taggit.egg-info/SOURCES.txt
+django_taggit.egg-info/dependency_links.txt
+django_taggit.egg-info/not-zip-safe
+django_taggit.egg-info/top_level.txt
+docs/Makefile
+docs/admin.txt
+docs/api.txt
+docs/changelog.txt
+docs/conf.py
+docs/custom_tagging.txt
+docs/external_apps.txt
+docs/forms.txt
+docs/getting_started.txt
+docs/index.txt
+taggit/__init__.py
+taggit/admin.py
+taggit/forms.py
+taggit/managers.py
+taggit/models.py
+taggit/utils.py
+taggit/views.py
+taggit/locale/cs/LC_MESSAGES/django.mo
+taggit/locale/cs/LC_MESSAGES/django.po
+taggit/locale/de/LC_MESSAGES/django.mo
+taggit/locale/de/LC_MESSAGES/django.po
+taggit/locale/en/LC_MESSAGES/django.po
+taggit/locale/eo/LC_MESSAGES/django.mo
+taggit/locale/eo/LC_MESSAGES/django.po
+taggit/locale/he/LC_MESSAGES/django.mo
+taggit/locale/he/LC_MESSAGES/django.po
+taggit/locale/it/LC_MESSAGES/django.mo
+taggit/locale/it/LC_MESSAGES/django.po
+taggit/locale/ja/LC_MESSAGES/django.mo
+taggit/locale/ja/LC_MESSAGES/django.po
+taggit/locale/nb/LC_MESSAGES/django.mo
+taggit/locale/nb/LC_MESSAGES/django.po
+taggit/locale/nl/LC_MESSAGES/django.mo
+taggit/locale/nl/LC_MESSAGES/django.po
+taggit/locale/pt_BR/LC_MESSAGES/django.mo
+taggit/locale/pt_BR/LC_MESSAGES/django.po
+taggit/locale/ru/LC_MESSAGES/django.mo
+taggit/locale/ru/LC_MESSAGES/django.po
+taggit/migrations/0001_initial.py
+taggit/migrations/0002_auto_20150616_2121.py
+taggit/migrations/__init__.py
+taggit/south_migrations/0001_initial.py
+taggit/south_migrations/0002_unique_tagnames.py
+taggit/south_migrations/__init__.py
+tests/__init__.py
+tests/forms.py
+tests/models.py
+tests/tests.py
+tests/migrations/0001_initial.py
+tests/migrations/__init__.py \ No newline at end of file
diff --git a/django_taggit.egg-info/dependency_links.txt b/django_taggit.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/django_taggit.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/django_taggit.egg-info/not-zip-safe b/django_taggit.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/django_taggit.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/django_taggit.egg-info/top_level.txt b/django_taggit.egg-info/top_level.txt
new file mode 100644
index 0000000..ecae4ce
--- /dev/null
+++ b/django_taggit.egg-info/top_level.txt
@@ -0,0 +1 @@
+taggit
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..3374c0a
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+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 " 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 " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+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-taggit.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-taggit.qhc"
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+ "run these through (pdf)latex."
+
+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."
diff --git a/docs/admin.txt b/docs/admin.txt
new file mode 100644
index 0000000..35f3c71
--- /dev/null
+++ b/docs/admin.txt
@@ -0,0 +1,14 @@
+Using tags in the admin
+=======================
+
+By default if you have a :class:`TaggableManager` on your model it will show up
+in the admin, just as it will in any other form. One important thing to note
+is that you *cannot* include a :class:`TaggableManager` in
+:attr:`ModelAdmin.list_display`, if you do you'll see an exception that looks
+like::
+
+ AttributeError: 'TaggableManager' object has no attribute 'flatchoices'
+
+This is for the same reason that you cannot include a :class:`ManyToManyField`,
+it would result in an unreasonable number of queries being executed. If you really would like to add it, you can read the
+`Django documentation <http://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display>`_.
diff --git a/docs/api.txt b/docs/api.txt
new file mode 100644
index 0000000..ff6b7c1
--- /dev/null
+++ b/docs/api.txt
@@ -0,0 +1,114 @@
+The API
+=======
+
+After you've got your ``TaggableManager`` added to your model you can start
+playing around with the API.
+
+.. class:: TaggableManager([verbose_name="Tags", help_text="A comma-separated list of tags.", through=None, blank=False])
+
+ :param verbose_name: The verbose_name for this field.
+ :param help_text: The help_text to be used in forms (including the admin).
+ :param through: The through model, see :doc:`custom_tagging` for more
+ information.
+ :param blank: Controls whether this field is required.
+
+ .. method:: add(*tags)
+
+ This adds tags to an object. The tags can be either ``Tag`` instances, or
+ strings::
+
+ >>> apple.tags.all()
+ []
+ >>> apple.tags.add("red", "green", "fruit")
+
+ .. method:: remove(*tags)
+
+ Removes a tag from an object. No exception is raised if the object
+ doesn't have that tag.
+
+ .. method:: clear()
+
+ Removes all tags from an object.
+
+ .. method:: set(*tags)
+
+ Removes all the current tags and then adds the specified tags to the
+ object.
+
+ .. method: most_common()
+
+ Returns a ``QuerySet`` of all tags, annotated with the number of times
+ they appear, available as the ``num_times`` attribute on each tag. The
+ ``QuerySet``is ordered by ``num_times``, descending. The ``QuerySet``
+ is lazily evaluated, and can be sliced efficiently.
+
+ .. method:: similar_objects()
+
+ Returns a list (not a lazy ``QuerySet``) of other objects tagged
+ similarly to this one, ordered with most similar first. Each object in
+ the list is decorated with a ``similar_tags`` attribute, the number of
+ tags it shares with this object.
+
+ If the model is using generic tagging (the default), this method
+ searches tagged objects from all classes. If you are querying on a
+ model with its own tagging through table, only other instances of the
+ same model will be returned.
+
+ .. method:: names()
+
+ Convenience method, returning a ``ValuesListQuerySet`` (basically
+ just an iterable) containing the name of each tag as a string::
+
+ >>> apple.tags.names()
+ [u'green and juicy', u'red']
+
+ .. method:: slugs()
+
+ Convenience method, returning a ``ValuesListQuerySet`` (basically
+ just an iterable) containing the slug of each tag as a string::
+
+ >>> apple.tags.slugs()
+ [u'green-and-juicy', u'red']
+
+ .. hint::
+
+ You can subclass ``_TaggableManager`` (note the underscore) to add
+ methods or functionality. ``TaggableManager`` takes an optional
+ manager keyword argument for your custom class, like this::
+
+ class Food(models.Model):
+ # ... fields here
+ tags = TaggableManager(manager=_CustomTaggableManager)
+
+Filtering
+~~~~~~~~~
+
+To find all of a model with a specific tags you can filter, using the normal
+Django ORM API. For example if you had a ``Food`` model, whose
+``TaggableManager`` was named ``tags``, you could find all the delicious fruit
+like so::
+
+ >>> Food.objects.filter(tags__name__in=["delicious"])
+ [<Food: apple>, <Food: pear>, <Food: plum>]
+
+
+If you're filtering on multiple tags, it's very common to get duplicate
+results, because of the way relational databases work. Often you'll want to
+make use of the ``distinct()`` method on ``QuerySets``::
+
+ >>> Food.objects.filter(tags__name__in=["delicious", "red"])
+ [<Food: apple>, <Food: apple>]
+ >>> Food.objects.filter(tags__name__in=["delicious", "red"]).distinct()
+ [<Food: apple>]
+
+You can also filter by the slug on tags. If you're using a custom ``Tag``
+model you can use this API to filter on any fields it has.
+
+Aggregation
+~~~~~~~~~~~
+
+Unfortunately, due to a
+`bug in Django <http://code.djangoproject.com/ticket/10870>`_, it is not
+currently (Django < 1.6) possible to use aggregation in conjunction with ``taggit``. This is
+a `documented interaction <http://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations-and-aggregation>`_
+of generic relations (which ``taggit`` uses internally) and aggregates.
diff --git a/docs/changelog.txt b/docs/changelog.txt
new file mode 100644
index 0000000..7c4c0b0
--- /dev/null
+++ b/docs/changelog.txt
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.txt
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..53763ce
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+#
+# django-taggit documentation build configuration file, created by
+# sphinx-quickstart on Mon May 3 22:22:47 2010.
+#
+# 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 sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# 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.intersphinx']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'django-taggit'
+copyright = u'2010-2014, Alex Gaynor and others.'
+
+# 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 short X.Y version.
+version = '0.12'
+# The full version, including alpha/beta/rc tags.
+release = '0.12'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#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 documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_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 = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+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']
+
+# 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_use_modindex = 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, 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 = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'django-taggitdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'django-taggit.tex', u'django-taggit Documentation',
+ u'Alex Gaynor', '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
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/docs/custom_tagging.txt b/docs/custom_tagging.txt
new file mode 100644
index 0000000..87ff286
--- /dev/null
+++ b/docs/custom_tagging.txt
@@ -0,0 +1,167 @@
+Using a Custom Tag or Through Model
+===================================
+
+By default ``django-taggit`` uses a "through model" with a
+``GenericForeignKey`` on it, that has another ``ForeignKey`` to an included
+``Tag`` model. However, there are some cases where this isn't desirable, for
+example if you want the speed and referential guarantees of a real
+``ForeignKey``, if you have a model with a non-integer primary key, or if you
+want to store additional data about a tag, such as whether it is official. In
+these cases ``django-taggit`` makes it easy to substitute your own through
+model, or ``Tag`` model.
+
+To change the behavior there are a number of classes you can subclass to obtain
+different behavior:
+
+=============================== =======================================================================
+Class name Behavior
+=============================== =======================================================================
+``TaggedItemBase`` Allows custom ``ForeignKeys`` to models.
+``GenericTaggedItemBase`` Allows custom ``Tag`` models. Tagged models use an integer primary key.
+``GenericUUIDTaggedItemBase`` Allows custom ``Tag`` models. Tagged models use a UUID primary key.
+``CommonGenericTaggedItemBase`` Allows custom ``Tag`` models and ``GenericForeignKeys`` to models.
+``ItemBase`` Allows custom ``Tag`` models and ``ForeignKeys`` to models.
+=============================== =======================================================================
+
+Custom ForeignKeys
+~~~~~~~~~~~~~~~~~~
+
+Your intermediary model must be a subclass of
+``taggit.models.TaggedItemBase`` with a foreign key to your content
+model named ``content_object``. Pass this intermediary model as the
+``through`` argument to ``TaggableManager``::
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+ from taggit.models import TaggedItemBase
+
+
+ class TaggedFood(TaggedItemBase):
+ content_object = models.ForeignKey('Food')
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager(through=TaggedFood)
+
+
+Once this is done, the API works the same as for GFK-tagged models.
+
+Custom GenericForeignKeys
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The default ``GenericForeignKey`` used by ``django-taggit`` assume your
+tagged object use an integer primary key. For non-integer primary key,
+your intermediary model must be a subclass of ``taggit.models.CommonGenericTaggedItemBase``
+with a field named ``"object_id"`` of the type of your primary key.
+
+For example, if your primary key is a string::
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+ from taggit.models import CommonGenericTaggedItemBase, TaggedItemBase
+
+ class GenericStringTaggedItem(CommonGenericTaggedItemBase, TaggedItemBase):
+ object_id = models.CharField(max_length=50, verbose_name=_('Object id'), db_index=True)
+
+ class Food(models.Model):
+ food_id = models.CharField(primary_key=True)
+ # ... fields here
+
+ tags = TaggableManager(through=GenericStringTaggedItem)
+
+GenericUUIDTaggedItemBase
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note::
+
+ ``GenericUUIDTaggedItemBase`` relies on Django UUIDField introduced with
+ Django 1.8. Therefore ``GenericUUIDTaggedItemBase`` is only defined
+ if you are using Django 1.8+.
+
+A common use case of a non-integer primary key, is UUID primary key.
+``django-taggit`` provides a base class ``GenericUUIDTaggedItemBase`` ready
+to use with models using an UUID primary key::
+
+ from django.db import models
+ from django.utils.translation import ugettext_lazy as _
+
+ from taggit.managers import TaggableManager
+ from taggit.models import GenericUUIDTaggedItemBase, TaggedItemBase
+
+ class UUIDTaggedItem(GenericUUIDTaggedItemBase, TaggedItemBase):
+ # If you only inherit GenericUUIDTaggedItemBase, you need to define
+ # a tag field. e.g.
+ # tag = models.ForeignKey(Tag, related_name="uuid_tagged_items", on_delete=models.CASCADE)
+
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+ class Food(models.Model):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+ # ... fields here
+
+ tags = TaggableManager(through=UUIDTaggedItem)
+
+Custom tag
+~~~~~~~~~~
+
+When providing a custom ``Tag`` model it should be a ``ForeignKey`` to your tag
+model named ``"tag"``:
+
+ .. code-block:: python
+
+ from django.db import models
+ from django.utils.translation import ugettext_lazy as _
+
+ from taggit.managers import TaggableManager
+ from taggit.models import TagBase, GenericTaggedItemBase
+
+
+ class MyCustomTag(TagBase):
+ # ... fields here
+
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+ # ... methods (if any) here
+
+
+ class TaggedWhatever(GenericTaggedItemBase):
+ # TaggedWhatever can also extend TaggedItemBase or a combination of
+ # both TaggedItemBase and GenericTaggedItemBase. GenericTaggedItemBase
+ # allows using the same tag for different kinds of objects, in this
+ # example Food and Drink.
+
+ # Here is where you provide your custom Tag class.
+ tag = models.ForeignKey(MyCustomTag,
+ related_name="%(app_label)s_%(class)s_items")
+
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager(through=TaggedWhatever)
+
+
+ class Drink(models.Model):
+ # ... fields here
+
+ tags = TaggableManager(through=TaggedWhatever)
+
+
+.. class:: TagBase
+
+ .. method:: slugify(tag, i=None)
+
+ By default ``taggit`` uses :func:`django.template.defaultfilters.slugify`
+ to calculate a slug for a given tag. However, if you want to implement
+ your own logic you can override this method, which receives the ``tag``
+ (a string), and ``i``, which is either ``None`` or an integer, which
+ signifies how many times the slug for this tag has been attempted to be
+ calculated, it is ``None`` on the first time, and the counting begins
+ at ``1`` thereafter.
diff --git a/docs/external_apps.txt b/docs/external_apps.txt
new file mode 100644
index 0000000..1d29d53
--- /dev/null
+++ b/docs/external_apps.txt
@@ -0,0 +1,34 @@
+External Applications
+=====================
+
+In addition to the features included in ``django-taggit`` directly, there are a
+number of external applications which provide additional features that may be
+of interest.
+
+.. note::
+
+ Despite their mention here, the following applications are in no way
+ official, nor have they in any way been reviewed or tested.
+
+
+If you have an application that you'd like to see listed here, simply fork
+``taggit`` on `github`__, add it to this list, and send a pull request.
+
+ * ``django-taggit-suggest``: Provides support for defining keyword and regular
+ expression rules for suggesting new tags for content. This used to be
+ available at ``taggit.contrib.suggest``. Available on `github`__.
+ * ``django-taggit-templatetags``: Provides several templatetags, including one
+ for tag clouds, to expose various ``taggit`` APIs directly to templates.
+ Available on `github`__.
+ * ``django-taggit-helpers``: Makes it easier to work with admin pages of models
+ associated with ``taggit`` tags by adding helper classes: ``TaggitCounter``,
+ ``TaggitListFilter``, ``TaggitStackedInline``, ``TaggitTabularInline``.
+ Available on `github`__.
+ * ``django-taggit-labels``: Provides a clickable label widget for the
+ Django admin for user friendly selection from managed tag sets.
+
+__ http://github.com/alex/django-taggit
+__ http://github.com/frankwiles/django-taggit-suggest
+__ http://github.com/feuervogel/django-taggit-templatetags
+__ http://github.com/mfcovington/django-taggit-helpers
+__ https://github.com/bennylope/django-taggit-labels
diff --git a/docs/forms.txt b/docs/forms.txt
new file mode 100644
index 0000000..f364c13
--- /dev/null
+++ b/docs/forms.txt
@@ -0,0 +1,51 @@
+.. _tags-in-forms:
+
+Tags in forms
+=============
+
+The ``TaggableManager`` will show up automatically as a field in a
+``ModelForm`` or in the admin. Tags input via the form field are parsed
+as follows:
+
+* If the input doesn't contain any commas or double quotes, it is simply
+ treated as a space-delimited list of tag names.
+
+* If the input does contain either of these characters:
+
+ * Groups of characters which appear between double quotes take
+ precedence as multi-word tags (so double quoted tag names may
+ contain commas). An unclosed double quote will be ignored.
+
+ * Otherwise, if there are any unquoted commas in the input, it will
+ be treated as comma-delimited. If not, it will be treated as
+ space-delimited.
+
+Examples:
+
+====================== ================================= ================================================
+Tag input string Resulting tags Notes
+====================== ================================= ================================================
+apple ball cat ``["apple", "ball", "cat"]`` No commas, so space delimited
+apple, ball cat ``["apple", "ball cat"]`` Comma present, so comma delimited
+"apple, ball" cat dog ``["apple, ball", "cat", "dog"]`` All commas are quoted, so space delimited
+"apple, ball", cat dog ``["apple, ball", "cat dog"]`` Contains an unquoted comma, so comma delimited
+apple "ball cat" dog ``["apple", "ball cat", "dog"]`` No commas, so space delimited
+"apple" "ball dog ``["apple", "ball", "dog"]`` Unclosed double quote is ignored
+====================== ================================= ================================================
+
+
+``commit=False``
+~~~~~~~~~~~~~~~~
+
+If, when saving a form, you use the ``commit=False`` option you'll need to call
+``save_m2m()`` on the form after you save the object, just as you would for a
+form with normal many to many fields on it::
+
+ if request.method == "POST":
+ form = MyFormClass(request.POST)
+ if form.is_valid():
+ obj = form.save(commit=False)
+ obj.user = request.user
+ obj.save()
+ # Without this next line the tags won't be saved.
+ form.save_m2m()
diff --git a/docs/getting_started.txt b/docs/getting_started.txt
new file mode 100644
index 0000000..b7903cb
--- /dev/null
+++ b/docs/getting_started.txt
@@ -0,0 +1,39 @@
+Getting Started
+===============
+
+To get started using ``django-taggit`` simply install it with
+``pip``::
+
+ $ pip install django-taggit
+
+
+Add ``"taggit"`` to your project's ``INSTALLED_APPS`` setting.
+
+Run `./manage.py syncdb` or `./manage.py migrate` if using migrations.
+
+.. note::
+
+ If you are using South you'll have to add the following setting, since
+ taggit uses Django migrations by default::
+
+ SOUTH_MIGRATION_MODULES = {
+ 'taggit': 'taggit.south_migrations',
+ }
+
+And then to any model you want tagging on do the following::
+
+ from django.db import models
+
+ from taggit.managers import TaggableManager
+
+ class Food(models.Model):
+ # ... fields here
+
+ tags = TaggableManager()
+
+.. note::
+
+ If you want ``django-taggit`` to be **CASE INSENSITIVE** when looking up existing tags, you'll have to set to ``True`` the TAGGIT_CASE_INSENSITIVE setting (by default ``False``)::
+
+ TAGGIT_CASE_INSENSITIVE = True
+
diff --git a/docs/index.txt b/docs/index.txt
new file mode 100644
index 0000000..bc8d077
--- /dev/null
+++ b/docs/index.txt
@@ -0,0 +1,45 @@
+Welcome to django-taggit's documentation!
+=========================================
+
+``django-taggit`` is a reusable Django application designed to making adding
+tagging to your project easy and fun.
+
+``django-taggit`` works with Django 1.4.5+ and Python 2.7-3.X.
+
+.. warning::
+
+ Since version 0.10.0 taggit uses South for database migrations.
+ This means that users who are upgrading to 0.10.0 and up will have to fake
+ the initial migration, like this::
+
+ python manage.py migrate taggit --fake 0001
+ python manage.py migrate
+
+ Since version 0.12.0 taggit uses Django migrations by default. South users
+ have to adjust their settings::
+
+ SOUTH_MIGRATION_MODULES = {
+ 'taggit': 'taggit.south_migrations',
+ }
+
+ For more information, see `south documentation`__
+
+.. toctree::
+ :maxdepth: 2
+
+ getting_started
+ forms
+ admin
+ api
+ custom_tagging
+ external_apps
+ changelog
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+__ http://south.readthedocs.org/en/latest/
diff --git a/runtests.py b/runtests.py
new file mode 100755
index 0000000..517dd80
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+import os
+import sys
+
+from django.conf import settings
+from django.core.management import execute_from_command_line
+
+
+if not settings.configured:
+ settings.configure(
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ }
+ },
+ INSTALLED_APPS=[
+ 'django.contrib.contenttypes',
+ 'taggit',
+ 'tests',
+ ],
+ MIDDLEWARE_CLASSES=[],
+ )
+
+
+def runtests():
+ argv = sys.argv[:1] + ['test'] + sys.argv[1:]
+ execute_from_command_line(argv)
+
+
+if __name__ == '__main__':
+ runtests()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..dd960e1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,18 @@
+[metadata]
+license-file = LICENSE
+
+[wheel]
+universal = 1
+
+[flake8]
+ignore = E501,E302
+exclude = south_migrations,migrations
+
+[isort]
+forced_separate = tests,taggit
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..cb953af
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,41 @@
+from setuptools import setup, find_packages
+
+import taggit
+
+
+with open('README.rst') as f:
+ readme = f.read()
+
+setup(
+ name='django-taggit',
+ version='.'.join(str(i) for i in taggit.VERSION),
+ description='django-taggit is a reusable Django application for simple tagging.',
+ long_description=readme,
+ author='Alex Gaynor',
+ author_email='alex.gaynor@gmail.com',
+ url='http://github.com/alex/django-taggit/tree/master',
+ packages=find_packages(exclude=('tests*',)),
+ package_data = {
+ 'taggit': [
+ 'locale/*/LC_MESSAGES/*',
+ ],
+ },
+ license='BSD',
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Framework :: Django',
+ ],
+ include_package_data=True,
+ zip_safe=False,
+)
diff --git a/taggit/__init__.py b/taggit/__init__.py
new file mode 100644
index 0000000..fb92fdc
--- /dev/null
+++ b/taggit/__init__.py
@@ -0,0 +1 @@
+VERSION = (0, 17, 4)
diff --git a/taggit/admin.py b/taggit/admin.py
new file mode 100644
index 0000000..0498c9d
--- /dev/null
+++ b/taggit/admin.py
@@ -0,0 +1,21 @@
+from __future__ import unicode_literals
+
+from django.contrib import admin
+
+from taggit.models import Tag, TaggedItem
+
+
+class TaggedItemInline(admin.StackedInline):
+ model = TaggedItem
+
+class TagAdmin(admin.ModelAdmin):
+ inlines = [
+ TaggedItemInline
+ ]
+ list_display = ["name", "slug"]
+ ordering = ["name", "slug"]
+ search_fields = ["name"]
+ prepopulated_fields = {"slug": ["name"]}
+
+
+admin.site.register(Tag, TagAdmin)
diff --git a/taggit/forms.py b/taggit/forms.py
new file mode 100644
index 0000000..88ac842
--- /dev/null
+++ b/taggit/forms.py
@@ -0,0 +1,27 @@
+from __future__ import unicode_literals
+
+from django import forms
+from django.utils import six
+from django.utils.translation import ugettext as _
+
+from taggit.utils import edit_string_for_tags, parse_tags
+
+
+class TagWidget(forms.TextInput):
+ def render(self, name, value, attrs=None):
+ if value is not None and not isinstance(value, six.string_types):
+ value = edit_string_for_tags([
+ o.tag for o in value.select_related("tag")])
+ return super(TagWidget, self).render(name, value, attrs)
+
+
+class TagField(forms.CharField):
+ widget = TagWidget
+
+ def clean(self, value):
+ value = super(TagField, self).clean(value)
+ try:
+ return parse_tags(value)
+ except ValueError:
+ raise forms.ValidationError(
+ _("Please provide a comma-separated list of tags."))
diff --git a/taggit/locale/cs/LC_MESSAGES/django.mo b/taggit/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..9ce13fb
--- /dev/null
+++ b/taggit/locale/cs/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/cs/LC_MESSAGES/django.po b/taggit/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000..13262e1
--- /dev/null
+++ b/taggit/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,64 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-01 16:52+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: forms.py:24
+msgid "Please provide a comma-separated list of tags."
+msgstr "Vložte čárkami oddělený seznam tagů"
+
+#: managers.py:59 models.py:59
+msgid "Tags"
+msgstr "Tagy"
+
+#: managers.py:60
+msgid "A comma-separated list of tags."
+msgstr "Čárkami oddělený seznam tagů"
+
+#: models.py:15
+msgid "Name"
+msgstr "Jméno"
+
+#: models.py:16
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:58
+msgid "Tag"
+msgstr "Tag"
+
+#: models.py:65
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s označen tagem %(tag)s"
+
+#: models.py:112
+msgid "Object id"
+msgstr "ID objektu"
+
+#: models.py:115
+msgid "Content type"
+msgstr "Typ obsahu"
+
+#: models.py:158
+msgid "Tagged Item"
+msgstr "Tagem označená položka"
+
+#: models.py:159
+msgid "Tagged Items"
+msgstr "Tagy označené položky"
diff --git a/taggit/locale/de/LC_MESSAGES/django.mo b/taggit/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..3d9eaaf
--- /dev/null
+++ b/taggit/locale/de/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/de/LC_MESSAGES/django.po b/taggit/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..98ecdac
--- /dev/null
+++ b/taggit/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:26-0700\n"
+"PO-Revision-Date: 2010-09-07 09:26-0700\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: German <de@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Bitte eine durch Komma getrennte Schlagwortliste eingeben."
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "Schlagwörter"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Eine durch Komma getrennte Schlagwortliste."
+
+#: models.py:10
+msgid "Name"
+msgstr "Name"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Kürzel"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Schlagwort"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s verschlagwortet mit %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Objekt-ID"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "Inhaltstyp"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Verschlagwortetes Objekt"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Verschlagwortete Objekte"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
+"\" in your expression."
+msgstr ""
+"Bitte einen regulären Ausdruck eingeben. Fügen Sie \"(?i) \" dem "
+"Ausdruck hinzu, um nicht zwischen Groß- und Kleinschreibung zu "
+"unterscheiden."
diff --git a/taggit/locale/en/LC_MESSAGES/django.po b/taggit/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c5642c7
--- /dev/null
+++ b/taggit/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,68 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:45-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr ""
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr ""
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr ""
+
+#: models.py:10
+msgid "Name"
+msgstr ""
+
+#: models.py:11
+msgid "Slug"
+msgstr ""
+
+#: models.py:49
+msgid "Tag"
+msgstr ""
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr ""
+
+#: models.py:100
+msgid "Object id"
+msgstr ""
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr ""
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr ""
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr ""
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
+"\" in your expression."
+msgstr ""
diff --git a/taggit/locale/eo/LC_MESSAGES/django.mo b/taggit/locale/eo/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b81e262
--- /dev/null
+++ b/taggit/locale/eo/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/eo/LC_MESSAGES/django.po b/taggit/locale/eo/LC_MESSAGES/django.po
new file mode 100644
index 0000000..b1a0792
--- /dev/null
+++ b/taggit/locale/eo/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:26-0700\n"
+"PO-Revision-Date: 2014-03-29 18:57+0100\n"
+"Last-Translator: Baptiste Darthenay <taggit.batisteo@recursor.net>\n"
+"Language-Team: Esperanto <taggit.batisteo@recursor.net>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: eo\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Bonvolu enmeti liston da etikedoj apartitaj per komoj."
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "Etikedoj"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Listo da etikedoj apartitaj per komoj."
+
+#: models.py:10
+msgid "Name"
+msgstr "Nomo"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Ĵetonvorto"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Etikedo"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s etikedita %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Objekto ID"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "Enhavtipo"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Etikedita elemento"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Etikeditaj elementoj"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
+"i)\" in your expression."
+msgstr ""
+"Entajpu validan regulan esprimon. Por ke estu usklecoblinda, enmetu \"(?i)\" "
+"en via esprimo."
diff --git a/taggit/locale/he/LC_MESSAGES/django.mo b/taggit/locale/he/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..562db71
--- /dev/null
+++ b/taggit/locale/he/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/he/LC_MESSAGES/django.po b/taggit/locale/he/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6d2246a
--- /dev/null
+++ b/taggit/locale/he/LC_MESSAGES/django.po
@@ -0,0 +1,68 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Django Taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-06-26 12:47-0500\n"
+"PO-Revision-Date: 2010-06-26 12:54-0600\n"
+"Last-Translator: Alex <alex.gaynor@gmail.com>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "נא לספק רשימה של תגים מופרדת עם פסיקים."
+
+#: managers.py:41
+#: managers.py:113
+#: models.py:18
+msgid "Tags"
+msgstr "תגיות"
+
+#: managers.py:114
+msgid "A comma-separated list of tags."
+msgstr "רשימה של תגים מופרדת עם פסיקים."
+
+#: models.py:10
+msgid "Name"
+msgstr "שם"
+
+#: models.py:11
+msgid "Slug"
+msgstr ""
+
+#: models.py:17
+msgid "Tag"
+msgstr "תג"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s מתויג עם %(tag)s"
+
+#: models.py:86
+msgid "Object id"
+msgstr ""
+
+#: models.py:87
+msgid "Content type"
+msgstr ""
+
+#: models.py:92
+msgid "Tagged Item"
+msgstr ""
+
+#: models.py:93
+msgid "Tagged Items"
+msgstr ""
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr ""
diff --git a/taggit/locale/it/LC_MESSAGES/django.mo b/taggit/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..9c7a317
--- /dev/null
+++ b/taggit/locale/it/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/it/LC_MESSAGES/django.po b/taggit/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6b15590
--- /dev/null
+++ b/taggit/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,70 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-04-13 15:57+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Fornire una lista di tag separati da virgola."
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "Tag"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Una lista di tag separati da virgola."
+
+#: models.py:10
+msgid "Name"
+msgstr "Nome"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Tag"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s con tag %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Id Oggetto"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "Tipo"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Oggetto con tag"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Oggetti con tag"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
+"\" in your expression."
+msgstr ""
+"Inserire un'espressione regolare valida. Per renderla indipendente dal maiuscolo e minuscolo, includere \"(?i)"
+"\" nell'espressione."
diff --git a/taggit/locale/ja/LC_MESSAGES/django.mo b/taggit/locale/ja/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..6b7adae
--- /dev/null
+++ b/taggit/locale/ja/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/ja/LC_MESSAGES/django.po b/taggit/locale/ja/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6d042d1
--- /dev/null
+++ b/taggit/locale/ja/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:26-0700\n"
+"PO-Revision-Date: 2014-04-23 08:05+0900\n"
+"Last-Translator: Tatsuo Ikeda <jp.ne.co.jp@gmail.com>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: ja\n"
+"X-Generator: Poedit 1.6.4\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "複数タグはカンマ区切りのリストを入れてください。"
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "タグ一覧"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "複数タグはカンマ区切りのリスト。"
+
+#: models.py:10
+msgid "Name"
+msgstr "名称"
+
+#: models.py:11
+msgid "Slug"
+msgstr "スラッグ"
+
+#: models.py:49
+msgid "Tag"
+msgstr "タグ"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s tagged with %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "オブジェクト ID"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "コンテンツタイプ"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "タグ付け済みのアイテム"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "タグ付け済みのアイテム一覧"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
+"i)\" in your expression."
+msgstr ""
+"正しい正規表現を入力してください。 大文字と小文字を区別しないようにするには "
+"\"(?i)\" を正規表現に含めてください。"
diff --git a/taggit/locale/nb/LC_MESSAGES/django.mo b/taggit/locale/nb/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..237612d
--- /dev/null
+++ b/taggit/locale/nb/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/nb/LC_MESSAGES/django.po b/taggit/locale/nb/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a1ba128
--- /dev/null
+++ b/taggit/locale/nb/LC_MESSAGES/django.po
@@ -0,0 +1,72 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.9.3\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:45-0700\n"
+"PO-Revision-Date: 2012-12-08 14:42+0100\n"
+"Last-Translator: Bjørn Pettersen <bp@datakortet.no>\n"
+"Language-Team: Norwegian <bp@datakortet.no>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.5.4\n"
+"Language: Norwegian\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Vennligst oppgi en kommaseparert tagg-liste."
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "Tagger"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "En kommaseparert tagg-liste."
+
+#: models.py:10
+msgid "Name"
+msgstr "Navn"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Tagg"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s tagget med %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Objekt-id"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "Innholdstype"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Tagget Element"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Taggede Elementer"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
+"i)\" in your expression."
+msgstr ""
+"Skriv et gyldig regulært utrykk (regex). For å gjøre det uavhengig av "
+"forskjellen mellom store og små bokstaver må du inkludere \"(?i)\" i din "
+"regex."
diff --git a/taggit/locale/nl/LC_MESSAGES/django.mo b/taggit/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..28e7b7e
--- /dev/null
+++ b/taggit/locale/nl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/nl/LC_MESSAGES/django.po b/taggit/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..447ca7d
--- /dev/null
+++ b/taggit/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,63 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:45-0700\n"
+"PO-Revision-Date: 2010-09-07 23:04+0100\n"
+"Last-Translator: Jeffrey Gelens <jeffrey@gelens.org>\n"
+"Language-Team: Dutch\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Geef een door komma gescheiden lijst van tags."
+
+#: managers.py:39
+#: managers.py:83
+#: models.py:50
+msgid "Tags"
+msgstr "Tags"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Een door komma gescheiden lijst van tags."
+
+#: models.py:10
+msgid "Name"
+msgstr "Naam"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Tag"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s getagged met %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Object-id"
+
+#: models.py:104
+#: models.py:110
+msgid "Content type"
+msgstr "Inhoudstype"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Object getagged"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Objecten getagged"
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr "Voer een valide reguliere expressie in. Voeg \"(?i)\" aan de expressie toe om deze hoofdletter ongevoelig te maken."
diff --git a/taggit/locale/pt_BR/LC_MESSAGES/django.mo b/taggit/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..c78bcb8
--- /dev/null
+++ b/taggit/locale/pt_BR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/pt_BR/LC_MESSAGES/django.po b/taggit/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8804bbf
--- /dev/null
+++ b/taggit/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,62 @@
+# This file is distributed under WTFPL license.
+#
+# Translators:
+# RPB <r@ifgy.co>, 2013.
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-01-15 22:25-0200\n"
+"PO-Revision-Date: 2013-01-12 18:11-0200\n"
+"Last-Translator: RPB <r@ifgy.co>\n"
+"Language-Team: Portuguese (Brazil) <r@ifgy.co>\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+
+#: forms.py:21
+msgid "Please provide a comma-separated list of tags."
+msgstr "Favor fornecer uma lista de marcadores separados por vírgula."
+
+#: managers.py:39 models.py:57
+msgid "Tags"
+msgstr "Marcadores"
+
+#: managers.py:40
+msgid "A comma-separated list of tags."
+msgstr "Uma lista de marcadores separados por vírgula."
+
+#: models.py:10
+msgid "Name"
+msgstr "Nome"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:56
+msgid "Tag"
+msgstr "Marcador"
+
+#: models.py:63
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s marcados com %(tag)s"
+
+#: models.py:113
+msgid "Object id"
+msgstr "Id do objeto"
+
+#: models.py:117 models.py:123
+msgid "Content type"
+msgstr "Tipo de conteúdo"
+
+#: models.py:159
+msgid "Tagged Item"
+msgstr "Item marcado"
+
+#: models.py:160
+msgid "Tagged Items"
+msgstr "Itens marcados"
diff --git a/taggit/locale/ru/LC_MESSAGES/django.mo b/taggit/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..61a7e39
--- /dev/null
+++ b/taggit/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/taggit/locale/ru/LC_MESSAGES/django.po b/taggit/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..6629cba
--- /dev/null
+++ b/taggit/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,69 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Django Taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-06-11 11:28+0700\n"
+"PO-Revision-Date: 2010-06-11 11:30+0700\n"
+"Last-Translator: Igor 'idle sign' Starikov <idlesign@yandex.ru>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Poedit-Language: Russian\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Укажите метки через запятую."
+
+#: managers.py:41
+#: managers.py:101
+#: models.py:17
+msgid "Tags"
+msgstr "Метки"
+
+#: managers.py:102
+msgid "A comma-separated list of tags."
+msgstr "Список меток через запятую."
+
+#: models.py:9
+msgid "Name"
+msgstr "Название"
+
+#: models.py:10
+msgid "Slug"
+msgstr "Слаг"
+
+#: models.py:16
+msgid "Tag"
+msgstr "Метка"
+
+#: models.py:55
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "элемент «%(object)s» с меткой «%(tag)s»"
+
+#: models.py:82
+msgid "Object id"
+msgstr "ID объекта"
+
+#: models.py:83
+msgid "Content type"
+msgstr "Тип содержимого"
+
+#: models.py:87
+msgid "Tagged Item"
+msgstr "Элемент с меткой"
+
+#: models.py:88
+msgid "Tagged Items"
+msgstr "Элементы с меткой"
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr "Введите регулярное выражение. Чтобы сделать его чувствительным к регистру укажите \"(?i)\"."
diff --git a/taggit/managers.py b/taggit/managers.py
new file mode 100644
index 0000000..350ab1c
--- /dev/null
+++ b/taggit/managers.py
@@ -0,0 +1,592 @@
+from __future__ import unicode_literals
+
+from operator import attrgetter
+
+from django import VERSION
+from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
+from django.db import models, router
+from django.db.models.fields import Field
+from django.db.models.fields.related import (add_lazy_relation, ManyToManyRel,
+ OneToOneRel, RelatedField)
+
+if VERSION < (1, 8):
+ # related.py was removed in Django 1.8
+
+ # Depending on how Django was updated, related.py could still exist
+ # on the users system even on Django 1.8+, so we check the Django
+ # version before importing it to make sure this doesn't get imported
+ # accidentally.
+ from django.db.models.related import RelatedObject
+else:
+ RelatedObject = None
+
+from django.utils import six
+from django.utils.text import capfirst
+from django.utils.translation import ugettext_lazy as _
+
+from taggit.forms import TagField
+from taggit.models import CommonGenericTaggedItemBase, TaggedItem
+from taggit.utils import _get_field, require_instance_manager
+
+try:
+ from django.contrib.contenttypes.fields import GenericRelation
+except ImportError: # django < 1.7
+ from django.contrib.contenttypes.generic import GenericRelation
+
+try:
+ from django.db.models.query_utils import PathInfo
+except ImportError: # Django < 1.8
+ try:
+ from django.db.models.related import PathInfo
+ except ImportError:
+ pass # PathInfo is not used on Django < 1.6
+
+
+def _model_name(model):
+ if VERSION < (1, 7):
+ return model._meta.module_name
+ else:
+ return model._meta.model_name
+
+
+class TaggableRel(ManyToManyRel):
+ def __init__(self, field, related_name, through, to=None):
+ # rel.to renamed to rel.model in Django 1.9
+ if VERSION >= (1, 9):
+ self.model = to
+ else:
+ self.to = to
+ self.related_name = related_name
+ self.limit_choices_to = {}
+ self.symmetrical = True
+ self.multiple = True
+ self.through = None if VERSION < (1, 7) else through
+ self.field = field
+ self.through_fields = None
+
+ def get_joining_columns(self):
+ return self.field.get_reverse_joining_columns()
+
+ def get_extra_restriction(self, where_class, alias, related_alias):
+ return self.field.get_extra_restriction(where_class, related_alias, alias)
+
+
+class ExtraJoinRestriction(object):
+ """
+ An extra restriction used for contenttype restriction in joins.
+ """
+ contains_aggregate = False
+
+ def __init__(self, alias, col, content_types):
+ self.alias = alias
+ self.col = col
+ self.content_types = content_types
+
+ def as_sql(self, qn, connection):
+ if len(self.content_types) == 1:
+ extra_where = "%s.%s = %%s" % (qn(self.alias), qn(self.col))
+ else:
+ extra_where = "%s.%s IN (%s)" % (qn(self.alias), qn(self.col),
+ ','.join(['%s'] * len(self.content_types)))
+ return extra_where, self.content_types
+
+ def relabel_aliases(self, change_map):
+ self.alias = change_map.get(self.alias, self.alias)
+
+ def clone(self):
+ return self.__class__(self.alias, self.col, self.content_types[:])
+
+
+class _TaggableManager(models.Manager):
+ def __init__(self, through, model, instance, prefetch_cache_name):
+ self.through = through
+ self.model = model
+ self.instance = instance
+ self.prefetch_cache_name = prefetch_cache_name
+ self._db = None
+
+ def is_cached(self, instance):
+ return self.prefetch_cache_name in instance._prefetched_objects_cache
+
+ def get_queryset(self, extra_filters=None):
+ try:
+ return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
+ except (AttributeError, KeyError):
+ kwargs = extra_filters if extra_filters else {}
+ return self.through.tags_for(self.model, self.instance, **kwargs)
+
+ def get_prefetch_queryset(self, instances, queryset=None):
+ if queryset is not None:
+ raise ValueError("Custom queryset can't be used for this lookup.")
+
+ instance = instances[0]
+ from django.db import connections
+ db = self._db or router.db_for_read(instance.__class__, instance=instance)
+
+ fieldname = ('object_id' if issubclass(self.through, CommonGenericTaggedItemBase)
+ else 'content_object')
+ fk = self.through._meta.get_field(fieldname)
+ query = {
+ '%s__%s__in' % (self.through.tag_relname(), fk.name):
+ set(obj._get_pk_val() for obj in instances)
+ }
+ join_table = self.through._meta.db_table
+ source_col = fk.column
+ connection = connections[db]
+ qn = connection.ops.quote_name
+ qs = self.get_queryset(query).using(db).extra(
+ select={
+ '_prefetch_related_val': '%s.%s' % (qn(join_table), qn(source_col))
+ }
+ )
+ return (qs,
+ attrgetter('_prefetch_related_val'),
+ lambda obj: obj._get_pk_val(),
+ False,
+ self.prefetch_cache_name)
+
+ # Django < 1.6 uses the previous name of query_set
+ get_query_set = get_queryset
+ get_prefetch_query_set = get_prefetch_queryset
+
+ def _lookup_kwargs(self):
+ return self.through.lookup_kwargs(self.instance)
+
+ @require_instance_manager
+ def add(self, *tags):
+ str_tags = set()
+ tag_objs = set()
+ for t in tags:
+ if isinstance(t, self.through.tag_model()):
+ tag_objs.add(t)
+ elif isinstance(t, six.string_types):
+ str_tags.add(t)
+ else:
+ raise ValueError("Cannot add {0} ({1}). Expected {2} or str.".format(
+ t, type(t), type(self.through.tag_model())))
+
+ if getattr(settings, 'TAGGIT_CASE_INSENSITIVE', False):
+ # Some databases can do case-insensitive comparison with IN, which
+ # would be faster, but we can't rely on it or easily detect it.
+ existing = []
+ tags_to_create = []
+
+ for name in str_tags:
+ try:
+ tag = self.through.tag_model().objects.get(name__iexact=name)
+ existing.append(tag)
+ except self.through.tag_model().DoesNotExist:
+ tags_to_create.append(name)
+ else:
+ # If str_tags has 0 elements Django actually optimizes that to not do a
+ # query. Malcolm is very smart.
+ existing = self.through.tag_model().objects.filter(
+ name__in=str_tags
+ )
+
+ tags_to_create = str_tags - set(t.name for t in existing)
+
+ tag_objs.update(existing)
+
+ for new_tag in tags_to_create:
+ tag_objs.add(self.through.tag_model().objects.create(name=new_tag))
+
+ for tag in tag_objs:
+ self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs())
+
+ @require_instance_manager
+ def names(self):
+ return self.get_queryset().values_list('name', flat=True)
+
+ @require_instance_manager
+ def slugs(self):
+ return self.get_queryset().values_list('slug', flat=True)
+
+ @require_instance_manager
+ def set(self, *tags):
+ self.clear()
+ self.add(*tags)
+
+ @require_instance_manager
+ def remove(self, *tags):
+ self.through.objects.filter(**self._lookup_kwargs()).filter(
+ tag__name__in=tags).delete()
+
+ @require_instance_manager
+ def clear(self):
+ self.through.objects.filter(**self._lookup_kwargs()).delete()
+
+ def most_common(self):
+ return self.get_queryset().annotate(
+ num_times=models.Count(self.through.tag_relname())
+ ).order_by('-num_times')
+
+ @require_instance_manager
+ def similar_objects(self):
+ lookup_kwargs = self._lookup_kwargs()
+ lookup_keys = sorted(lookup_kwargs)
+ qs = self.through.objects.values(*six.iterkeys(lookup_kwargs))
+ qs = qs.annotate(n=models.Count('pk'))
+ qs = qs.exclude(**lookup_kwargs)
+ qs = qs.filter(tag__in=self.all())
+ qs = qs.order_by('-n')
+
+ # TODO: This all feels like a bit of a hack.
+ items = {}
+ if len(lookup_keys) == 1:
+ # Can we do this without a second query by using a select_related()
+ # somehow?
+ f = _get_field(self.through, lookup_keys[0])
+ rel_model = f.rel.model if VERSION >= (1, 9) else f.rel.to
+ objs = rel_model._default_manager.filter(**{
+ "%s__in" % f.rel.field_name: [r["content_object"] for r in qs]
+ })
+ for obj in objs:
+ items[(getattr(obj, f.rel.field_name),)] = obj
+ else:
+ preload = {}
+ for result in qs:
+ preload.setdefault(result['content_type'], set())
+ preload[result["content_type"]].add(result["object_id"])
+
+ for ct, obj_ids in preload.items():
+ ct = ContentType.objects.get_for_id(ct)
+ for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids):
+ items[(ct.pk, obj.pk)] = obj
+
+ results = []
+ for result in qs:
+ obj = items[
+ tuple(result[k] for k in lookup_keys)
+ ]
+ obj.similar_tags = result["n"]
+ results.append(obj)
+ return results
+
+ # _TaggableManager needs to be hashable but BaseManagers in Django 1.8+ overrides
+ # the __eq__ method which makes the default __hash__ method disappear.
+ # This checks if the __hash__ attribute is None, and if so, it reinstates the original method.
+ if models.Manager.__hash__ is None:
+ __hash__ = object.__hash__
+
+
+class TaggableManager(RelatedField, Field):
+ # Field flags
+ many_to_many = True
+ many_to_one = False
+ one_to_many = False
+ one_to_one = False
+
+ _related_name_counter = 0
+
+ def __init__(self, verbose_name=_("Tags"),
+ help_text=_("A comma-separated list of tags."),
+ through=None, blank=False, related_name=None, to=None,
+ manager=_TaggableManager):
+
+ self.through = through or TaggedItem
+ self.swappable = False
+ self.manager = manager
+
+ rel = TaggableRel(self, related_name, self.through, to=to)
+
+ Field.__init__(
+ self,
+ verbose_name=verbose_name,
+ help_text=help_text,
+ blank=blank,
+ null=True,
+ serialize=False,
+ rel=rel,
+ )
+ # NOTE: `to` is ignored, only used via `deconstruct`.
+
+ def __get__(self, instance, model):
+ if instance is not None and instance.pk is None:
+ raise ValueError("%s objects need to have a primary key value "
+ "before you can access their tags." % model.__name__)
+ manager = self.manager(
+ through=self.through,
+ model=model,
+ instance=instance,
+ prefetch_cache_name=self.name
+ )
+ return manager
+
+ def deconstruct(self):
+ """
+ Deconstruct the object, used with migrations.
+ """
+ name, path, args, kwargs = super(TaggableManager, self).deconstruct()
+ # Remove forced kwargs.
+ for kwarg in ('serialize', 'null'):
+ del kwargs[kwarg]
+ # Add arguments related to relations.
+ # Ref: https://github.com/alex/django-taggit/issues/206#issuecomment-37578676
+ if isinstance(self.rel.through, six.string_types):
+ kwargs['through'] = self.rel.through
+ elif not self.rel.through._meta.auto_created:
+ kwargs['through'] = "%s.%s" % (self.rel.through._meta.app_label, self.rel.through._meta.object_name)
+
+ # rel.to renamed to remote_field.model in Django 1.9
+ if VERSION >= (1, 9):
+ if isinstance(self.remote_field.model, six.string_types):
+ kwargs['to'] = self.remote_field.model
+ else:
+ kwargs['to'] = '%s.%s' % (self.remote_field.model._meta.app_label, self.remote_field.model._meta.object_name)
+ else:
+ if isinstance(self.rel.to, six.string_types):
+ kwargs['to'] = self.rel.to
+ else:
+ kwargs['to'] = '%s.%s' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
+
+ return name, path, args, kwargs
+
+ def contribute_to_class(self, cls, name):
+ if VERSION < (1, 7):
+ self.name = self.column = self.attname = name
+ else:
+ self.set_attributes_from_name(name)
+ self.model = cls
+
+ cls._meta.add_field(self)
+ setattr(cls, name, self)
+ if not cls._meta.abstract:
+ # rel.to renamed to remote_field.model in Django 1.9
+ if VERSION >= (1, 9):
+ if isinstance(self.remote_field.model, six.string_types):
+ def resolve_related_class(field, model, cls):
+ field.remote_field.model = model
+ add_lazy_relation(cls, self, self.remote_field.model, resolve_related_class)
+ else:
+ if isinstance(self.rel.to, six.string_types):
+ def resolve_related_class(field, model, cls):
+ field.rel.to = model
+ add_lazy_relation(cls, self, self.rel.to, resolve_related_class)
+
+ if isinstance(self.through, six.string_types):
+ def resolve_related_class(field, model, cls):
+ self.through = model
+ self.rel.through = model
+ self.post_through_setup(cls)
+ add_lazy_relation(
+ cls, self, self.through, resolve_related_class
+ )
+ else:
+ self.post_through_setup(cls)
+
+ def get_internal_type(self):
+ return 'ManyToManyField'
+
+ def __lt__(self, other):
+ """
+ Required contribute_to_class as Django uses bisect
+ for ordered class contribution and bisect requires
+ a orderable type in py3.
+ """
+ return False
+
+ def post_through_setup(self, cls):
+ if RelatedObject is not None: # Django < 1.8
+ self.related = RelatedObject(cls, self.model, self)
+
+ self.use_gfk = (
+ self.through is None or issubclass(self.through, CommonGenericTaggedItemBase)
+ )
+
+ # rel.to renamed to remote_field.model in Django 1.9
+ if VERSION >= (1, 9):
+ if not self.remote_field.model:
+ self.remote_field.model = self.through._meta.get_field("tag").remote_field.model
+ else:
+ if not self.rel.to:
+ self.rel.to = self.through._meta.get_field("tag").rel.to
+
+ if RelatedObject is not None: # Django < 1.8
+ self.related = RelatedObject(self.through, cls, self)
+
+ if self.use_gfk:
+ tagged_items = GenericRelation(self.through)
+ tagged_items.contribute_to_class(cls, 'tagged_items')
+
+ for rel in cls._meta.local_many_to_many:
+ if rel == self or not isinstance(rel, TaggableManager):
+ continue
+ if rel.through == self.through:
+ raise ValueError('You can\'t have two TaggableManagers with the'
+ ' same through model.')
+
+ def save_form_data(self, instance, value):
+ getattr(instance, self.name).set(*value)
+
+ def formfield(self, form_class=TagField, **kwargs):
+ defaults = {
+ "label": capfirst(self.verbose_name),
+ "help_text": self.help_text,
+ "required": not self.blank
+ }
+ defaults.update(kwargs)
+ return form_class(**defaults)
+
+ def value_from_object(self, instance):
+ if instance.pk:
+ return self.through.objects.filter(**self.through.lookup_kwargs(instance))
+ return self.through.objects.none()
+
+ def related_query_name(self):
+ return _model_name(self.model)
+
+ def m2m_reverse_name(self):
+ return _get_field(self.through, 'tag').column
+
+ def m2m_reverse_field_name(self):
+ return _get_field(self.through, 'tag').name
+
+ def m2m_target_field_name(self):
+ return self.model._meta.pk.name
+
+ def m2m_reverse_target_field_name(self):
+ # rel.to renamed to remote_field.model in Django 1.9
+ if VERSION >= (1, 9):
+ return self.remote_field.model._meta.pk.name
+ else:
+ return self.rel.to._meta.pk.name
+
+ def m2m_column_name(self):
+ if self.use_gfk:
+ return self.through._meta.virtual_fields[0].fk_field
+ return self.through._meta.get_field('content_object').column
+
+ def db_type(self, connection=None):
+ return None
+
+ def m2m_db_table(self):
+ return self.through._meta.db_table
+
+ def bulk_related_objects(self, new_objs, using):
+ return []
+
+ def extra_filters(self, pieces, pos, negate):
+ if negate or not self.use_gfk:
+ return []
+ prefix = "__".join(["tagged_items"] + pieces[:pos - 2])
+ get = ContentType.objects.get_for_model
+ cts = [get(obj) for obj in _get_subclasses(self.model)]
+ if len(cts) == 1:
+ return [("%s__content_type" % prefix, cts[0])]
+ return [("%s__content_type__in" % prefix, cts)]
+
+ def get_extra_join_sql(self, connection, qn, lhs_alias, rhs_alias):
+ model_name = _model_name(self.through)
+ if rhs_alias == '%s_%s' % (self.through._meta.app_label, model_name):
+ alias_to_join = rhs_alias
+ else:
+ alias_to_join = lhs_alias
+ extra_col = _get_field(self.through, 'content_type').column
+ content_type_ids = [ContentType.objects.get_for_model(subclass).pk for
+ subclass in _get_subclasses(self.model)]
+ if len(content_type_ids) == 1:
+ content_type_id = content_type_ids[0]
+ extra_where = " AND %s.%s = %%s" % (qn(alias_to_join),
+ qn(extra_col))
+ params = [content_type_id]
+ else:
+ extra_where = " AND %s.%s IN (%s)" % (qn(alias_to_join),
+ qn(extra_col),
+ ','.join(['%s'] *
+ len(content_type_ids)))
+ params = content_type_ids
+ return extra_where, params
+
+ # This and all the methods till the end of class are only used in django >= 1.6
+ def _get_mm_case_path_info(self, direct=False):
+ pathinfos = []
+ linkfield1 = _get_field(self.through, 'content_object')
+ linkfield2 = _get_field(self.through, self.m2m_reverse_field_name())
+ if direct:
+ join1infos = linkfield1.get_reverse_path_info()
+ join2infos = linkfield2.get_path_info()
+ else:
+ join1infos = linkfield2.get_reverse_path_info()
+ join2infos = linkfield1.get_path_info()
+ pathinfos.extend(join1infos)
+ pathinfos.extend(join2infos)
+ return pathinfos
+
+ def _get_gfk_case_path_info(self, direct=False):
+ pathinfos = []
+ from_field = self.model._meta.pk
+ opts = self.through._meta
+ linkfield = _get_field(self.through, self.m2m_reverse_field_name())
+ if direct:
+ join1infos = [PathInfo(self.model._meta, opts, [from_field], self.rel, True, False)]
+ join2infos = linkfield.get_path_info()
+ else:
+ join1infos = linkfield.get_reverse_path_info()
+ join2infos = [PathInfo(opts, self.model._meta, [from_field], self, True, False)]
+ pathinfos.extend(join1infos)
+ pathinfos.extend(join2infos)
+ return pathinfos
+
+ def get_path_info(self):
+ if self.use_gfk:
+ return self._get_gfk_case_path_info(direct=True)
+ else:
+ return self._get_mm_case_path_info(direct=True)
+
+ def get_reverse_path_info(self):
+ if self.use_gfk:
+ return self._get_gfk_case_path_info(direct=False)
+ else:
+ return self._get_mm_case_path_info(direct=False)
+
+ def get_joining_columns(self, reverse_join=False):
+ if reverse_join:
+ return ((self.model._meta.pk.column, "object_id"),)
+ else:
+ return (("object_id", self.model._meta.pk.column),)
+
+ def get_extra_restriction(self, where_class, alias, related_alias):
+ extra_col = _get_field(self.through, 'content_type').column
+ content_type_ids = [ContentType.objects.get_for_model(subclass).pk
+ for subclass in _get_subclasses(self.model)]
+ return ExtraJoinRestriction(related_alias, extra_col, content_type_ids)
+
+ def get_reverse_joining_columns(self):
+ return self.get_joining_columns(reverse_join=True)
+
+ @property
+ def related_fields(self):
+ return [(_get_field(self.through, 'object_id'), self.model._meta.pk)]
+
+ @property
+ def foreign_related_fields(self):
+ return [self.related_fields[0][1]]
+
+
+def _get_subclasses(model):
+ subclasses = [model]
+ if VERSION < (1, 8):
+ all_fields = (_get_field(model, f) for f in model._meta.get_all_field_names())
+ else:
+ all_fields = model._meta.get_fields()
+ for field in all_fields:
+ # Django 1.8 +
+ if (not RelatedObject and isinstance(field, OneToOneRel) and
+ getattr(field.field.rel, "parent_link", None)):
+ subclasses.extend(_get_subclasses(field.related_model))
+
+ # < Django 1.8
+ if (RelatedObject and isinstance(field, RelatedObject) and
+ getattr(field.field.rel, "parent_link", None)):
+ subclasses.extend(_get_subclasses(field.model))
+ return subclasses
+
+
+# `total_ordering` does not exist in Django 1.4, as such
+# we special case this import to be py3k specific which
+# is not supported by Django 1.4
+if six.PY3:
+ from django.utils.functional import total_ordering
+ TaggableManager = total_ordering(TaggableManager)
diff --git a/taggit/migrations/0001_initial.py b/taggit/migrations/0001_initial.py
new file mode 100644
index 0000000..ed51048
--- /dev/null
+++ b/taggit/migrations/0001_initial.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Tag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', unique=True, max_length=100, verbose_name='Name')),
+ ('slug', models.SlugField(help_text='', unique=True, max_length=100, verbose_name='Slug')),
+ ],
+ options={
+ 'verbose_name': 'Tag',
+ 'verbose_name_plural': 'Tags',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('object_id', models.IntegerField(help_text='', verbose_name='Object id', db_index=True)),
+ ('content_type', models.ForeignKey(related_name='taggit_taggeditem_tagged_items', verbose_name='Content type', to='contenttypes.ContentType', help_text='', on_delete=models.CASCADE)),
+ ('tag', models.ForeignKey(related_name='taggit_taggeditem_items', to='taggit.Tag', help_text='', on_delete=models.CASCADE)),
+ ],
+ options={
+ 'verbose_name': 'Tagged Item',
+ 'verbose_name_plural': 'Tagged Items',
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/taggit/migrations/0002_auto_20150616_2121.py b/taggit/migrations/0002_auto_20150616_2121.py
new file mode 100644
index 0000000..012a16f
--- /dev/null
+++ b/taggit/migrations/0002_auto_20150616_2121.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('taggit', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterIndexTogether(
+ name='taggeditem',
+ index_together=set([('content_type', 'object_id')]),
+ ),
+ ]
diff --git a/taggit/migrations/__init__.py b/taggit/migrations/__init__.py
new file mode 100644
index 0000000..c7e346b
--- /dev/null
+++ b/taggit/migrations/__init__.py
@@ -0,0 +1,21 @@
+"""
+Django migrations for taggit app
+
+This package does not contain South migrations. South migrations can be found
+in the ``south_migrations`` package.
+"""
+
+SOUTH_ERROR_MESSAGE = """\n
+For South support, customize the SOUTH_MIGRATION_MODULES setting like so:
+
+ SOUTH_MIGRATION_MODULES = {
+ 'taggit': 'taggit.south_migrations',
+ }
+"""
+
+# Ensure the user is not using Django 1.6 or below with South
+try:
+ from django.db import migrations # noqa
+except ImportError:
+ from django.core.exceptions import ImproperlyConfigured
+ raise ImproperlyConfigured(SOUTH_ERROR_MESSAGE)
diff --git a/taggit/models.py b/taggit/models.py
new file mode 100644
index 0000000..4e86658
--- /dev/null
+++ b/taggit/models.py
@@ -0,0 +1,225 @@
+from __future__ import unicode_literals
+
+import django
+from django import VERSION
+from django.contrib.contenttypes.models import ContentType
+from django.db import IntegrityError, models, transaction
+from django.db.models.query import QuerySet
+from django.template.defaultfilters import slugify as default_slugify
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext
+
+from taggit.utils import _get_field
+try:
+ from unidecode import unidecode
+except ImportError:
+ unidecode = lambda tag: tag
+
+
+try:
+ from django.contrib.contenttypes.fields import GenericForeignKey
+except ImportError: # django < 1.7
+ from django.contrib.contenttypes.generic import GenericForeignKey
+
+
+try:
+ atomic = transaction.atomic
+except AttributeError:
+ from contextlib import contextmanager
+
+ @contextmanager
+ def atomic(using=None):
+ sid = transaction.savepoint(using=using)
+ try:
+ yield
+ except IntegrityError:
+ transaction.savepoint_rollback(sid, using=using)
+ raise
+ else:
+ transaction.savepoint_commit(sid, using=using)
+
+
+@python_2_unicode_compatible
+class TagBase(models.Model):
+ name = models.CharField(verbose_name=_('Name'), unique=True, max_length=100)
+ slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+ if not self.pk and not self.slug:
+ self.slug = self.slugify(self.name)
+ from django.db import router
+ using = kwargs.get("using") or router.db_for_write(
+ type(self), instance=self)
+ # Make sure we write to the same db for all attempted writes,
+ # with a multi-master setup, theoretically we could try to
+ # write and rollback on different DBs
+ kwargs["using"] = using
+ # Be oportunistic and try to save the tag, this should work for
+ # most cases ;)
+ try:
+ with atomic(using=using):
+ res = super(TagBase, self).save(*args, **kwargs)
+ return res
+ except IntegrityError:
+ pass
+ # Now try to find existing slugs with similar names
+ slugs = set(
+ self.__class__._default_manager
+ .filter(slug__startswith=self.slug)
+ .values_list('slug', flat=True)
+ )
+ i = 1
+ while True:
+ slug = self.slugify(self.name, i)
+ if slug not in slugs:
+ self.slug = slug
+ # We purposely ignore concurrecny issues here for now.
+ # (That is, till we found a nice solution...)
+ return super(TagBase, self).save(*args, **kwargs)
+ i += 1
+ else:
+ return super(TagBase, self).save(*args, **kwargs)
+
+ def slugify(self, tag, i=None):
+ slug = default_slugify(unidecode(tag))
+ if i is not None:
+ slug += "_%d" % i
+ return slug
+
+
+class Tag(TagBase):
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+
+@python_2_unicode_compatible
+class ItemBase(models.Model):
+ def __str__(self):
+ return ugettext("%(object)s tagged with %(tag)s") % {
+ "object": self.content_object,
+ "tag": self.tag
+ }
+
+ class Meta:
+ abstract = True
+
+ @classmethod
+ def tag_model(cls):
+ return _get_field(cls, 'tag').rel.to
+
+ @classmethod
+ def tag_relname(cls):
+ return _get_field(cls, 'tag').rel.related_name
+
+ @classmethod
+ def lookup_kwargs(cls, instance):
+ return {
+ 'content_object': instance
+ }
+
+ @classmethod
+ def bulk_lookup_kwargs(cls, instances):
+ return {
+ "content_object__in": instances,
+ }
+
+
+class TaggedItemBase(ItemBase):
+ tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE)
+
+ class Meta:
+ abstract = True
+
+ @classmethod
+ def tags_for(cls, model, instance=None, **extra_filters):
+ kwargs = extra_filters or {}
+ if instance is not None:
+ kwargs.update({
+ '%s__content_object' % cls.tag_relname(): instance
+ })
+ return cls.tag_model().objects.filter(**kwargs)
+ kwargs.update({
+ '%s__content_object__isnull' % cls.tag_relname(): False
+ })
+ return cls.tag_model().objects.filter(**kwargs).distinct()
+
+
+class CommonGenericTaggedItemBase(ItemBase):
+ content_type = models.ForeignKey(
+ ContentType,
+ on_delete=models.CASCADE,
+ verbose_name=_('Content type'),
+ related_name="%(app_label)s_%(class)s_tagged_items"
+ )
+ content_object = GenericForeignKey()
+
+ class Meta:
+ abstract = True
+
+ @classmethod
+ def lookup_kwargs(cls, instance):
+ return {
+ 'object_id': instance.pk,
+ 'content_type': ContentType.objects.get_for_model(instance)
+ }
+
+ @classmethod
+ def bulk_lookup_kwargs(cls, instances):
+ if isinstance(instances, QuerySet):
+ # Can do a real object_id IN (SELECT ..) query.
+ return {
+ "object_id__in": instances,
+ "content_type": ContentType.objects.get_for_model(instances.model),
+ }
+ else:
+ # TODO: instances[0], can we assume there are instances.
+ return {
+ "object_id__in": [instance.pk for instance in instances],
+ "content_type": ContentType.objects.get_for_model(instances[0]),
+ }
+
+ @classmethod
+ def tags_for(cls, model, instance=None, **extra_filters):
+ ct = ContentType.objects.get_for_model(model)
+ kwargs = {
+ "%s__content_type" % cls.tag_relname(): ct
+ }
+ if instance is not None:
+ kwargs["%s__object_id" % cls.tag_relname()] = instance.pk
+ if extra_filters:
+ kwargs.update(extra_filters)
+ return cls.tag_model().objects.filter(**kwargs).distinct()
+
+
+class GenericTaggedItemBase(CommonGenericTaggedItemBase):
+ object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True)
+
+ class Meta:
+ abstract = True
+
+
+if VERSION >= (1, 8):
+
+ class GenericUUIDTaggedItemBase(CommonGenericTaggedItemBase):
+ object_id = models.UUIDField(verbose_name=_('Object id'), db_index=True)
+
+ class Meta:
+ abstract = True
+
+
+class TaggedItem(GenericTaggedItemBase, TaggedItemBase):
+ class Meta:
+ verbose_name = _("Tagged Item")
+ verbose_name_plural = _("Tagged Items")
+ if django.VERSION >= (1, 5):
+ index_together = [
+ ["content_type", "object_id"],
+ ]
diff --git a/taggit/south_migrations/0001_initial.py b/taggit/south_migrations/0001_initial.py
new file mode 100644
index 0000000..6808f38
--- /dev/null
+++ b/taggit/south_migrations/0001_initial.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Tag'
+ db.create_table('taggit_tag', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
+ ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=100)),
+ ))
+ db.send_create_signal('taggit', ['Tag'])
+
+ # Adding model 'TaggedItem'
+ db.create_table('taggit_taggeditem', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('tag', self.gf('django.db.models.fields.related.ForeignKey')(related_name='taggit_taggeditem_items', to=orm['taggit.Tag'])),
+ ('object_id', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
+ ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='taggit_taggeditem_tagged_items', to=orm['contenttypes.ContentType'])),
+ ))
+ db.send_create_signal('taggit', ['TaggedItem'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'Tag'
+ db.delete_table('taggit_tag')
+
+ # Deleting model 'TaggedItem'
+ db.delete_table('taggit_taggeditem')
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'taggit.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
+ },
+ 'taggit.taggeditem': {
+ 'Meta': {'object_name': 'TaggedItem'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
+ }
+ }
+
+ complete_apps = ['taggit']
diff --git a/taggit/south_migrations/0002_unique_tagnames.py b/taggit/south_migrations/0002_unique_tagnames.py
new file mode 100644
index 0000000..d68ea10
--- /dev/null
+++ b/taggit/south_migrations/0002_unique_tagnames.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding unique constraint on 'Tag', fields ['name']
+ db.create_unique('taggit_tag', ['name'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'Tag', fields ['name']
+ db.delete_unique('taggit_tag', ['name'])
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'taggit.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
+ },
+ 'taggit.taggeditem': {
+ 'Meta': {'object_name': 'TaggedItem'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
+ }
+ }
+
+ complete_apps = ['taggit']
diff --git a/taggit/south_migrations/__init__.py b/taggit/south_migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/taggit/south_migrations/__init__.py
diff --git a/taggit/utils.py b/taggit/utils.py
new file mode 100644
index 0000000..775d16a
--- /dev/null
+++ b/taggit/utils.py
@@ -0,0 +1,137 @@
+from __future__ import unicode_literals
+
+from django import VERSION
+from django.utils import six
+from django.utils.encoding import force_text
+from django.utils.functional import wraps
+
+
+def _get_field(model, name):
+ if VERSION < (1, 8):
+ return model._meta.get_field_by_name(name)[0]
+ else:
+ return model._meta.get_field(name)
+
+
+def parse_tags(tagstring):
+ """
+ Parses tag input, with multiple word input being activated and
+ delineated by commas and double quotes. Quotes take precedence, so
+ they may contain commas.
+
+ Returns a sorted list of unique tag names.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ if not tagstring:
+ return []
+
+ tagstring = force_text(tagstring)
+
+ # 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 ',' not in tagstring and '"' not in tagstring:
+ words = list(set(split_strip(tagstring, ' ')))
+ words.sort()
+ return words
+
+ words = []
+ buffer = []
+ # Defer splitting of non-quoted sections until we know if there are
+ # any unquoted commas.
+ to_be_split = []
+ saw_loose_comma = False
+ open_quote = False
+ i = iter(tagstring)
+ try:
+ while True:
+ c = six.next(i)
+ if c == '"':
+ if buffer:
+ to_be_split.append(''.join(buffer))
+ buffer = []
+ # Find the matching quote
+ open_quote = True
+ c = six.next(i)
+ while c != '"':
+ buffer.append(c)
+ c = six.next(i)
+ if buffer:
+ word = ''.join(buffer).strip()
+ if word:
+ words.append(word)
+ buffer = []
+ open_quote = False
+ else:
+ 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 ',' in buffer:
+ saw_loose_comma = True
+ to_be_split.append(''.join(buffer))
+ if to_be_split:
+ if saw_loose_comma:
+ delimiter = ','
+ else:
+ delimiter = ' '
+ for chunk in to_be_split:
+ words.extend(split_strip(chunk, delimiter))
+ words = list(set(words))
+ words.sort()
+ return words
+
+
+def split_strip(string, delimiter=','):
+ """
+ Splits ``string`` on ``delimiter``, stripping each resulting string
+ and returning a list of non-empty strings.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ if not string:
+ return []
+
+ words = [w.strip() for w in string.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
+ the list suitable for editing by the user, such that submitting the
+ given string representation back without changing it will give the
+ same list of tags.
+
+ Tag names which contain commas will be double quoted.
+
+ If any tag name which isn't being quoted contains whitespace, the
+ resulting string of tag names will be comma-delimited, otherwise
+ it will be space-delimited.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ names = []
+ for tag in tags:
+ name = tag.name
+ if ',' in name or ' ' in name:
+ names.append('"%s"' % name)
+ else:
+ names.append(name)
+ return ', '.join(sorted(names))
+
+
+def require_instance_manager(func):
+ @wraps(func)
+ def inner(self, *args, **kwargs):
+ if self.instance is None:
+ raise TypeError("Can't call %s with a non-instance manager" % func.__name__)
+ return func(self, *args, **kwargs)
+ return inner
diff --git a/taggit/views.py b/taggit/views.py
new file mode 100644
index 0000000..5b6c0c4
--- /dev/null
+++ b/taggit/views.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+
+from django.contrib.contenttypes.models import ContentType
+from django.shortcuts import get_object_or_404
+from django.views.generic.list import ListView
+
+from taggit.models import Tag, TaggedItem
+
+
+def tagged_object_list(request, slug, queryset, **kwargs):
+ if callable(queryset):
+ queryset = queryset()
+ tag = get_object_or_404(Tag, slug=slug)
+ qs = queryset.filter(pk__in=TaggedItem.objects.filter(
+ tag=tag, content_type=ContentType.objects.get_for_model(queryset.model)
+ ).values_list("object_id", flat=True))
+ if "extra_context" not in kwargs:
+ kwargs["extra_context"] = {}
+ kwargs["extra_context"]["tag"] = tag
+ return ListView.as_view(request, qs, **kwargs)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/forms.py b/tests/forms.py
new file mode 100644
index 0000000..4513389
--- /dev/null
+++ b/tests/forms.py
@@ -0,0 +1,36 @@
+from __future__ import absolute_import, unicode_literals
+
+from django import forms, VERSION
+
+from .models import (CustomPKFood, DirectCustomPKFood, DirectFood, Food,
+ OfficialFood)
+
+fields = None
+if VERSION >= (1, 6):
+ fields = '__all__'
+
+
+class FoodForm(forms.ModelForm):
+ class Meta:
+ model = Food
+ fields = fields
+
+class DirectFoodForm(forms.ModelForm):
+ class Meta:
+ model = DirectFood
+ fields = fields
+
+class DirectCustomPKFoodForm(forms.ModelForm):
+ class Meta:
+ model = DirectCustomPKFood
+ fields = fields
+
+class CustomPKFoodForm(forms.ModelForm):
+ class Meta:
+ model = CustomPKFood
+ fields = fields
+
+class OfficialFoodForm(forms.ModelForm):
+ class Meta:
+ model = OfficialFood
+ fields = fields
diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py
new file mode 100644
index 0000000..85d86c2
--- /dev/null
+++ b/tests/migrations/0001_initial.py
@@ -0,0 +1,468 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('taggit', '0001_initial'),
+ ('contenttypes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Article',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('title', models.CharField(help_text='', max_length=100)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CustomManager',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CustomPKFood',
+ fields=[
+ ('name', models.CharField(help_text='', max_length=50, serialize=False, primary_key=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CustomPKPet',
+ fields=[
+ ('name', models.CharField(help_text='', max_length=50, serialize=False, primary_key=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='CustomPKHousePet',
+ fields=[
+ ('custompkpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.CustomPKPet', help_text='')),
+ ('trained', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.custompkpet',),
+ ),
+ migrations.CreateModel(
+ name='DirectCustomPKFood',
+ fields=[
+ ('name', models.CharField(help_text='', max_length=50, serialize=False, primary_key=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='DirectCustomPKPet',
+ fields=[
+ ('name', models.CharField(help_text='', max_length=50, serialize=False, primary_key=True)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='DirectCustomPKHousePet',
+ fields=[
+ ('directcustompkpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.DirectCustomPKPet', help_text='')),
+ ('trained', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.directcustompkpet',),
+ ),
+ migrations.CreateModel(
+ name='DirectFood',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='DirectPet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='DirectHousePet',
+ fields=[
+ ('directpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.DirectPet', help_text='')),
+ ('trained', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.directpet',),
+ ),
+ migrations.CreateModel(
+ name='Food',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Movie',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MultipleTags',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MultipleTagsGFK',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('tags1', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OfficialFood',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OfficialPet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OfficialHousePet',
+ fields=[
+ ('officialpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.OfficialPet', help_text='')),
+ ('trained', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.officialpet',),
+ ),
+ migrations.CreateModel(
+ name='OfficialTag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', unique=True, max_length=100, verbose_name='Name')),
+ ('slug', models.SlugField(help_text='', unique=True, max_length=100, verbose_name='Slug')),
+ ('official', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='OfficialThroughModel',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('object_id', models.IntegerField(help_text='', verbose_name='Object id', db_index=True)),
+ ('content_type', models.ForeignKey(related_name='tests_officialthroughmodel_tagged_items', verbose_name='Content type', to='contenttypes.ContentType', help_text='')),
+ ('tag', models.ForeignKey(related_name='tagged_items', to='tests.OfficialTag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Parent',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Child',
+ fields=[
+ ('parent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.Parent', help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.parent',),
+ ),
+ migrations.CreateModel(
+ name='Pet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('name', models.CharField(help_text='', max_length=50)),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='HousePet',
+ fields=[
+ ('pet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.Pet', help_text='')),
+ ('trained', models.BooleanField(default=False, help_text='')),
+ ],
+ options={
+ },
+ bases=('tests.pet',),
+ ),
+ migrations.CreateModel(
+ name='Photo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedCustomPK',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('object_id', models.CharField(help_text='', max_length=50, verbose_name='Object id', db_index=True)),
+ ('content_type', models.ForeignKey(related_name='tests_taggedcustompk_tagged_items', verbose_name='Content type', to='contenttypes.ContentType', help_text='', on_delete=models.CASCADE)),
+ ('tag', models.ForeignKey(related_name='tests_taggedcustompk_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedCustomPKFood',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.DirectCustomPKFood')),
+ ('tag', models.ForeignKey(related_name='tests_taggedcustompkfood_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedCustomPKPet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.DirectCustomPKPet')),
+ ('tag', models.ForeignKey(related_name='tests_taggedcustompkpet_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedFood',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.DirectFood')),
+ ('tag', models.ForeignKey(related_name='tests_taggedfood_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TaggedPet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.DirectPet')),
+ ('tag', models.ForeignKey(related_name='tests_taggedpet_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Through1',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.MultipleTags')),
+ ('tag', models.ForeignKey(related_name='tests_through1_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Through2',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('content_object', models.ForeignKey(help_text='', to='tests.MultipleTags')),
+ ('tag', models.ForeignKey(related_name='tests_through2_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ThroughGFK',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, help_text='', verbose_name='ID')),
+ ('object_id', models.IntegerField(help_text='', verbose_name='Object id', db_index=True)),
+ ('content_type', models.ForeignKey(related_name='tests_throughgfk_tagged_items', verbose_name='Content type', to='contenttypes.ContentType', help_text='')),
+ ('tag', models.ForeignKey(related_name='tagged_items', to='taggit.Tag', help_text='')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AddField(
+ model_name='pet',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='parent',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='officialpet',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='tests.OfficialTag', through='tests.OfficialThroughModel', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='officialfood',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='tests.OfficialTag', through='tests.OfficialThroughModel', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='multipletagsgfk',
+ name='tags2',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.ThroughGFK', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='multipletags',
+ name='tags1',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.Through1', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='multipletags',
+ name='tags2',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.Through2', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='custompkpet',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedCustomPK', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='custompkfood',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedCustomPK', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='directpet',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedPet', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='directfood',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedFood', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='directcustompkpet',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedCustomPKPet', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.AddField(
+ model_name='directcustompkfood',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.TaggedCustomPKFood', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ migrations.CreateModel(
+ name='ArticleTag',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('taggit.tag',),
+ ),
+ migrations.CreateModel(
+ name='ArticleTaggedItem',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ },
+ bases=('taggit.taggeditem',),
+ ),
+ migrations.AddField(
+ model_name='article',
+ name='tags',
+ field=taggit.managers.TaggableManager(to='taggit.Tag', through='tests.ArticleTaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/migrations/__init__.py
diff --git a/tests/models.py b/tests/models.py
new file mode 100644
index 0000000..db1d96c
--- /dev/null
+++ b/tests/models.py
@@ -0,0 +1,228 @@
+from __future__ import unicode_literals
+
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+from taggit.managers import TaggableManager
+from taggit.models import (CommonGenericTaggedItemBase, GenericTaggedItemBase,
+ Tag, TagBase, TaggedItem, TaggedItemBase)
+
+
+# Ensure that two TaggableManagers with custom through model are allowed.
+class Through1(TaggedItemBase):
+ content_object = models.ForeignKey('MultipleTags')
+
+
+class Through2(TaggedItemBase):
+ content_object = models.ForeignKey('MultipleTags')
+
+
+class MultipleTags(models.Model):
+ tags1 = TaggableManager(through=Through1, related_name='tags1')
+ tags2 = TaggableManager(through=Through2, related_name='tags2')
+
+# Ensure that two TaggableManagers with GFK via different through models are allowed.
+class ThroughGFK(GenericTaggedItemBase):
+ tag = models.ForeignKey(Tag, related_name='tagged_items')
+
+class MultipleTagsGFK(models.Model):
+ tags1 = TaggableManager(related_name='tagsgfk1')
+ tags2 = TaggableManager(through=ThroughGFK, related_name='tagsgfk2')
+
+
+@python_2_unicode_compatible
+class Food(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager()
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class Pet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager()
+
+ def __str__(self):
+ return self.name
+
+
+class HousePet(Pet):
+ trained = models.BooleanField(default=False)
+
+
+# Test direct-tagging with custom through model
+
+class TaggedFood(TaggedItemBase):
+ content_object = models.ForeignKey('DirectFood')
+
+
+class TaggedPet(TaggedItemBase):
+ content_object = models.ForeignKey('DirectPet')
+
+
+@python_2_unicode_compatible
+class DirectFood(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through='TaggedFood')
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class DirectPet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=TaggedPet)
+
+ def __str__(self):
+ return self.name
+
+
+class DirectHousePet(DirectPet):
+ trained = models.BooleanField(default=False)
+
+
+# Test custom through model to model with custom PK
+
+class TaggedCustomPKFood(TaggedItemBase):
+ content_object = models.ForeignKey('DirectCustomPKFood')
+
+class TaggedCustomPKPet(TaggedItemBase):
+ content_object = models.ForeignKey('DirectCustomPKPet')
+
+@python_2_unicode_compatible
+class DirectCustomPKFood(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPKFood)
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class DirectCustomPKPet(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPKPet)
+
+ def __str__(self):
+ return self.name
+
+class DirectCustomPKHousePet(DirectCustomPKPet):
+ trained = models.BooleanField(default=False)
+
+# Test custom through model to model with custom PK using GenericForeignKey
+
+class TaggedCustomPK(CommonGenericTaggedItemBase, TaggedItemBase):
+ object_id = models.CharField(max_length=50, verbose_name='Object id', db_index=True)
+
+@python_2_unicode_compatible
+class CustomPKFood(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPK)
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class CustomPKPet(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPK)
+
+ def __str__(self):
+ return self.name
+
+class CustomPKHousePet(CustomPKPet):
+ trained = models.BooleanField(default=False)
+
+# Test custom through model to a custom tag model
+
+class OfficialTag(TagBase):
+ official = models.BooleanField(default=False)
+
+class OfficialThroughModel(GenericTaggedItemBase):
+ tag = models.ForeignKey(OfficialTag, related_name="tagged_items")
+
+@python_2_unicode_compatible
+class OfficialFood(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=OfficialThroughModel)
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class OfficialPet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=OfficialThroughModel)
+
+ def __str__(self):
+ return self.name
+
+class OfficialHousePet(OfficialPet):
+ trained = models.BooleanField(default=False)
+
+
+class Media(models.Model):
+ tags = TaggableManager()
+
+ class Meta:
+ abstract = True
+
+class Photo(Media):
+ pass
+
+class Movie(Media):
+ pass
+
+
+class ArticleTag(Tag):
+ class Meta:
+ proxy = True
+
+ def slugify(self, tag, i=None):
+ slug = "category-%s" % tag.lower()
+
+ if i is not None:
+ slug += "-%d" % i
+ return slug
+
+
+class ArticleTaggedItem(TaggedItem):
+ class Meta:
+ proxy = True
+
+ @classmethod
+ def tag_model(self):
+ return ArticleTag
+
+
+class Article(models.Model):
+ title = models.CharField(max_length=100)
+
+ tags = TaggableManager(through=ArticleTaggedItem)
+
+
+class CustomManager(models.Model):
+ class Foo(object):
+ def __init__(*args, **kwargs):
+ pass
+
+ tags = TaggableManager(manager=Foo)
+
+
+class Parent(models.Model):
+ tags = TaggableManager()
+
+
+class Child(Parent):
+ pass
diff --git a/tests/tests.py b/tests/tests.py
new file mode 100644
index 0000000..9618c0d
--- /dev/null
+++ b/tests/tests.py
@@ -0,0 +1,659 @@
+from __future__ import absolute_import, unicode_literals
+
+from unittest import TestCase as UnitTestCase
+
+import django
+from django.contrib.contenttypes.models import ContentType
+from django.core import serializers
+from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.db import connection, models
+from django.test import TestCase, TransactionTestCase
+from django.test.utils import override_settings
+from django.utils.encoding import force_text
+
+from .forms import (CustomPKFoodForm, DirectCustomPKFoodForm, DirectFoodForm,
+ FoodForm, OfficialFoodForm)
+from .models import (Article, Child, CustomManager, CustomPKFood,
+ CustomPKHousePet, CustomPKPet, DirectCustomPKFood,
+ DirectCustomPKHousePet, DirectCustomPKPet, DirectFood,
+ DirectHousePet, DirectPet, Food, HousePet, Movie,
+ OfficialFood, OfficialHousePet, OfficialPet,
+ OfficialTag, OfficialThroughModel, Pet, Photo,
+ TaggedCustomPK, TaggedCustomPKFood, TaggedCustomPKPet,
+ TaggedFood, TaggedPet)
+
+from taggit.managers import _model_name, _TaggableManager, TaggableManager
+from taggit.models import Tag, TaggedItem
+
+from taggit.utils import edit_string_for_tags, parse_tags
+
+try:
+ from unittest import skipIf, skipUnless
+except ImportError:
+ from django.utils.unittest import skipIf, skipUnless
+
+
+class BaseTaggingTest(object):
+ def assert_tags_equal(self, qs, tags, sort=True, attr="name"):
+ got = [getattr(obj, attr) for obj in qs]
+ if sort:
+ got.sort()
+ tags.sort()
+ self.assertEqual(got, tags)
+
+ def _get_form_str(self, form_str):
+ if django.VERSION >= (1, 3):
+ form_str %= {
+ "help_start": '<span class="helptext">',
+ "help_stop": "</span>"
+ }
+ else:
+ form_str %= {
+ "help_start": "",
+ "help_stop": ""
+ }
+ return form_str
+
+ def assert_form_renders(self, form, html):
+ self.assertHTMLEqual(str(form), self._get_form_str(html))
+
+
+class BaseTaggingTestCase(TestCase, BaseTaggingTest):
+ pass
+
+
+class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
+ pass
+
+
+class TagModelTestCase(BaseTaggingTransactionTestCase):
+ food_model = Food
+ tag_model = Tag
+
+ def test_unique_slug(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("Red", "red")
+
+ def test_update(self):
+ special = self.tag_model.objects.create(name="special")
+ special.save()
+
+ def test_add(self):
+ apple = self.food_model.objects.create(name="apple")
+ yummy = self.tag_model.objects.create(name="yummy")
+ apple.tags.add(yummy)
+
+ def test_slugify(self):
+ a = Article.objects.create(title="django-taggit 1.0 Released")
+ a.tags.add("awesome", "release", "AWESOME")
+ self.assert_tags_equal(a.tags.all(), [
+ "category-awesome",
+ "category-release",
+ "category-awesome-1"
+ ], attr="slug")
+
+ def test_integers(self):
+ """Adding an integer as a tag should raise a ValueError (#237)."""
+ apple = self.food_model.objects.create(name="apple")
+ with self.assertRaisesRegexp(ValueError, (
+ r"Cannot add 1 \(<(type|class) 'int'>\). "
+ r"Expected <class 'django.db.models.base.ModelBase'> or str.")):
+ apple.tags.add(1)
+
+class TagModelDirectTestCase(TagModelTestCase):
+ food_model = DirectFood
+ tag_model = Tag
+
+class TagModelDirectCustomPKTestCase(TagModelTestCase):
+ food_model = DirectCustomPKFood
+ tag_model = Tag
+
+class TagModelCustomPKTestCase(TagModelTestCase):
+ food_model = CustomPKFood
+ tag_model = Tag
+
+class TagModelOfficialTestCase(TagModelTestCase):
+ food_model = OfficialFood
+ tag_model = OfficialTag
+
+class TaggableManagerTestCase(BaseTaggingTestCase):
+ food_model = Food
+ pet_model = Pet
+ housepet_model = HousePet
+ taggeditem_model = TaggedItem
+ tag_model = Tag
+
+ def test_add_tag(self):
+ apple = self.food_model.objects.create(name="apple")
+ self.assertEqual(list(apple.tags.all()), [])
+ self.assertEqual(list(self.food_model.tags.all()), [])
+
+ apple.tags.add('green')
+ self.assert_tags_equal(apple.tags.all(), ['green'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green'])
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add('green')
+ self.assert_tags_equal(pear.tags.all(), ['green'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green'])
+
+ apple.tags.add('red')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
+
+ self.assert_tags_equal(
+ self.food_model.tags.most_common(),
+ ['green', 'red'],
+ sort=False
+ )
+
+ apple.tags.remove('green')
+ self.assert_tags_equal(apple.tags.all(), ['red'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
+ tag = self.tag_model.objects.create(name="delicious")
+ apple.tags.add(tag)
+ self.assert_tags_equal(apple.tags.all(), ["red", "delicious"])
+
+ apple.delete()
+ self.assert_tags_equal(self.food_model.tags.all(), ["green"])
+
+ def test_add_queries(self):
+ # Prefill content type cache:
+ ContentType.objects.get_for_model(self.food_model)
+ apple = self.food_model.objects.create(name="apple")
+ # 1 query to see which tags exist
+ # + 3 queries to create the tags.
+ # + 6 queries to create the intermediary things (including SELECTs, to
+ # make sure we don't double create.
+ # + 12 on Django 1.6 for save points.
+ queries = 22
+ if django.VERSION < (1, 6):
+ queries -= 12
+ self.assertNumQueries(queries, apple.tags.add, "red", "delicious", "green")
+
+ pear = self.food_model.objects.create(name="pear")
+ # 1 query to see which tags exist
+ # + 4 queries to create the intermeidary things (including SELECTs, to
+ # make sure we dont't double create.
+ # + 4 on Django 1.6 for save points.
+ queries = 9
+ if django.VERSION < (1, 6):
+ queries -= 4
+ self.assertNumQueries(queries, pear.tags.add, "green", "delicious")
+
+ self.assertNumQueries(0, pear.tags.add)
+
+ def test_require_pk(self):
+ food_instance = self.food_model()
+ self.assertRaises(ValueError, lambda: food_instance.tags.all())
+
+ def test_delete_obj(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red")
+ self.assert_tags_equal(apple.tags.all(), ["red"])
+ strawberry = self.food_model.objects.create(name="strawberry")
+ strawberry.tags.add("red")
+ apple.delete()
+ self.assert_tags_equal(strawberry.tags.all(), ["red"])
+
+ def test_delete_bulk(self):
+ apple = self.food_model.objects.create(name="apple")
+ kitty = self.pet_model.objects.create(pk=apple.pk, name="kitty")
+
+ apple.tags.add("red", "delicious", "fruit")
+ kitty.tags.add("feline")
+
+ self.food_model.objects.all().delete()
+
+ self.assert_tags_equal(kitty.tags.all(), ["feline"])
+
+ def test_lookup_by_tag(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red", "green")
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green")
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["red"])),
+ [apple]
+ )
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["green"])),
+ [apple, pear]
+ )
+
+ kitty = self.pet_model.objects.create(name="kitty")
+ kitty.tags.add("fuzzy", "red")
+ dog = self.pet_model.objects.create(name="dog")
+ dog.tags.add("woof", "red")
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["red"]).distinct()),
+ [apple]
+ )
+
+ tag = self.tag_model.objects.get(name="woof")
+ self.assertEqual(list(self.pet_model.objects.filter(tags__in=[tag])), [dog])
+
+ cat = self.housepet_model.objects.create(name="cat", trained=True)
+ cat.tags.add("fuzzy")
+
+ pks = self.pet_model.objects.filter(tags__name__in=["fuzzy"])
+ model_name = self.pet_model.__name__
+ self.assertQuerysetEqual(pks,
+ ['<{0}: kitty>'.format(model_name),
+ '<{0}: cat>'.format(model_name)],
+ ordered=False)
+
+ def test_lookup_bulk(self):
+ apple = self.food_model.objects.create(name="apple")
+ pear = self.food_model.objects.create(name="pear")
+ apple.tags.add('fruit', 'green')
+ pear.tags.add('fruit', 'yummie')
+
+ def lookup_qs():
+ # New fix: directly allow WHERE object_id IN (SELECT id FROM ..)
+ objects = self.food_model.objects.all()
+ lookup = self.taggeditem_model.bulk_lookup_kwargs(objects)
+ list(self.taggeditem_model.objects.filter(**lookup))
+
+ def lookup_list():
+ # Simulate old situation: iterate over a list.
+ objects = list(self.food_model.objects.all())
+ lookup = self.taggeditem_model.bulk_lookup_kwargs(objects)
+ list(self.taggeditem_model.objects.filter(**lookup))
+
+ self.assertNumQueries(1, lookup_qs)
+ self.assertNumQueries(2, lookup_list)
+
+ def test_exclude(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red", "green", "delicious")
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green", "delicious")
+
+ self.food_model.objects.create(name="guava")
+
+ pks = self.food_model.objects.exclude(tags__name__in=["red"])
+ model_name = self.food_model.__name__
+ self.assertQuerysetEqual(pks,
+ ['<{0}: pear>'.format(model_name),
+ '<{0}: guava>'.format(model_name)],
+ ordered=False)
+
+ def test_similarity_by_tag(self):
+ """Test that pears are more similar to apples than watermelons"""
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("green", "juicy", "small", "sour")
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green", "juicy", "small", "sweet")
+
+ watermelon = self.food_model.objects.create(name="watermelon")
+ watermelon.tags.add("green", "juicy", "large", "sweet")
+
+ similar_objs = apple.tags.similar_objects()
+ self.assertEqual(similar_objs, [pear, watermelon])
+ self.assertEqual([obj.similar_tags for obj in similar_objs],
+ [3, 2])
+
+ def test_tag_reuse(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("juicy", "juicy")
+ self.assert_tags_equal(apple.tags.all(), ['juicy'])
+
+ def test_query_traverse(self):
+ spot = self.pet_model.objects.create(name='Spot')
+ spike = self.pet_model.objects.create(name='Spike')
+ spot.tags.add('scary')
+ spike.tags.add('fluffy')
+ lookup_kwargs = {
+ '%s__name' % _model_name(self.pet_model): 'Spot'
+ }
+ self.assert_tags_equal(
+ self.tag_model.objects.filter(**lookup_kwargs),
+ ['scary']
+ )
+
+ def test_taggeditem_unicode(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("juicy")
+
+ self.assertEqual(
+ force_text(self.taggeditem_model.objects.all()[0]),
+ "apple tagged with juicy"
+ )
+
+ def test_abstract_subclasses(self):
+ p = Photo.objects.create()
+ p.tags.add("outdoors", "pretty")
+ self.assert_tags_equal(
+ p.tags.all(),
+ ["outdoors", "pretty"]
+ )
+
+ m = Movie.objects.create()
+ m.tags.add("hd")
+ self.assert_tags_equal(
+ m.tags.all(),
+ ["hd"],
+ )
+
+ def test_field_api(self):
+ # Check if tag field, which simulates m2m, has django-like api.
+ field = self.food_model._meta.get_field('tags')
+ self.assertTrue(hasattr(field, 'rel'))
+ self.assertTrue(hasattr(field.rel, 'to'))
+ self.assertTrue(hasattr(field, 'related'))
+
+ # This API has changed in Django 1.8
+ # https://code.djangoproject.com/ticket/21414
+ if django.VERSION >= (1, 8):
+ self.assertEqual(self.food_model, field.model)
+ self.assertEqual(self.tag_model, field.related.model)
+ else:
+ self.assertEqual(self.food_model, field.related.model)
+
+ def test_names_method(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('green')
+ apple.tags.add('red')
+ self.assertEqual(list(apple.tags.names()), ['green', 'red'])
+
+ def test_slugs_method(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('green and juicy')
+ apple.tags.add('red')
+ self.assertEqual(list(apple.tags.slugs()), ['green-and-juicy', 'red'])
+
+ def test_serializes(self):
+ apple = self.food_model.objects.create(name="apple")
+ serializers.serialize("json", (apple,))
+
+ def test_prefetch_related(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('1', '2')
+ orange = self.food_model.objects.create(name="orange")
+ orange.tags.add('2', '4')
+ with self.assertNumQueries(2):
+ l = list(self.food_model.objects.prefetch_related('tags').all())
+ with self.assertNumQueries(0):
+ foods = dict((f.name, set(t.name for t in f.tags.all())) for f in l)
+ self.assertEqual(foods, {
+ 'orange': set(['2', '4']),
+ 'apple': set(['1', '2'])
+ })
+
+ def test_internal_type_is_manytomany(self):
+ self.assertEqual(
+ TaggableManager().get_internal_type(), 'ManyToManyField'
+ )
+
+ def test_prefetch_no_extra_join(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('1', '2')
+ with self.assertNumQueries(2):
+ l = list(self.food_model.objects.prefetch_related('tags').all())
+ join_clause = 'INNER JOIN "%s"' % self.taggeditem_model._meta.db_table
+ self.assertEqual(connection.queries[-1]['sql'].count(join_clause), 1, connection.queries[-2:])
+
+ @override_settings(TAGGIT_CASE_INSENSITIVE=True)
+ def test_with_case_insensitive_option(self):
+ spain = self.tag_model.objects.create(name="Spain", slug="spain")
+ orange = self.food_model.objects.create(name="orange")
+ orange.tags.add('spain')
+ self.assertEqual(list(orange.tags.all()), [spain])
+
+
+class TaggableManagerDirectTestCase(TaggableManagerTestCase):
+ food_model = DirectFood
+ pet_model = DirectPet
+ housepet_model = DirectHousePet
+ taggeditem_model = TaggedFood
+
+class TaggableManagerDirectCustomPKTestCase(TaggableManagerTestCase):
+ food_model = DirectCustomPKFood
+ pet_model = DirectCustomPKPet
+ housepet_model = DirectCustomPKHousePet
+ taggeditem_model = TaggedCustomPKFood
+
+ def test_require_pk(self):
+ # TODO with a charfield pk, pk is never None, so taggit has no way to
+ # tell if the instance is saved or not
+ pass
+
+class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
+ food_model = CustomPKFood
+ pet_model = CustomPKPet
+ housepet_model = CustomPKHousePet
+ taggeditem_model = TaggedCustomPK
+
+ def test_require_pk(self):
+ # TODO with a charfield pk, pk is never None, so taggit has no way to
+ # tell if the instance is saved or not
+ pass
+
+class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
+ food_model = OfficialFood
+ pet_model = OfficialPet
+ housepet_model = OfficialHousePet
+ taggeditem_model = OfficialThroughModel
+ tag_model = OfficialTag
+
+ def test_extra_fields(self):
+ self.tag_model.objects.create(name="red")
+ self.tag_model.objects.create(name="delicious", official=True)
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("delicious", "red")
+
+ pear = self.food_model.objects.create(name="Pear")
+ pear.tags.add("delicious")
+
+ self.assertEqual(apple, self.food_model.objects.get(tags__official=False))
+
+ def test_get_tags_with_count(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red", "green", "delicious")
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green", "delicious")
+
+ tag_info = self.tag_model.objects.filter(officialfood__in=[apple.id, pear.id],name='green').annotate(models.Count('name'))
+ self.assertEqual(tag_info[0].name__count, 2)
+
+class TaggableManagerInitializationTestCase(TaggableManagerTestCase):
+ """Make sure manager override defaults and sets correctly."""
+ food_model = Food
+ custom_manager_model = CustomManager
+
+ def test_default_manager(self):
+ self.assertEqual(self.food_model.tags.__class__, _TaggableManager)
+
+ def test_custom_manager(self):
+ self.assertEqual(self.custom_manager_model.tags.__class__, CustomManager.Foo)
+
+class TaggableFormTestCase(BaseTaggingTestCase):
+ form_class = FoodForm
+ food_model = Food
+
+ def test_form(self):
+ self.assertEqual(list(self.form_class.base_fields), ['name', 'tags'])
+
+ f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'})
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+ f.save()
+ apple = self.food_model.objects.get(name='apple')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy'])
+
+ f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy, delicious'}, instance=apple)
+ f.save()
+ apple = self.food_model.objects.get(name='apple')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy', 'delicious'])
+ self.assertEqual(self.food_model.objects.count(), 1)
+
+ f = self.form_class({"name": "raspberry"})
+ self.assertFalse(f.is_valid())
+
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ apple.tags.add('has,comma')
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ apple.tags.add('has space')
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has space&quot;, &quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ def test_formfield(self):
+ tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
+ ff = tm.formfield()
+ self.assertEqual(ff.label, 'Categories')
+ self.assertEqual(ff.help_text, 'Add some categories')
+ self.assertEqual(ff.required, False)
+
+ self.assertEqual(ff.clean(""), [])
+
+ tm = TaggableManager()
+ ff = tm.formfield()
+ self.assertRaises(ValidationError, ff.clean, "")
+
+class TaggableFormDirectTestCase(TaggableFormTestCase):
+ form_class = DirectFoodForm
+ food_model = DirectFood
+
+class TaggableFormDirectCustomPKTestCase(TaggableFormTestCase):
+ form_class = DirectCustomPKFoodForm
+ food_model = DirectCustomPKFood
+
+class TaggableFormCustomPKTestCase(TaggableFormTestCase):
+ form_class = CustomPKFoodForm
+ food_model = CustomPKFood
+
+class TaggableFormOfficialTestCase(TaggableFormTestCase):
+ form_class = OfficialFoodForm
+ food_model = OfficialFood
+
+
+class TagStringParseTestCase(UnitTestCase):
+ """
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+
+ def test_with_simple_space_delimited_tags(self):
+ """
+ Test with simple space-delimited tags.
+ """
+ self.assertEqual(parse_tags('one'), ['one'])
+ self.assertEqual(parse_tags('one two'), ['one', 'two'])
+ self.assertEqual(parse_tags('one two three'), ['one', 'three', 'two'])
+ self.assertEqual(parse_tags('one one two two'), ['one', '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.assertEqual(parse_tags(',one'), ['one'])
+ self.assertEqual(parse_tags(',one two'), ['one two'])
+ self.assertEqual(parse_tags(',one two three'), ['one two three'])
+ self.assertEqual(parse_tags('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.assertEqual(parse_tags('"one'), ['one'])
+ self.assertEqual(parse_tags('"one two'), ['one', 'two'])
+ self.assertEqual(parse_tags('"one two three'), ['one', 'three', 'two'])
+ self.assertEqual(parse_tags('"one two"'), ['one two'])
+ self.assertEqual(parse_tags('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.assertEqual(parse_tags('one two "thr,ee"'), ['one', 'thr,ee', 'two'])
+
+ def test_with_loose_commas(self):
+ """
+ Loose commas - split on commas
+ """
+ self.assertEqual(parse_tags('"one", two three'), ['one', 'two three'])
+
+ def test_tags_with_double_quotes_can_contain_commas(self):
+ """
+ Double quotes can contain commas
+ """
+ self.assertEqual(parse_tags('a-one "a-two, and a-three"'),
+ ['a-one', 'a-two, and a-three'])
+ self.assertEqual(parse_tags('"two", one, one, two, "one"'),
+ ['one', 'two'])
+
+ def test_with_naughty_input(self):
+ """
+ Test with naughty input.
+ """
+ # Bad users! Naughty users!
+ self.assertEqual(parse_tags(None), [])
+ self.assertEqual(parse_tags(''), [])
+ self.assertEqual(parse_tags('"'), [])
+ self.assertEqual(parse_tags('""'), [])
+ self.assertEqual(parse_tags('"' * 7), [])
+ self.assertEqual(parse_tags(',,,,,,'), [])
+ self.assertEqual(parse_tags('",",",",",",","'), [','])
+ self.assertEqual(parse_tags('a-one "a-two" and "a-three'),
+ ['a-one', 'a-three', 'a-two', 'and'])
+
+ 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.assertEqual(edit_string_for_tags([plain]), 'plain')
+ self.assertEqual(edit_string_for_tags([plain, spaces]), '"spa ces", plain')
+ self.assertEqual(edit_string_for_tags([plain, spaces, comma]), '"com,ma", "spa ces", plain')
+ self.assertEqual(edit_string_for_tags([plain, comma]), '"com,ma", plain')
+ self.assertEqual(edit_string_for_tags([comma, spaces]), '"com,ma", "spa ces"')
+
+
+@skipIf(django.VERSION < (1, 7), "not relevant for Django < 1.7")
+class DeconstructTestCase(UnitTestCase):
+ def test_deconstruct_kwargs_kept(self):
+ instance = TaggableManager(through=OfficialThroughModel, to='dummy.To')
+ name, path, args, kwargs = instance.deconstruct()
+ new_instance = TaggableManager(*args, **kwargs)
+ self.assertEqual('tests.OfficialThroughModel', new_instance.rel.through)
+ self.assertEqual('dummy.To', new_instance.rel.to)
+
+
+@skipUnless(django.VERSION < (1, 7), "test only applies to 1.6 and below")
+class SouthSupportTests(TestCase):
+ def test_import_migrations_module(self):
+ try:
+ from taggit.migrations import __doc__ # noqa
+ except ImproperlyConfigured as e:
+ exception = e
+ self.assertIn("SOUTH_MIGRATION_MODULES", exception.args[0])
+
+
+class InheritedPrefetchTests(TestCase):
+
+ def test_inherited_tags_with_prefetch(self):
+ child = Child()
+ child.save()
+ child.tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
+
+ child = Child.objects.get()
+ no_prefetch_tags = child.tags.all()
+ self.assertEquals(4, no_prefetch_tags.count())
+ child = Child.objects.prefetch_related('tags').get()
+ prefetch_tags = child.tags.all()
+ self.assertEquals(4, prefetch_tags.count())
+ self.assertEquals(set([t.name for t in no_prefetch_tags]),
+ set([t.name for t in prefetch_tags]))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..123c3f1
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,129 @@
+[testenv]
+skipsdist = True
+usedevelop = True
+deps =
+ flake8
+deps14 =
+ https://github.com/django/django/archive/stable/1.4.x.tar.gz#egg=django
+deps15 =
+ https://github.com/django/django/archive/stable/1.5.x.tar.gz#egg=django
+deps16 =
+ https://github.com/django/django/archive/stable/1.6.x.tar.gz#egg=django
+deps17 =
+ https://github.com/django/django/archive/stable/1.7.x.tar.gz#egg=django
+deps18 =
+ https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
+deps19 =
+ https://github.com/django/django/archive/stable/1.9.x.tar.gz#egg=django
+
+commands =
+ python ./runtests.py {posargs}
+
+
+[testenv:py26-1.4.x]
+basepython = python2.6
+deps =
+ {[testenv]deps}
+ {[testenv]deps14}
+
+[testenv:py26-1.5.x]
+basepython = python2.6
+deps =
+ {[testenv]deps}
+ {[testenv]deps15}
+
+[testenv:py26-1.6.x]
+basepython = python2.6
+deps =
+ {[testenv]deps}
+ {[testenv]deps16}
+
+[testenv:py27-1.4.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps14}
+
+[testenv:py27-1.5.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps15}
+
+[testenv:py27-1.6.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps16}
+
+[testenv:py27-1.7.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps17}
+
+[testenv:py27-1.8.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps18}
+
+[testenv:py27-1.9.x]
+basepython = python2.7
+deps =
+ {[testenv]deps}
+ {[testenv]deps19}
+
+[testenv:py33-1.5.x]
+basepython = python3.3
+deps =
+ {[testenv]deps}
+ {[testenv]deps15}
+
+[testenv:py33-1.6.x]
+basepython = python3.3
+deps =
+ {[testenv]deps}
+ {[testenv]deps16}
+
+[testenv:py33-1.7.x]
+basepython = python3.3
+deps =
+ {[testenv]deps}
+ {[testenv]deps17}
+
+[testenv:py33-1.8.x]
+basepython = python3.3
+deps =
+ {[testenv]deps}
+ {[testenv]deps18}
+
+[testenv:py34-1.5.x]
+basepython = python3.4
+deps =
+ {[testenv]deps}
+ {[testenv]deps15}
+
+[testenv:py34-1.6.x]
+basepython = python3.4
+deps =
+ {[testenv]deps}
+ {[testenv]deps16}
+
+[testenv:py34-1.7.x]
+basepython = python3.4
+deps =
+ {[testenv]deps}
+ {[testenv]deps17}
+
+[testenv:py34-1.8.x]
+basepython = python3.4
+deps =
+ {[testenv]deps}
+ {[testenv]deps18}
+
+[testenv:py34-1.9.x]
+basepython = python3.4
+deps =
+ {[testenv]deps}
+ {[testenv]deps19}