django-taggit (0.17.4-1) unstable; urgency=medium

* New upstream release.

# imported from the archive
This commit is contained in:
Michal Čihař 2015-11-27 08:37:05 +01:00
commit 0c6b364e5e
73 changed files with 4959 additions and 0 deletions

15
AUTHORS Normal file
View File

@ -0,0 +1,15 @@
django-taggit was originally created by Alex Gaynor.
The following is a list of much appreciated contributors:
Nathan Borror <nathan@playgroundblues.com>
fakeempire <adam@fakeempire.com>
Ben Firshman <ben@firshman.co.uk>
Alex Gaynor <alex.gaynor@gmail.com>
Rob Hudson <rob@cogit8.org>
Carl Meyer <carl@oddbird.net>
Frank Wiles
Jonathan Buchanan
idle sign <idlesign@yandex.ru>
Charles Leifer
Florian Apolloner <apollo13@apolloner.eu>

163
CHANGELOG.txt Normal file
View File

@ -0,0 +1,163 @@
Changelog
=========
0.17.4 (2015-11-25)
~~~~~~~~~~~~~~~~~~~
* Allows custom Through Model with GenericForeignKey
* https://github.com/alex/django-taggit/pull/359
0.17.3 (2015-10-26)
~~~~~~~~~~~~~~~~~~~
* Silence Django 1.9 warning about on_delete
* https://github.com/alex/django-taggit/pull/338
0.17.2 (2015-10-25)
~~~~~~~~~~~~~~~~~~~
* Django 1.9 beta compatibility
* https://github.com/alex/django-taggit/pull/352
0.17.1 (2015-09-10)
~~~~~~~~~~~~~~~~~~~
* Fix unknown column `object_id` issue with Django 1.6+
* https://github.com/alex/django-taggit/pull/305
0.17.0 (2015-08-14)
~~~~~~~~~~~~~~~~~~~
* Database index added on TaggedItem fields content_type & object_id
* https://github.com/alex/django-taggit/pull/319
0.16.4 (2015-08-13)
~~~~~~~~~~~~~~~~~~~
* Access default manager via class instead of instance
* https://github.com/alex/django-taggit/pull/335
0.16.3 (2015-08-08)
~~~~~~~~~~~~~~~~~~~
* Prevent IntegrityError with custom TagBase classes
* https://github.com/alex/django-taggit/pull/334
0.16.2 (2015-07-13)
~~~~~~~~~~~~~~~~~~~
* Fix an admin bug related to the `Manager` property `through_fields`
* https://github.com/alex/django-taggit/pull/328
0.16.1 (2015-07-09)
~~~~~~~~~~~~~~~~~~~
* Fix bug that assumed all primary keys are named 'id'
* https://github.com/alex/django-taggit/pull/322
0.16.0 (2015-07-04)
~~~~~~~~~~~~~~~~~~~
* Add option to allow case-insensitive tags
* https://github.com/alex/django-taggit/pull/325
0.15.0 (2015-06-23)
~~~~~~~~~~~~~~~~~~~
* Fix wrong slugs for non-latin chars
* Only works if optional GPL dependency (unidecode) is installed
* https://github.com/alex/django-taggit/pull/315
* https://github.com/alex/django-taggit/pull/273
0.14.0 (2015-04-26)
~~~~~~~~~~~~~~~~~~~
* Prevent extra JOIN when prefetching
* https://github.com/alex/django-taggit/pull/275
* Prevent _meta warnings with Django 1.8
* https://github.com/alex/django-taggit/pull/299
0.13.0 (2015-04-02)
~~~~~~~~~~~~~~~~~~~
* Django 1.8 support
* https://github.com/alex/django-taggit/pull/297
0.12.3 (2015-03-03)
~~~~~~~~~~~~~~~~~~~
* Specify that the internal type of the TaggitManager is a ManyToManyField
0.12.2 (2014-21-09)
~~~~~~~~~~~~~~~~~~~
* Fixed 1.7 migrations.
0.12.1 (2014-10-08)
~~~~~~~~~~~~~~~~~~~
* Final (hopefully) fixes for the upcoming Django 1.7 release.
* Added Japanese translation.
0.12.0 (2014-20-04)
~~~~~~~~~~~~~~~~~~~
* **Backwards incompatible:** Support for Django 1.7 migrations. South users
have to set ``SOUTH_MIGRATION_MODULES`` to use ``taggit.south_migrations``
for taggit.
* **Backwards incompatible:** Django's new transaction handling is used on
Django 1.6 and newer.
* **Backwards incompatible:** ``Tag.save`` got changed to opportunistically
try to save the tag and if that fails fall back to selecting existing
similar tags and retry -- if that fails too an ``IntegrityError`` is
raised by the database, your app will have to handle that.
* Added Italian and Esperanto translations.
0.11.2 (2013-13-12)
~~~~~~~~~~~~~~~~~~~
* Forbid multiple TaggableManagers via generic foreign keys.
0.11.1 (2013-25-11)
~~~~~~~~~~~~~~~~~~~
* Fixed support for Django 1.4 and 1.5.
0.11.0 (2013-25-11)
~~~~~~~~~~~~~~~~~~~
* Added support for prefetch_related on tags fields.
* Fixed support for Django 1.7.
* Made the tagging relations unserializeable again.
* Allow more than one TaggableManager on models (assuming concrete FKs are
used for the relations).
0.10.0 (2013-17-08)
~~~~~~~~~~~~~~~~~~~
* Support for Django 1.6 and 1.7.
* Python3 support
* **Backwards incompatible:** Dropped support for Django < 1.4.5.
* Tag names are unique now, use the provided South migrations to upgrade.
0.9.2 (2011-01-17)
~~~~~~~~~~~~~~~~~~
* **Backwards incompatible:** Forms containing a :class:`TaggableManager` by
default now require tags, to change this provide ``blank=True`` to the
:class:`TaggableManager`.
* Now works with Django 1.3 (as of beta-1).
0.9.0 (2010-09-22)
~~~~~~~~~~~~~~~~~~
* Added a Hebrew locale.
* Added an index on the ``object_id`` field of ``TaggedItem``.
* When displaying tags always join them with commas, never spaces.
* The docs are now available `online <http://django-taggit.readthedocs.org/>`_.
* Custom ``Tag`` models are now allowed.
* **Backwards incompatible:** Filtering on tags is no longer
``filter(tags__in=["foo"])``, it is written
``filter(tags__name__in=["foo"])``.
* Added a German locale.
* Added a Dutch locale.
* Removed ``taggit.contrib.suggest``, it now lives in an external application,
see :doc:`external_apps` for more information.
0.8.0 (2010-06-22)
~~~~~~~~~~~~~~~~~~
* Fixed querying for objects using ``exclude(tags__in=tags)``.
* Marked strings as translatable.
* Added a Russian translation.
* Created a `mailing list <http://groups.google.com/group/django-taggit>`_.
* Smarter tagstring parsing for form field; ported from Jonathan
Buchanan's `django-tagging
<http://django-tagging.googlecode.com>`_. Now supports tags
containing commas. See :ref:`tags-in-forms` for details.
* Switched to using savepoints around the slug generation for tags. This
ensures that it works fine on databases (such as Postgres) which dirty a
transaction with an ``IntegrityError``.
* Added Python 2.4 compatibility.
* Added Django 1.1 compatibility.

27
LICENSE Normal file
View File

@ -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.

9
MANIFEST.in Normal file
View File

@ -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 *

62
PKG-INFO Normal file
View File

