commit 0c6b364e5ec3befdfdc6f2fa02fe4fd1bc52027f Author: Michal Čihař Date: Fri Nov 27 08:37:05 2015 +0100 django-taggit (0.17.4-1) unstable; urgency=medium * New upstream release. # imported from the archive 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 +fakeempire +Ben Firshman +Alex Gaynor +Rob Hudson +Carl Meyer +Frank Wiles +Jonathan Buchanan +idle sign +Charles Leifer +Florian Apolloner 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 `_. + * 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 `_. + * Smarter tagstring parsing for form field; ported from Jonathan + Buchanan's `django-tagging + `_. 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() + [, , ] + >>> apple.tags.remove("green") + >>> apple.tags.all() + [, ] + >>> Food.objects.filter(tags__name__in=["red"]) + [, ] + + 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 `_. And for questions about usage or + development you can contact the + `mailinglist `_. + +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() + [, , ] + >>> apple.tags.remove("green") + >>> apple.tags.all() + [, ] + >>> Food.objects.filter(tags__name__in=["red"]) + [, ] + +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 `_. And for questions about usage or +development you can contact the +`mailinglist `_. 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ř 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ř Thu, 29 Oct 2015 14:44:36 +0100 + +django-taggit (0.17.1-1) unstable; urgency=medium + + * New upstream release. + + -- Michal Čihař 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ř 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ř 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ř 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ř 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ř 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ř 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ř 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ř Mon, 09 Sep 2013 09:36:36 +0200 + +django-taggit (0.10a1-2) unstable; urgency=low + + * Fix typo in package name. + + -- Michal Čihař Thu, 15 Aug 2013 16:48:21 +0200 + +django-taggit (0.10a1-1) unstable; urgency=low + + * Initial release. (Closes: #719809) + + -- Michal Čihař 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ř +Uploaders: Python Applications Packaging Team +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ř on +Thu, 15 Aug 2013 16:18:02 +0200. + +It was downloaded from . + +Upstream Authors: + + Nathan Borror + fakeempire + Ben Firshman + Alex Gaynor + Rob Hudson + Carl Meyer + Frank Wiles + Jonathan Buchanan + idle sign + Charles Leifer + Florian Apolloner + +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ř + 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() + [, , ] + >>> apple.tags.remove("green") + >>> apple.tags.all() + [, ] + >>> Food.objects.filter(tags__name__in=["red"]) + [, ] + + 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 `_. And for questions about usage or + development you can contact the + `mailinglist `_. + +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 ' where 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 `_. 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"]) + [, , ] + + +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.objects.filter(tags__name__in=["delicious", "red"]).distinct() + [] + +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 `_, it is not +currently (Django < 1.6) possible to use aggregation in conjunction with ``taggit``. This is +a `documented interaction `_ +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 +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['_static'] + +# 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 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 Binary files /dev/null and b/taggit/locale/cs/LC_MESSAGES/django.mo 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 , 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 \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=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 Binary files /dev/null and b/taggit/locale/de/LC_MESSAGES/django.mo 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 \n" +"Language-Team: German \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 , 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 \n" +"Language-Team: LANGUAGE \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 Binary files /dev/null and b/taggit/locale/eo/LC_MESSAGES/django.mo 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 \n" +"Language-Team: Esperanto \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 Binary files /dev/null and b/taggit/locale/he/LC_MESSAGES/django.mo 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 , 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 \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: 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 Binary files /dev/null and b/taggit/locale/it/LC_MESSAGES/django.mo 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 , 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 \n" +"Language-Team: LANGUAGE \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 Binary files /dev/null and b/taggit/locale/ja/LC_MESSAGES/django.mo 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 \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 Binary files /dev/null and b/taggit/locale/nb/LC_MESSAGES/django.mo 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 , 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 \n" +"Language-Team: Norwegian \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 Binary files /dev/null and b/taggit/locale/nl/LC_MESSAGES/django.mo 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 \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 Binary files /dev/null and b/taggit/locale/pt_BR/LC_MESSAGES/django.mo 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 , 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 \n" +"Language-Team: Portuguese (Brazil) \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 Binary files /dev/null and b/taggit/locale/ru/LC_MESSAGES/django.mo 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 , 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 \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 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 + `_ + """ + 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 + `_ + """ + 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 + `_ + """ + 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 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 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": '', + "help_stop": "" + } + 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 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, """ +
%(help_start)sA comma-separated list of tags.%(help_stop)s""") + 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, """ +
%(help_start)sA comma-separated list of tags.%(help_stop)s""") + + apple.tags.add('has,comma') + f = self.form_class(instance=apple) + self.assert_form_renders(f, """ +
%(help_start)sA comma-separated list of tags.%(help_stop)s""") + + apple.tags.add('has space') + f = self.form_class(instance=apple) + self.assert_form_renders(f, """ +
%(help_start)sA comma-separated list of tags.%(help_stop)s""") + + 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 + `_ + """ + + 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}