@ -0,0 +1,62 @@
Metadata-Version: 1.1
Name: django-taggit
Version: 0.17.4
Summary: django-taggit is a reusable Django application for simple tagging.
Home-page: http://github.com/alex/django-taggit/tree/master
Author: Alex Gaynor
Author-email: alex.gaynor@gmail.com
License: BSD
Description: django-taggit
=============
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
.. code:: python
from django.db import models
from taggit.managers import TaggableManager
class Food(models.Model):
# ... fields here
tags = TaggableManager()
Then you can use the API like so:
.. code:: python
>>> apple = Food.objects.create(name="apple")
>>> apple.tags.add("red", "green", "delicious")
>>> apple.tags.all()
[<Tag: red>, <Tag: green>, <Tag: delicious>]
>>> apple.tags.remove("green")
>>> apple.tags.all()
[<Tag: red>, <Tag: delicious>]
>>> Food.objects.filter(tags__name__in=["red"])
[<Food: apple>, <Food: cherry>]
Tags will show up for you automatically in forms and the admin.
``django-taggit`` requires Django 1.4.5 or greater.
For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
development you can contact the
`mailinglist <http://groups.google.com/group/django-taggit>`_.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Framework :: Django

39
README.rst Normal file
View File

@ -0,0 +1,39 @@
django-taggit
=============
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
.. code:: python
from django.db import models
from taggit.managers import TaggableManager
class Food(models.Model):
# ... fields here
tags = TaggableManager()
Then you can use the API like so:
.. code:: python
>>> apple = Food.objects.create(name="apple")
>>> apple.tags.add("red", "green", "delicious")
>>> apple.tags.all()
[<Tag: red>, <Tag: green>, <Tag: delicious>]
>>> apple.tags.remove("green")
>>> apple.tags.all()
[<Tag: red>, <Tag: delicious>]
>>> Food.objects.filter(tags__name__in=["red"])
[<Food: apple>, <Food: cherry>]
Tags will show up for you automatically in forms and the admin.
``django-taggit`` requires Django 1.4.5 or greater.
For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
development you can contact the
`mailinglist <http://groups.google.com/group/django-taggit>`_.

97
debian/changelog vendored Normal file
View File

@ -0,0 +1,97 @@
django-taggit (0.17.4-1) unstable; urgency=medium
* New upstream release.
-- Michal Čihař <nijel@debian.org> Fri, 27 Nov 2015 08:37:05 +0100
django-taggit (0.17.3-1) unstable; urgency=medium
* New upstream release.
- Django 1.9 compatibility.
-- Michal Čihař <nijel@debian.org> Thu, 29 Oct 2015 14:44:36 +0100
django-taggit (0.17.1-1) unstable; urgency=medium
* New upstream release.
-- Michal Čihař <nijel@debian.org> Thu, 17 Sep 2015 09:47:25 +0200
django-taggit (0.17.0-1) unstable; urgency=medium
* New upstream release.
* Update debian/watch.
* Add python 3 package.
* Run tests during build.
-- Michal Čihař <nijel@debian.org> Mon, 24 Aug 2015 13:44:42 +0200
django-taggit (0.12.2-1) unstable; urgency=medium
* New upstream release.
- Fixed 1.7 migrations.
* Bump standards to 3.9.6.
-- Michal Čihař <nijel@debian.org> Thu, 25 Sep 2014 09:20:46 +0200
django-taggit (0.12.1-1) unstable; urgency=medium
* New upstream release.
- Complete support for Django 1.7 (Closes: #755614).
-- Michal Čihař <nijel@debian.org> Mon, 11 Aug 2014 09:55:45 +0200
django-taggit (0.12-1) unstable; urgency=low
* New upstream release.
- Backwards incompatible: Support for Django 1.7 migrations. South users
have to set SOUTH_MIGRATION_MODULES to use taggit.south_migrations
for taggit.
- Backwards incompatible: Django's new transaction handling is used on
Django 1.6 and newer.
- Backwards incompatible: Tag.save got changed to opportunistically
try to save the tag and if that fails fall back to selecting existing
similar tags and retry -- if that fails too an IntegrityError is
raised by the database, your app will have to handle that.
- Added Italian and Esperanto translations.
-- Michal Čihař <nijel@debian.org> Tue, 29 Apr 2014 10:05:29 +0200
django-taggit (0.11.2-2) unstable; urgency=medium
* Bump standards to 3.9.5.
* Really do not ship tests in module without namespace (Closes: #738210).
-- Michal Čihař <nijel@debian.org> Mon, 10 Feb 2014 09:36:44 +0100
django-taggit (0.11.2-1) unstable; urgency=medium
* New upstream release.
* Do not ship tests in module without namespace (Closes: #733729).
-- Michal Čihař <nijel@debian.org> Fri, 03 Jan 2014 10:20:12 +0100
django-taggit (0.11.1-1) unstable; urgency=low
* New upstream release.
* Bump standards to 3.9.5.
-- Michal Čihař <nijel@debian.org> Mon, 09 Dec 2013 14:00:12 +0100
django-taggit (0.10a1-3) unstable; urgency=low
* Fixed Vcs-* fields in debian/control (Closes: #721524).
-- Michal Čihař <nijel@debian.org> Mon, 09 Sep 2013 09:36:36 +0200
django-taggit (0.10a1-2) unstable; urgency=low
* Fix typo in package name.
-- Michal Čihař <nijel@debian.org> Thu, 15 Aug 2013 16:48:21 +0200
django-taggit (0.10a1-1) unstable; urgency=low
* Initial release. (Closes: #719809)
-- Michal Čihař <nijel@debian.org> Thu, 15 Aug 2013 16:31:05 +0200

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

41
debian/control vendored Normal file
View File

@ -0,0 +1,41 @@
Source: django-taggit
Section: python
Priority: optional
Maintainer: Michal Čihař <nijel@debian.org>
Uploaders: Python Applications Packaging Team <python-apps-team@lists.alioth.debian.org>
Build-Depends: debhelper (>= 9)
Build-Depends-Indep: python (>= 2.6.6-3), python-setuptools, python3-setuptools, python3-all, dh-python, python-django, python3-django
Standards-Version: 3.9.6
Vcs-Browser: http://anonscm.debian.org/gitweb/?p=collab-maint/django-taggit.git
Vcs-Git: git://anonscm.debian.org/collab-maint/django-taggit.git
Homepage: https://github.com/alex/django-taggit
X-Python-Version: >= 2.6
X-Python3-Version: >= 3.2
Package: python-django-taggit
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}, python-django
Breaks: ${python:Breaks}
Description: simple tagging for Django (Python 2)
This is a generic tagging application for Django, which allows
association of a number of tags with any Model instance and makes
retrieval of tags simple.
.
django-taggit a simpler approach to tagging with Django. Add "taggit" to your
INSTALLED_APPS then just add a TaggableManager to your model.
.
This package installs the library for Python 2.
Package: python3-django-taggit
Architecture: all
Depends: ${python3:Depends}, ${misc:Depends}, python3-django
Breaks: ${python3:Breaks}
Description: simple tagging for Django (Python 3)
This is a generic tagging application for Django, which allows
association of a number of tags with any Model instance and makes
retrieval of tags simple.
.
django-taggit a simpler approach to tagging with Django. Add "taggit" to your
INSTALLED_APPS then just add a TaggableManager to your model.
.
This package installs the library for Python 3.

54
debian/copyright vendored Normal file
View File

@ -0,0 +1,54 @@
This package was debianized by Michal Čihař <nijel@debian.org> on
Thu, 15 Aug 2013 16:18:02 +0200.
It was downloaded from <https://pypi.python.org/pypi/django-taggit>.
Upstream Authors:
Nathan Borror <nathan@playgroundblues.com>
fakeempire <adam@fakeempire.com>
Ben Firshman <ben@firshman.co.uk>
Alex Gaynor <alex.gaynor@gmail.com>
Rob Hudson <rob@cogit8.org>
Carl Meyer <carl@oddbird.net>
Frank Wiles
Jonathan Buchanan
idle sign <idlesign@yandex.ru>
Charles Leifer
Florian Apolloner <apollo13@apolloner.eu>
Copyright:
Copyright (c) Alex Gaynor and individual contributors.
License:
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of django-taggit nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
On Debian systems, the complete text of the GNU General
Public License can be found in `/usr/share/common-licenses/GPL-2'.
The Debian packaging is Copyright (C) 2013, Michal Čihař
<nijel@debian.org> and is licensed under the GPL, see above.

5
debian/gbp.conf vendored Normal file
View File

@ -0,0 +1,5 @@
# Configuration file for git-buildpackage and friends
[DEFAULT]
sign-tags = True
pristine-tar = True

9
debian/rules vendored Executable file
View File

@ -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

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (quilt)

2
debian/watch vendored Normal file
View File

@ -0,0 +1,2 @@
version=3
http://pypi.debian.net/django-taggit/django-taggit-(.*)\.tar\.gz

View File

@ -0,0 +1,62 @@
Metadata-Version: 1.1
Name: django-taggit
Version: 0.17.4
Summary: django-taggit is a reusable Django application for simple tagging.
Home-page: http://github.com/alex/django-taggit/tree/master
Author: Alex Gaynor
Author-email: alex.gaynor@gmail.com
License: BSD
Description: django-taggit
=============
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
.. code:: python
from django.db import models
from taggit.managers import TaggableManager
class Food(models.Model):
# ... fields here
tags = TaggableManager()
Then you can use the API like so:
.. code:: python
>>> apple = Food.objects.create(name="apple")
>>> apple.tags.add("red", "green", "delicious")
>>> apple.tags.all()
[<Tag: red>, <Tag: green>, <Tag: delicious>]
>>> apple.tags.remove("green")
>>> apple.tags.all()
[<Tag: red>, <Tag: delicious>]
>>> Food.objects.filter(tags__name__in=["red"])
[<Food: apple>, <Food: cherry>]
Tags will show up for you automatically in forms and the admin.
``django-taggit`` requires Django 1.4.5 or greater.
For more info check out the `documentation <https://django-taggit.readthedocs.org/en/latest/>`_. And for questions about usage or
development you can contact the
`mailinglist <http://groups.google.com/group/django-taggit>`_.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Framework :: Django

View File

@ -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

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
taggit

89
docs/Makefile Normal file
View File

@ -0,0 +1,89 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-taggit.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-taggit.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

14
docs/admin.txt Normal file
View File

@ -0,0 +1,14 @@
Using tags in the admin
=======================
By default if you have a :class:`TaggableManager` on your model it will show up
in the admin, just as it will in any other form. One important thing to note
is that you *cannot* include a :class:`TaggableManager` in
:attr:`ModelAdmin.list_display`, if you do you'll see an exception that looks
like::
AttributeError: 'TaggableManager' object has no attribute 'flatchoices'
This is for the same reason that you cannot include a :class:`ManyToManyField`,
it would result in an unreasonable number of queries being executed. If you really would like to add it, you can read the
`Django documentation <http://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display>`_.

114
docs/api.txt Normal file
View File

@ -0,0 +1,114 @@
The API
=======
After you've got your ``TaggableManager`` added to your model you can start
playing around with the API.
.. class:: TaggableManager([verbose_name="Tags", help_text="A comma-separated list of tags.", through=None, blank=False])
:param verbose_name: The verbose_name for this field.
:param help_text: The help_text to be used in forms (including the admin).
:param through: The through model, see :doc:`custom_tagging` for more
information.
:param blank: Controls whether this field is required.
.. method:: add(*tags)
This adds tags to an object. The tags can be either ``Tag`` instances, or
strings::
>>> apple.tags.all()
[]
>>> apple.tags.add("red", "green", "fruit")
.. method:: remove(*tags)
Removes a tag from an object. No exception is raised if the object
doesn't have that tag.
.. method:: clear()
Removes all tags from an object.
.. method:: set(*tags)
Removes all the current tags and then adds the specified tags to the
object.
.. method: most_common()
Returns a ``QuerySet`` of all tags, annotated with the number of times
they appear, available as the ``num_times`` attribute on each tag. The
``QuerySet``is ordered by ``num_times``, descending. The ``QuerySet``
is lazily evaluated, and can be sliced efficiently.
.. method:: similar_objects()
Returns a list (not a lazy ``QuerySet``) of other objects tagged
similarly to this one, ordered with most similar first. Each object in
the list is decorated with a ``similar_tags`` attribute, the number of
tags it shares with this object.
If the model is using generic tagging (the default), this method
searches tagged objects from all classes. If you are querying on a
model with its own tagging through table, only other instances of the
same model will be returned.
.. method:: names()
Convenience method, returning a ``ValuesListQuerySet`` (basically
just an iterable) containing the name of each tag as a string::
>>> apple.tags.names()
[u'green and juicy', u'red']
.. method:: slugs()
Convenience method, returning a ``ValuesListQuerySet`` (basically
just an iterable) containing the slug of each tag as a string::
>>> apple.tags.slugs()
[u'green-and-juicy', u'red']
.. hint::
You can subclass ``_TaggableManager`` (note the underscore) to add
methods or functionality. ``TaggableManager`` takes an optional
manager keyword argument for your custom class, like this::
class Food(models.Model):
# ... fields here
tags = TaggableManager(manager=_CustomTaggableManager)
Filtering
~~~~~~~~~
To find all of a model with a specific tags you can filter, using the normal
Django ORM API. For example if you had a ``Food`` model, whose
``TaggableManager`` was named ``tags``, you could find all the delicious fruit
like so::
>>> Food.objects.filter(tags__name__in=["delicious"])
[<Food: apple>, <Food: pear>, <Food: plum>]
If you're filtering on multiple tags, it's very common to get duplicate
results, because of the way relational databases work. Often you'll want to
make use of the ``distinct()`` method on ``QuerySets``::
>>> Food.objects.filter(tags__name__in=["delicious", "red"])
[<Food: apple>, <Food: apple>]
>>> Food.objects.filter(tags__name__in=["delicious", "red"]).distinct()
[<Food: apple>]
You can also filter by the slug on tags. If you're using a custom ``Tag``
model you can use this API to filter on any fields it has.
Aggregation
~~~~~~~~~~~
Unfortunately, due to a
`bug in Django <http://code.djangoproject.com/ticket/10870>`_, it is not
currently (Django < 1.6) possible to use aggregation in conjunction with ``taggit``. This is
a `documented interaction <http://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations-and-aggregation>`_
of generic relations (which ``taggit`` uses internally) and aggregates.

1
docs/changelog.txt Normal file
View File

@ -0,0 +1 @@
.. include:: ../CHANGELOG.txt

198
docs/conf.py Normal file
View File

@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
#
# django-taggit documentation build configuration file, created by
# sphinx-quickstart on Mon May 3 22:22:47 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.txt'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-taggit'
copyright = u'2010-2014, Alex Gaynor and others.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.12'
# The full version, including alpha/beta/rc tags.
release = '0.12'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-taggitdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-taggit.tex', u'django-taggit Documentation',
u'Alex Gaynor', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

167
docs/custom_tagging.txt Normal file
View File

@ -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.

34
docs/external_apps.txt Normal file
View File

@ -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

51
docs/forms.txt Normal file
View File

@ -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()

39
docs/getting_started.txt Normal file
View File

@ -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

45
docs/index.txt Normal file
View File

@ -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/

31
runtests.py Executable file
View File

@ -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()

18
setup.cfg Normal file
View File

@ -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

41
setup.py Normal file
View File

@ -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,
)

1
taggit/__init__.py Normal file
View File

@ -0,0 +1 @@
VERSION = (0, 17, 4)

21
taggit/admin.py Normal file
View File

@ -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)

27
taggit/forms.py Normal file
View File

@ -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."))

Binary file not shown.

View File

@ -0,0 +1,64 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-08-01 16:52+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: forms.py:24
msgid "Please provide a comma-separated list of tags."
msgstr "Vložte čárkami oddělený seznam tagů"
#: managers.py:59 models.py:59
msgid "Tags"
msgstr "Tagy"
#: managers.py:60
msgid "A comma-separated list of tags."
msgstr "Čárkami oddělený seznam tagů"
#: models.py:15
msgid "Name"
msgstr "Jméno"
#: models.py:16
msgid "Slug"
msgstr "Slug"
#: models.py:58
msgid "Tag"
msgstr "Tag"
#: models.py:65
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s označen tagem %(tag)s"
#: models.py:112
msgid "Object id"
msgstr "ID objektu"
#: models.py:115
msgid "Content type"
msgstr "Typ obsahu"
#: models.py:158
msgid "Tagged Item"
msgstr "Tagem označená položka"
#: models.py:159
msgid "Tagged Items"
msgstr "Tagy označené položky"

Binary file not shown.

View File

@ -0,0 +1,67 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:26-0700\n"
"PO-Revision-Date: 2010-09-07 09:26-0700\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: German <de@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Bitte eine durch Komma getrennte Schlagwortliste eingeben."
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr "Schlagwörter"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "Eine durch Komma getrennte Schlagwortliste."
#: models.py:10
msgid "Name"
msgstr "Name"
#: models.py:11
msgid "Slug"
msgstr "Kürzel"
#: models.py:49
msgid "Tag"
msgstr "Schlagwort"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s verschlagwortet mit %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "Objekt-ID"
#: models.py:104 models.py:110
msgid "Content type"
msgstr "Inhaltstyp"
#: models.py:138
msgid "Tagged Item"
msgstr "Verschlagwortetes Objekt"
#: models.py:139
msgid "Tagged Items"
msgstr "Verschlagwortete Objekte"
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
"\" in your expression."
msgstr ""
"Bitte einen regulären Ausdruck eingeben. Fügen Sie \"(?i) \" dem "
"Ausdruck hinzu, um nicht zwischen Groß- und Kleinschreibung zu "
"unterscheiden."

View File

@ -0,0 +1,68 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:45-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr ""
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr ""
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr ""
#: models.py:10
msgid "Name"
msgstr ""
#: models.py:11
msgid "Slug"
msgstr ""
#: models.py:49
msgid "Tag"
msgstr ""
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr ""
#: models.py:100
msgid "Object id"
msgstr ""
#: models.py:104 models.py:110
msgid "Content type"
msgstr ""
#: models.py:138
msgid "Tagged Item"
msgstr ""
#: models.py:139
msgid "Tagged Items"
msgstr ""
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
"\" in your expression."
msgstr ""

Binary file not shown.

View File

@ -0,0 +1,67 @@
msgid ""
msgstr ""
"Project-Id-Version: django-taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:26-0700\n"
"PO-Revision-Date: 2014-03-29 18:57+0100\n"
"Last-Translator: Baptiste Darthenay <taggit.batisteo@recursor.net>\n"
"Language-Team: Esperanto <taggit.batisteo@recursor.net>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: eo\n"
"X-Generator: Poedit 1.5.4\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Bonvolu enmeti liston da etikedoj apartitaj per komoj."
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr "Etikedoj"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "Listo da etikedoj apartitaj per komoj."
#: models.py:10
msgid "Name"
msgstr "Nomo"
#: models.py:11
msgid "Slug"
msgstr "Ĵetonvorto"
#: models.py:49
msgid "Tag"
msgstr "Etikedo"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s etikedita %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "Objekto ID"
#: models.py:104 models.py:110
msgid "Content type"
msgstr "Enhavtipo"
#: models.py:138
msgid "Tagged Item"
msgstr "Etikedita elemento"
#: models.py:139
msgid "Tagged Items"
msgstr "Etikeditaj elementoj"
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
"i)\" in your expression."
msgstr ""
"Entajpu validan regulan esprimon. Por ke estu usklecoblinda, enmetu \"(?i)\" "
"en via esprimo."

Binary file not shown.

View File

@ -0,0 +1,68 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Django Taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-06-26 12:47-0500\n"
"PO-Revision-Date: 2010-06-26 12:54-0600\n"
"Last-Translator: Alex <alex.gaynor@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "נא לספק רשימה של תגים מופרדת עם פסיקים."
#: managers.py:41
#: managers.py:113
#: models.py:18
msgid "Tags"
msgstr "תגיות"
#: managers.py:114
msgid "A comma-separated list of tags."
msgstr "רשימה של תגים מופרדת עם פסיקים."
#: models.py:10
msgid "Name"
msgstr "שם"
#: models.py:11
msgid "Slug"
msgstr ""
#: models.py:17
msgid "Tag"
msgstr "תג"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s מתויג עם %(tag)s"
#: models.py:86
msgid "Object id"
msgstr ""
#: models.py:87
msgid "Content type"
msgstr ""
#: models.py:92
msgid "Tagged Item"
msgstr ""
#: models.py:93
msgid "Tagged Items"
msgstr ""
#: contrib/suggest/models.py:57
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
msgstr ""

Binary file not shown.

View File

@ -0,0 +1,70 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-04-13 15:57+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Fornire una lista di tag separati da virgola."
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr "Tag"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "Una lista di tag separati da virgola."
#: models.py:10
msgid "Name"
msgstr "Nome"
#: models.py:11
msgid "Slug"
msgstr "Slug"
#: models.py:49
msgid "Tag"
msgstr "Tag"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s con tag %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "Id Oggetto"
#: models.py:104 models.py:110
msgid "Content type"
msgstr "Tipo"
#: models.py:138
msgid "Tagged Item"
msgstr "Oggetto con tag"
#: models.py:139
msgid "Tagged Items"
msgstr "Oggetti con tag"
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
"\" in your expression."
msgstr ""
"Inserire un'espressione regolare valida. Per renderla indipendente dal maiuscolo e minuscolo, includere \"(?i)"
"\" nell'espressione."

Binary file not shown.

View File

@ -0,0 +1,67 @@
msgid ""
msgstr ""
"Project-Id-Version: django-taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:26-0700\n"
"PO-Revision-Date: 2014-04-23 08:05+0900\n"
"Last-Translator: Tatsuo Ikeda <jp.ne.co.jp@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: ja\n"
"X-Generator: Poedit 1.6.4\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "複数タグはカンマ区切りのリストを入れてください。"
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr "タグ一覧"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "複数タグはカンマ区切りのリスト。"
#: models.py:10
msgid "Name"
msgstr "名称"
#: models.py:11
msgid "Slug"
msgstr "スラッグ"
#: models.py:49
msgid "Tag"
msgstr "タグ"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s tagged with %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "オブジェクト ID"
#: models.py:104 models.py:110
msgid "Content type"
msgstr "コンテンツタイプ"
#: models.py:138
msgid "Tagged Item"
msgstr "タグ付け済みのアイテム"
#: models.py:139
msgid "Tagged Items"
msgstr "タグ付け済みのアイテム一覧"
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
"i)\" in your expression."
msgstr ""
"正しい正規表現を入力してください。 大文字と小文字を区別しないようにするには "
"\"(?i)\" を正規表現に含めてください。"

Binary file not shown.

View File

@ -0,0 +1,72 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.9.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:45-0700\n"
"PO-Revision-Date: 2012-12-08 14:42+0100\n"
"Last-Translator: Bjørn Pettersen <bp@datakortet.no>\n"
"Language-Team: Norwegian <bp@datakortet.no>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.4\n"
"Language: Norwegian\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Vennligst oppgi en kommaseparert tagg-liste."
#: managers.py:39 managers.py:83 models.py:50
msgid "Tags"
msgstr "Tagger"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "En kommaseparert tagg-liste."
#: models.py:10
msgid "Name"
msgstr "Navn"
#: models.py:11
msgid "Slug"
msgstr "Slug"
#: models.py:49
msgid "Tag"
msgstr "Tagg"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s tagget med %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "Objekt-id"
#: models.py:104 models.py:110
msgid "Content type"
msgstr "Innholdstype"
#: models.py:138
msgid "Tagged Item"
msgstr "Tagget Element"
#: models.py:139
msgid "Tagged Items"
msgstr "Taggede Elementer"
#: contrib/suggest/models.py:57
msgid ""
"Enter a valid Regular Expression. To make it case-insensitive include \"(?"
"i)\" in your expression."
msgstr ""
"Skriv et gyldig regulært utrykk (regex). For å gjøre det uavhengig av "
"forskjellen mellom store og små bokstaver må du inkludere \"(?i)\" i din "
"regex."

Binary file not shown.

View File

@ -0,0 +1,63 @@
msgid ""
msgstr ""
"Project-Id-Version: django-taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-09-07 09:45-0700\n"
"PO-Revision-Date: 2010-09-07 23:04+0100\n"
"Last-Translator: Jeffrey Gelens <jeffrey@gelens.org>\n"
"Language-Team: Dutch\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Geef een door komma gescheiden lijst van tags."
#: managers.py:39
#: managers.py:83
#: models.py:50
msgid "Tags"
msgstr "Tags"
#: managers.py:84
msgid "A comma-separated list of tags."
msgstr "Een door komma gescheiden lijst van tags."
#: models.py:10
msgid "Name"
msgstr "Naam"
#: models.py:11
msgid "Slug"
msgstr "Slug"
#: models.py:49
msgid "Tag"
msgstr "Tag"
#: models.py:56
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s getagged met %(tag)s"
#: models.py:100
msgid "Object id"
msgstr "Object-id"
#: models.py:104
#: models.py:110
msgid "Content type"
msgstr "Inhoudstype"
#: models.py:138
msgid "Tagged Item"
msgstr "Object getagged"
#: models.py:139
msgid "Tagged Items"
msgstr "Objecten getagged"
#: contrib/suggest/models.py:57
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
msgstr "Voer een valide reguliere expressie in. Voeg \"(?i)\" aan de expressie toe om deze hoofdletter ongevoelig te maken."

Binary file not shown.

View File

@ -0,0 +1,62 @@
# This file is distributed under WTFPL license.
#
# Translators:
# RPB <r@ifgy.co>, 2013.
msgid ""
msgstr ""
"Project-Id-Version: django-taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-15 22:25-0200\n"
"PO-Revision-Date: 2013-01-12 18:11-0200\n"
"Last-Translator: RPB <r@ifgy.co>\n"
"Language-Team: Portuguese (Brazil) <r@ifgy.co>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
#: forms.py:21
msgid "Please provide a comma-separated list of tags."
msgstr "Favor fornecer uma lista de marcadores separados por vírgula."
#: managers.py:39 models.py:57
msgid "Tags"
msgstr "Marcadores"
#: managers.py:40
msgid "A comma-separated list of tags."
msgstr "Uma lista de marcadores separados por vírgula."
#: models.py:10
msgid "Name"
msgstr "Nome"
#: models.py:11
msgid "Slug"
msgstr "Slug"
#: models.py:56
msgid "Tag"
msgstr "Marcador"
#: models.py:63
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "%(object)s marcados com %(tag)s"
#: models.py:113
msgid "Object id"
msgstr "Id do objeto"
#: models.py:117 models.py:123
msgid "Content type"
msgstr "Tipo de conteúdo"
#: models.py:159
msgid "Tagged Item"
msgstr "Item marcado"
#: models.py:160
msgid "Tagged Items"
msgstr "Itens marcados"

Binary file not shown.

View File

@ -0,0 +1,69 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Django Taggit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-06-11 11:28+0700\n"
"PO-Revision-Date: 2010-06-11 11:30+0700\n"
"Last-Translator: Igor 'idle sign' Starikov <idlesign@yandex.ru>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Poedit-Language: Russian\n"
#: forms.py:20
msgid "Please provide a comma-separated list of tags."
msgstr "Укажите метки через запятую."
#: managers.py:41
#: managers.py:101
#: models.py:17
msgid "Tags"
msgstr "Метки"
#: managers.py:102
msgid "A comma-separated list of tags."
msgstr "Список меток через запятую."
#: models.py:9
msgid "Name"
msgstr "Название"
#: models.py:10
msgid "Slug"
msgstr "Слаг"
#: models.py:16
msgid "Tag"
msgstr "Метка"
#: models.py:55
#, python-format
msgid "%(object)s tagged with %(tag)s"
msgstr "элемент «%(object)s» с меткой «%(tag)s»"
#: models.py:82
msgid "Object id"
msgstr "ID объекта"
#: models.py:83
msgid "Content type"
msgstr "Тип содержимого"
#: models.py:87
msgid "Tagged Item"
msgstr "Элемент с меткой"
#: models.py:88
msgid "Tagged Items"
msgstr "Элементы с меткой"
#: contrib/suggest/models.py:57
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
msgstr "Введите регулярное выражение. Чтобы сделать его чувствительным к регистру укажите \"(?i)\"."

592
taggit/managers.py Normal file
View File

@ -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)

View File

@ -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,),
),
]

View File

@ -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')]),
),
]

View File

@ -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)

225
taggit/models.py Normal file
View File

@ -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"],
]

View File

@ -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']

View File

@ -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']

View File

137
taggit/utils.py Normal file
View File

@ -0,0 +1,137 @@
from __future__ import unicode_literals
from django import VERSION
from django.utils import six
from django.utils.encoding import force_text
from django.utils.functional import wraps
def _get_field(model, name):
if VERSION < (1, 8):
return model._meta.get_field_by_name(name)[0]
else:
return model._meta.get_field(name)
def parse_tags(tagstring):
"""
Parses tag input, with multiple word input being activated and
delineated by commas and double quotes. Quotes take precedence, so
they may contain commas.
Returns a sorted list of unique tag names.
Ported from Jonathan Buchanan's `django-tagging
<http://django-tagging.googlecode.com/>`_
"""
if not tagstring:
return []
tagstring = force_text(tagstring)
# Special case - if there are no commas or double quotes in the
# input, we don't *do* a recall... I mean, we know we only need to
# split on spaces.
if ',' not in tagstring and '"' not in tagstring:
words = list(set(split_strip(tagstring, ' ')))
words.sort()
return words
words = []
buffer = []
# Defer splitting of non-quoted sections until we know if there are
# any unquoted commas.
to_be_split = []
saw_loose_comma = False
open_quote = False
i = iter(tagstring)
try:
while True:
c = six.next(i)
if c == '"':
if buffer:
to_be_split.append(''.join(buffer))
buffer = []
# Find the matching quote
open_quote = True
c = six.next(i)
while c != '"':
buffer.append(c)
c = six.next(i)
if buffer:
word = ''.join(buffer).strip()
if word:
words.append(word)
buffer = []
open_quote = False
else:
if not saw_loose_comma and c == ',':
saw_loose_comma = True
buffer.append(c)
except StopIteration:
# If we were parsing an open quote which was never closed treat
# the buffer as unquoted.
if buffer:
if open_quote and ',' in buffer:
saw_loose_comma = True
to_be_split.append(''.join(buffer))
if to_be_split:
if saw_loose_comma:
delimiter = ','
else:
delimiter = ' '
for chunk in to_be_split:
words.extend(split_strip(chunk, delimiter))
words = list(set(words))
words.sort()
return words
def split_strip(string, delimiter=','):
"""
Splits ``string`` on ``delimiter``, stripping each resulting string
and returning a list of non-empty strings.
Ported from Jonathan Buchanan's `django-tagging
<http://django-tagging.googlecode.com/>`_
"""
if not string:
return []
words = [w.strip() for w in string.split(delimiter)]
return [w for w in words if w]
def edit_string_for_tags(tags):
"""
Given list of ``Tag`` instances, creates a string representation of
the list suitable for editing by the user, such that submitting the
given string representation back without changing it will give the
same list of tags.
Tag names which contain commas will be double quoted.
If any tag name which isn't being quoted contains whitespace, the
resulting string of tag names will be comma-delimited, otherwise
it will be space-delimited.
Ported from Jonathan Buchanan's `django-tagging
<http://django-tagging.googlecode.com/>`_
"""
names = []
for tag in tags:
name = tag.name
if ',' in name or ' ' in name:
names.append('"%s"' % name)
else:
names.append(name)
return ', '.join(sorted(names))
def require_instance_manager(func):
@wraps(func)
def inner(self, *args, **kwargs):
if self.instance is None:
raise TypeError("Can't call %s with a non-instance manager" % func.__name__)
return func(self, *args, **kwargs)
return inner

20
taggit/views.py Normal file
View File

@ -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)

0
tests/__init__.py Normal file
View File

36
tests/forms.py Normal file
View File

@ -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

View File

@ -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,
),
]

View File

228
tests/models.py Normal file
View File

@ -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

659
tests/tests.py Normal file
View File

@ -0,0 +1,659 @@
from __future__ import absolute_import, unicode_literals
from unittest import TestCase as UnitTestCase
import django
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import connection, models
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
from django.utils.encoding import force_text
from .forms import (CustomPKFoodForm, DirectCustomPKFoodForm, DirectFoodForm,
FoodForm, OfficialFoodForm)
from .models import (Article, Child, CustomManager, CustomPKFood,
CustomPKHousePet, CustomPKPet, DirectCustomPKFood,
DirectCustomPKHousePet, DirectCustomPKPet, DirectFood,
DirectHousePet, DirectPet, Food, HousePet, Movie,
OfficialFood, OfficialHousePet, OfficialPet,
OfficialTag, OfficialThroughModel, Pet, Photo,
TaggedCustomPK, TaggedCustomPKFood, TaggedCustomPKPet,
TaggedFood, TaggedPet)
from taggit.managers import _model_name, _TaggableManager, TaggableManager
from taggit.models import Tag, TaggedItem
from taggit.utils import edit_string_for_tags, parse_tags
try:
from unittest import skipIf, skipUnless
except ImportError:
from django.utils.unittest import skipIf, skipUnless
class BaseTaggingTest(object):
def assert_tags_equal(self, qs, tags, sort=True, attr="name"):
got = [getattr(obj, attr) for obj in qs]
if sort:
got.sort()
tags.sort()
self.assertEqual(got, tags)
def _get_form_str(self, form_str):
if django.VERSION >= (1, 3):
form_str %= {
"help_start": '<span class="helptext">',
"help_stop": "</span>"
}
else:
form_str %= {
"help_start": "",
"help_stop": ""
}
return form_str
def assert_form_renders(self, form, html):
self.assertHTMLEqual(str(form), self._get_form_str(html))
class BaseTaggingTestCase(TestCase, BaseTaggingTest):
pass
class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
pass
class TagModelTestCase(BaseTaggingTransactionTestCase):
food_model = Food
tag_model = Tag
def test_unique_slug(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("Red", "red")
def test_update(self):
special = self.tag_model.objects.create(name="special")
special.save()
def test_add(self):
apple = self.food_model.objects.create(name="apple")
yummy = self.tag_model.objects.create(name="yummy")
apple.tags.add(yummy)
def test_slugify(self):
a = Article.objects.create(title="django-taggit 1.0 Released")
a.tags.add("awesome", "release", "AWESOME")
self.assert_tags_equal(a.tags.all(), [
"category-awesome",
"category-release",
"category-awesome-1"
], attr="slug")
def test_integers(self):
"""Adding an integer as a tag should raise a ValueError (#237)."""
apple = self.food_model.objects.create(name="apple")
with self.assertRaisesRegexp(ValueError, (
r"Cannot add 1 \(<(type|class) 'int'>\). "
r"Expected <class 'django.db.models.base.ModelBase'> or str.")):
apple.tags.add(1)
class TagModelDirectTestCase(TagModelTestCase):
food_model = DirectFood
tag_model = Tag
class TagModelDirectCustomPKTestCase(TagModelTestCase):
food_model = DirectCustomPKFood
tag_model = Tag
class TagModelCustomPKTestCase(TagModelTestCase):
food_model = CustomPKFood
tag_model = Tag
class TagModelOfficialTestCase(TagModelTestCase):
food_model = OfficialFood
tag_model = OfficialTag
class TaggableManagerTestCase(BaseTaggingTestCase):
food_model = Food
pet_model = Pet
housepet_model = HousePet
taggeditem_model = TaggedItem
tag_model = Tag
def test_add_tag(self):
apple = self.food_model.objects.create(name="apple")
self.assertEqual(list(apple.tags.all()), [])
self.assertEqual(list(self.food_model.tags.all()), [])
apple.tags.add('green')
self.assert_tags_equal(apple.tags.all(), ['green'])
self.assert_tags_equal(self.food_model.tags.all(), ['green'])
pear = self.food_model.objects.create(name="pear")
pear.tags.add('green')
self.assert_tags_equal(pear.tags.all(), ['green'])
self.assert_tags_equal(self.food_model.tags.all(), ['green'])
apple.tags.add('red')
self.assert_tags_equal(apple.tags.all(), ['green', 'red'])
self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
self.assert_tags_equal(
self.food_model.tags.most_common(),
['green', 'red'],
sort=False
)
apple.tags.remove('green')
self.assert_tags_equal(apple.tags.all(), ['red'])
self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
tag = self.tag_model.objects.create(name="delicious")
apple.tags.add(tag)
self.assert_tags_equal(apple.tags.all(), ["red", "delicious"])
apple.delete()
self.assert_tags_equal(self.food_model.tags.all(), ["green"])
def test_add_queries(self):
# Prefill content type cache:
ContentType.objects.get_for_model(self.food_model)
apple = self.food_model.objects.create(name="apple")
# 1 query to see which tags exist
# + 3 queries to create the tags.
# + 6 queries to create the intermediary things (including SELECTs, to
# make sure we don't double create.
# + 12 on Django 1.6 for save points.
queries = 22
if django.VERSION < (1, 6):
queries -= 12
self.assertNumQueries(queries, apple.tags.add, "red", "delicious", "green")
pear = self.food_model.objects.create(name="pear")
# 1 query to see which tags exist
# + 4 queries to create the intermeidary things (including SELECTs, to
# make sure we dont't double create.
# + 4 on Django 1.6 for save points.
queries = 9
if django.VERSION < (1, 6):
queries -= 4
self.assertNumQueries(queries, pear.tags.add, "green", "delicious")
self.assertNumQueries(0, pear.tags.add)
def test_require_pk(self):
food_instance = self.food_model()
self.assertRaises(ValueError, lambda: food_instance.tags.all())
def test_delete_obj(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("red")
self.assert_tags_equal(apple.tags.all(), ["red"])
strawberry = self.food_model.objects.create(name="strawberry")
strawberry.tags.add("red")
apple.delete()
self.assert_tags_equal(strawberry.tags.all(), ["red"])
def test_delete_bulk(self):
apple = self.food_model.objects.create(name="apple")
kitty = self.pet_model.objects.create(pk=apple.pk, name="kitty")
apple.tags.add("red", "delicious", "fruit")
kitty.tags.add("feline")
self.food_model.objects.all().delete()
self.assert_tags_equal(kitty.tags.all(), ["feline"])
def test_lookup_by_tag(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("red", "green")
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green")
self.assertEqual(
list(self.food_model.objects.filter(tags__name__in=["red"])),
[apple]
)
self.assertEqual(
list(self.food_model.objects.filter(tags__name__in=["green"])),
[apple, pear]
)
kitty = self.pet_model.objects.create(name="kitty")
kitty.tags.add("fuzzy", "red")
dog = self.pet_model.objects.create(name="dog")
dog.tags.add("woof", "red")
self.assertEqual(
list(self.food_model.objects.filter(tags__name__in=["red"]).distinct()),
[apple]
)
tag = self.tag_model.objects.get(name="woof")
self.assertEqual(list(self.pet_model.objects.filter(tags__in=[tag])), [dog])
cat = self.housepet_model.objects.create(name="cat", trained=True)
cat.tags.add("fuzzy")
pks = self.pet_model.objects.filter(tags__name__in=["fuzzy"])
model_name = self.pet_model.__name__
self.assertQuerysetEqual(pks,
['<{0}: kitty>'.format(model_name),
'<{0}: cat>'.format(model_name)],
ordered=False)
def test_lookup_bulk(self):
apple = self.food_model.objects.create(name="apple")
pear = self.food_model.objects.create(name="pear")
apple.tags.add('fruit', 'green')
pear.tags.add('fruit', 'yummie')
def lookup_qs():
# New fix: directly allow WHERE object_id IN (SELECT id FROM ..)
objects = self.food_model.objects.all()
lookup = self.taggeditem_model.bulk_lookup_kwargs(objects)
list(self.taggeditem_model.objects.filter(**lookup))
def lookup_list():
# Simulate old situation: iterate over a list.
objects = list(self.food_model.objects.all())
lookup = self.taggeditem_model.bulk_lookup_kwargs(objects)
list(self.taggeditem_model.objects.filter(**lookup))
self.assertNumQueries(1, lookup_qs)
self.assertNumQueries(2, lookup_list)
def test_exclude(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("red", "green", "delicious")
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green", "delicious")
self.food_model.objects.create(name="guava")
pks = self.food_model.objects.exclude(tags__name__in=["red"])
model_name = self.food_model.__name__
self.assertQuerysetEqual(pks,
['<{0}: pear>'.format(model_name),
'<{0}: guava>'.format(model_name)],
ordered=False)
def test_similarity_by_tag(self):
"""Test that pears are more similar to apples than watermelons"""
apple = self.food_model.objects.create(name="apple")
apple.tags.add("green", "juicy", "small", "sour")
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green", "juicy", "small", "sweet")
watermelon = self.food_model.objects.create(name="watermelon")
watermelon.tags.add("green", "juicy", "large", "sweet")
similar_objs = apple.tags.similar_objects()
self.assertEqual(similar_objs, [pear, watermelon])
self.assertEqual([obj.similar_tags for obj in similar_objs],
[3, 2])
def test_tag_reuse(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("juicy", "juicy")
self.assert_tags_equal(apple.tags.all(), ['juicy'])
def test_query_traverse(self):
spot = self.pet_model.objects.create(name='Spot')
spike = self.pet_model.objects.create(name='Spike')
spot.tags.add('scary')
spike.tags.add('fluffy')
lookup_kwargs = {
'%s__name' % _model_name(self.pet_model): 'Spot'
}
self.assert_tags_equal(
self.tag_model.objects.filter(**lookup_kwargs),
['scary']
)
def test_taggeditem_unicode(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("juicy")
self.assertEqual(
force_text(self.taggeditem_model.objects.all()[0]),
"apple tagged with juicy"
)
def test_abstract_subclasses(self):
p = Photo.objects.create()
p.tags.add("outdoors", "pretty")
self.assert_tags_equal(
p.tags.all(),
["outdoors", "pretty"]
)
m = Movie.objects.create()
m.tags.add("hd")
self.assert_tags_equal(
m.tags.all(),
["hd"],
)
def test_field_api(self):
# Check if tag field, which simulates m2m, has django-like api.
field = self.food_model._meta.get_field('tags')
self.assertTrue(hasattr(field, 'rel'))
self.assertTrue(hasattr(field.rel, 'to'))
self.assertTrue(hasattr(field, 'related'))
# This API has changed in Django 1.8
# https://code.djangoproject.com/ticket/21414
if django.VERSION >= (1, 8):
self.assertEqual(self.food_model, field.model)
self.assertEqual(self.tag_model, field.related.model)
else:
self.assertEqual(self.food_model, field.related.model)
def test_names_method(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add('green')
apple.tags.add('red')
self.assertEqual(list(apple.tags.names()), ['green', 'red'])
def test_slugs_method(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add('green and juicy')
apple.tags.add('red')
self.assertEqual(list(apple.tags.slugs()), ['green-and-juicy', 'red'])
def test_serializes(self):
apple = self.food_model.objects.create(name="apple")
serializers.serialize("json", (apple,))
def test_prefetch_related(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add('1', '2')
orange = self.food_model.objects.create(name="orange")
orange.tags.add('2', '4')
with self.assertNumQueries(2):
l = list(self.food_model.objects.prefetch_related('tags').all())
with self.assertNumQueries(0):
foods = dict((f.name, set(t.name for t in f.tags.all())) for f in l)
self.assertEqual(foods, {
'orange': set(['2', '4']),
'apple': set(['1', '2'])
})
def test_internal_type_is_manytomany(self):
self.assertEqual(
TaggableManager().get_internal_type(), 'ManyToManyField'
)
def test_prefetch_no_extra_join(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add('1', '2')
with self.assertNumQueries(2):
l = list(self.food_model.objects.prefetch_related('tags').all())
join_clause = 'INNER JOIN "%s"' % self.taggeditem_model._meta.db_table
self.assertEqual(connection.queries[-1]['sql'].count(join_clause), 1, connection.queries[-2:])
@override_settings(TAGGIT_CASE_INSENSITIVE=True)
def test_with_case_insensitive_option(self):
spain = self.tag_model.objects.create(name="Spain", slug="spain")
orange = self.food_model.objects.create(name="orange")
orange.tags.add('spain')
self.assertEqual(list(orange.tags.all()), [spain])
class TaggableManagerDirectTestCase(TaggableManagerTestCase):
food_model = DirectFood
pet_model = DirectPet
housepet_model = DirectHousePet
taggeditem_model = TaggedFood
class TaggableManagerDirectCustomPKTestCase(TaggableManagerTestCase):
food_model = DirectCustomPKFood
pet_model = DirectCustomPKPet
housepet_model = DirectCustomPKHousePet
taggeditem_model = TaggedCustomPKFood
def test_require_pk(self):
# TODO with a charfield pk, pk is never None, so taggit has no way to
# tell if the instance is saved or not
pass
class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
food_model = CustomPKFood
pet_model = CustomPKPet
housepet_model = CustomPKHousePet
taggeditem_model = TaggedCustomPK
def test_require_pk(self):
# TODO with a charfield pk, pk is never None, so taggit has no way to
# tell if the instance is saved or not
pass
class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
food_model = OfficialFood
pet_model = OfficialPet
housepet_model = OfficialHousePet
taggeditem_model = OfficialThroughModel
tag_model = OfficialTag
def test_extra_fields(self):
self.tag_model.objects.create(name="red")
self.tag_model.objects.create(name="delicious", official=True)
apple = self.food_model.objects.create(name="apple")
apple.tags.add("delicious", "red")
pear = self.food_model.objects.create(name="Pear")
pear.tags.add("delicious")
self.assertEqual(apple, self.food_model.objects.get(tags__official=False))
def test_get_tags_with_count(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("red", "green", "delicious")
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green", "delicious")
tag_info = self.tag_model.objects.filter(officialfood__in=[apple.id, pear.id],name='green').annotate(models.Count('name'))
self.assertEqual(tag_info[0].name__count, 2)
class TaggableManagerInitializationTestCase(TaggableManagerTestCase):
"""Make sure manager override defaults and sets correctly."""
food_model = Food
custom_manager_model = CustomManager
def test_default_manager(self):
self.assertEqual(self.food_model.tags.__class__, _TaggableManager)
def test_custom_manager(self):
self.assertEqual(self.custom_manager_model.tags.__class__, CustomManager.Foo)
class TaggableFormTestCase(BaseTaggingTestCase):
form_class = FoodForm
food_model = Food
def test_form(self):
self.assertEqual(list(self.form_class.base_fields), ['name', 'tags'])
f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'})
self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
f.save()
apple = self.food_model.objects.get(name='apple')
self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy'])
f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy, delicious'}, instance=apple)
f.save()
apple = self.food_model.objects.get(name='apple')
self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy', 'delicious'])
self.assertEqual(self.food_model.objects.count(), 1)
f = self.form_class({"name": "raspberry"})
self.assertFalse(f.is_valid())
f = self.form_class(instance=apple)
self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
apple.tags.add('has,comma')
f = self.form_class(instance=apple)
self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
apple.tags.add('has space')
f = self.form_class(instance=apple)
self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has space&quot;, &quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
def test_formfield(self):
tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
ff = tm.formfield()
self.assertEqual(ff.label, 'Categories')
self.assertEqual(ff.help_text, 'Add some categories')
self.assertEqual(ff.required, False)
self.assertEqual(ff.clean(""), [])
tm = TaggableManager()
ff = tm.formfield()
self.assertRaises(ValidationError, ff.clean, "")
class TaggableFormDirectTestCase(TaggableFormTestCase):
form_class = DirectFoodForm
food_model = DirectFood
class TaggableFormDirectCustomPKTestCase(TaggableFormTestCase):
form_class = DirectCustomPKFoodForm
food_model = DirectCustomPKFood
class TaggableFormCustomPKTestCase(TaggableFormTestCase):
form_class = CustomPKFoodForm
food_model = CustomPKFood
class TaggableFormOfficialTestCase(TaggableFormTestCase):
form_class = OfficialFoodForm
food_model = OfficialFood
class TagStringParseTestCase(UnitTestCase):
"""
Ported from Jonathan Buchanan's `django-tagging
<http://django-tagging.googlecode.com/>`_
"""
def test_with_simple_space_delimited_tags(self):
"""
Test with simple space-delimited tags.
"""
self.assertEqual(parse_tags('one'), ['one'])
self.assertEqual(parse_tags('one two'), ['one', 'two'])
self.assertEqual(parse_tags('one two three'), ['one', 'three', 'two'])
self.assertEqual(parse_tags('one one two two'), ['one', 'two'])
def test_with_comma_delimited_multiple_words(self):
"""
Test with comma-delimited multiple words.
An unquoted comma in the input will trigger this.
"""
self.assertEqual(parse_tags(',one'), ['one'])
self.assertEqual(parse_tags(',one two'), ['one two'])
self.assertEqual(parse_tags(',one two three'), ['one two three'])
self.assertEqual(parse_tags('a-one, a-two and a-three'),
['a-one', 'a-two and a-three'])
def test_with_double_quoted_multiple_words(self):
"""
Test with double-quoted multiple words.
A completed quote will trigger this. Unclosed quotes are ignored.
"""
self.assertEqual(parse_tags('"one'), ['one'])
self.assertEqual(parse_tags('"one two'), ['one', 'two'])
self.assertEqual(parse_tags('"one two three'), ['one', 'three', 'two'])
self.assertEqual(parse_tags('"one two"'), ['one two'])
self.assertEqual(parse_tags('a-one "a-two and a-three"'),
['a-one', 'a-two and a-three'])
def test_with_no_loose_commas(self):
"""
Test with no loose commas -- split on spaces.
"""
self.assertEqual(parse_tags('one two "thr,ee"'), ['one', 'thr,ee', 'two'])
def test_with_loose_commas(self):
"""
Loose commas - split on commas
"""
self.assertEqual(parse_tags('"one", two three'), ['one', 'two three'])
def test_tags_with_double_quotes_can_contain_commas(self):
"""
Double quotes can contain commas
"""
self.assertEqual(parse_tags('a-one "a-two, and a-three"'),
['a-one', 'a-two, and a-three'])
self.assertEqual(parse_tags('"two", one, one, two, "one"'),
['one', 'two'])
def test_with_naughty_input(self):
"""
Test with naughty input.
"""
# Bad users! Naughty users!
self.assertEqual(parse_tags(None), [])
self.assertEqual(parse_tags(''), [])
self.assertEqual(parse_tags('"'), [])
self.assertEqual(parse_tags('""'), [])
self.assertEqual(parse_tags('"' * 7), [])
self.assertEqual(parse_tags(',,,,,,'), [])
self.assertEqual(parse_tags('",",",",",",","'), [','])
self.assertEqual(parse_tags('a-one "a-two" and "a-three'),
['a-one', 'a-three', 'a-two', 'and'])
def test_recreation_of_tag_list_string_representations(self):
plain = Tag.objects.create(name='plain')
spaces = Tag.objects.create(name='spa ces')
comma = Tag.objects.create(name='com,ma')
self.assertEqual(edit_string_for_tags([plain]), 'plain')
self.assertEqual(edit_string_for_tags([plain, spaces]), '"spa ces", plain')
self.assertEqual(edit_string_for_tags([plain, spaces, comma]), '"com,ma", "spa ces", plain')
self.assertEqual(edit_string_for_tags([plain, comma]), '"com,ma", plain')
self.assertEqual(edit_string_for_tags([comma, spaces]), '"com,ma", "spa ces"')
@skipIf(django.VERSION < (1, 7), "not relevant for Django < 1.7")
class DeconstructTestCase(UnitTestCase):
def test_deconstruct_kwargs_kept(self):
instance = TaggableManager(through=OfficialThroughModel, to='dummy.To')
name, path, args, kwargs = instance.deconstruct()
new_instance = TaggableManager(*args, **kwargs)
self.assertEqual('tests.OfficialThroughModel', new_instance.rel.through)
self.assertEqual('dummy.To', new_instance.rel.to)
@skipUnless(django.VERSION < (1, 7), "test only applies to 1.6 and below")
class SouthSupportTests(TestCase):
def test_import_migrations_module(self):
try:
from taggit.migrations import __doc__ # noqa
except ImproperlyConfigured as e:
exception = e
self.assertIn("SOUTH_MIGRATION_MODULES", exception.args[0])
class InheritedPrefetchTests(TestCase):
def test_inherited_tags_with_prefetch(self):
child = Child()
child.save()
child.tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
child = Child.objects.get()
no_prefetch_tags = child.tags.all()
self.assertEquals(4, no_prefetch_tags.count())
child = Child.objects.prefetch_related('tags').get()
prefetch_tags = child.tags.all()
self.assertEquals(4, prefetch_tags.count())
self.assertEquals(set([t.name for t in no_prefetch_tags]),
set([t.name for t in prefetch_tags]))

129
tox.ini Normal file
View File

@ -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}