зеркало из https://github.com/mozilla/kitsune.git
[bug 972522] Upgrade to django-taggit v0.11.2.
* Also, converted the dependency to a git submodule. * Includes migration to add taggit to south.
This commit is contained in:
Родитель
b5027306b5
Коммит
2cfb59c42b
|
@ -172,3 +172,6 @@
|
|||
[submodule "vendor/src/django-axes"]
|
||||
path = vendor/src/django-axes
|
||||
url = https://github.com/django-security/django-axes.git
|
||||
[submodule "vendor/src/django-taggit"]
|
||||
path = vendor/src/django-taggit
|
||||
url = https://github.com/alex/django-taggit.git
|
||||
|
|
|
@ -6,6 +6,10 @@ from django.db import models
|
|||
|
||||
class Migration(DataMigration):
|
||||
|
||||
depends_on = (
|
||||
('taggit', "0001_initial"),
|
||||
)
|
||||
|
||||
def forwards(self, orm):
|
||||
"""Unescalate questions over a week old."""
|
||||
# Get the escalate tag.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO `south_migrationhistory` (app_name, migration, applied) VALUES
|
||||
('taggit','0001_initial','2014-02-21 11:38:42');
|
|
@ -10,7 +10,6 @@ packages/pylint
|
|||
packages/ipython
|
||||
packages/pyflakes
|
||||
packages/amqp
|
||||
packages/django-taggit
|
||||
packages/anyjson
|
||||
packages/carrot
|
||||
packages/pyquery
|
||||
|
@ -98,3 +97,4 @@ src/billiard
|
|||
src/django-statici18n/src
|
||||
src/django-appconf
|
||||
src/django-axes
|
||||
src/django-taggit
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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
|
|
@ -1,28 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
recursive-include docs *.txt
|
||||
recursive-include taggit/locale *
|
|
@ -1,52 +0,0 @@
|
|||
Metadata-Version: 1.0
|
||||
Name: django-taggit
|
||||
Version: 0.9.2
|
||||
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: UNKNOWN
|
||||
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::
|
||||
|
||||
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::
|
||||
|
||||
>>> 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.1 or greater.
|
||||
|
||||
For more info checkout out the documentation. And for questions about usage or
|
||||
development you can contact the
|
||||
`mailinglist <http://groups.google.com/group/django-taggit>`_.
|
||||
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Framework :: Django
|
|
@ -1,35 +0,0 @@
|
|||
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::
|
||||
|
||||
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::
|
||||
|
||||
>>> 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.1 or greater.
|
||||
|
||||
For more info checkout out the documentation. And for questions about usage or
|
||||
development you can contact the
|
||||
`mailinglist <http://groups.google.com/group/django-taggit>`_.
|
|
@ -1,89 +0,0 @@
|
|||
# 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."
|
|
@ -1,14 +0,0 @@
|
|||
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.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display>`_.
|
|
@ -1,88 +0,0 @@
|
|||
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.
|
||||
|
||||
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 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,44 +0,0 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
0.9.2
|
||||
~~~~~
|
||||
|
||||
* *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
|
||||
~~~~~
|
||||
|
||||
* 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
|
||||
~~~~~
|
||||
|
||||
* 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.
|
|
@ -1,198 +0,0 @@
|
|||
# -*- 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, Alex Gaynor'
|
||||
|
||||
# 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.9.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.9.2'
|
||||
|
||||
# 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}
|
|
@ -1,58 +0,0 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
To change the behavior in other ways there are a number of other classes you
|
||||
can subclass to obtain different behavior:
|
||||
|
||||
========================= ===========================================================
|
||||
Class name Behavior
|
||||
========================= ===========================================================
|
||||
``TaggedItemBase`` Allows custom ``ForeignKeys`` to models.
|
||||
``GenericTaggedItemBase`` Allows custom ``Tag`` models.
|
||||
``ItemBase`` Allows custom ``Tag`` models and ``ForeignKeys`` to models.
|
||||
========================= ===========================================================
|
||||
|
||||
When providing a custom ``Tag`` model it should be a ``ForeignKey`` to your tag model named ``"tag"``.
|
||||
|
||||
.. 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.
|
|
@ -1,26 +0,0 @@
|
|||
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`__.
|
||||
|
||||
__ http://github.com/alex/django-taggit
|
||||
__ http://github.com/frankwiles/django-taggit-suggest
|
||||
__ http://github.com/feuervogel/django-taggit-templatetags
|
|
@ -1,51 +0,0 @@
|
|||
.. _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()
|
|
@ -1,22 +0,0 @@
|
|||
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.
|
||||
|
||||
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()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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.1 and 1.2 (see :doc:`issues` for known
|
||||
issues with older versions of Django), and Python 2.4-2.X.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
getting_started
|
||||
forms
|
||||
admin
|
||||
api
|
||||
custom_tagging
|
||||
issues
|
||||
external_apps
|
||||
changelog
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
|
@ -1,9 +0,0 @@
|
|||
Known Issues
|
||||
============
|
||||
|
||||
Currently there is 1 known issue:
|
||||
|
||||
* When run under Django 1.1, doing ``Model.objects.all().delete()`` (or any
|
||||
bulk deletion operation) on a model with a ``TaggableManager`` will result
|
||||
in losing the tags for items beyond just those assosciated with the deleted
|
||||
objects. This issue is not present in Django 1.2.
|
|
@ -1,37 +0,0 @@
|
|||
import os
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
from taggit import VERSION
|
||||
|
||||
|
||||
f = open(os.path.join(os.path.dirname(__file__), 'README.txt'))
|
||||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
setup(
|
||||
name='django-taggit',
|
||||
version=".".join(map(str, 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(),
|
||||
zip_safe=False,
|
||||
package_data = {
|
||||
'taggit': [
|
||||
'locale/*/LC_MESSAGES/*',
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Framework :: Django',
|
||||
],
|
||||
test_suite='taggit.tests.runtests.runtests'
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
VERSION = (0, 9, 2)
|
|
@ -1,16 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
|
||||
class TaggedItemInline(admin.StackedInline):
|
||||
model = TaggedItem
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
list_display = ["name"]
|
||||
inlines = [
|
||||
TaggedItemInline
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(Tag, TagAdmin)
|
|
@ -1,21 +0,0 @@
|
|||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from taggit.utils import parse_tags, edit_string_for_tags
|
||||
|
||||
|
||||
class TagWidget(forms.TextInput):
|
||||
def render(self, name, value, attrs=None):
|
||||
if value is not None and not isinstance(value, basestring):
|
||||
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."))
|
|
@ -1,244 +0,0 @@
|
|||
from django.contrib.contenttypes.generic import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import ManyToManyRel, RelatedField, add_lazy_relation
|
||||
from django.db.models.related import RelatedObject
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from taggit.forms import TagField
|
||||
from taggit.models import TaggedItem, GenericTaggedItemBase
|
||||
from taggit.utils import require_instance_manager
|
||||
|
||||
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
# 2.4 compat
|
||||
try:
|
||||
from django.utils.itercompat import all
|
||||
except ImportError:
|
||||
# 1.1.X compat
|
||||
def all(iterable):
|
||||
for item in iterable:
|
||||
if not item:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class TaggableRel(ManyToManyRel):
|
||||
def __init__(self):
|
||||
self.related_name = None
|
||||
self.limit_choices_to = {}
|
||||
self.symmetrical = True
|
||||
self.multiple = True
|
||||
self.through = None
|
||||
|
||||
|
||||
class TaggableManager(RelatedField):
|
||||
def __init__(self, verbose_name=_("Tags"),
|
||||
help_text=_("A comma-separated list of tags."), through=None, blank=False):
|
||||
self.through = through or TaggedItem
|
||||
self.rel = TaggableRel()
|
||||
self.verbose_name = verbose_name
|
||||
self.help_text = help_text
|
||||
self.blank = blank
|
||||
self.editable = True
|
||||
self.unique = False
|
||||
self.creates_table = False
|
||||
self.db_column = None
|
||||
self.choices = None
|
||||
self.serialize = False
|
||||
self.null = True
|
||||
self.creation_counter = models.Field.creation_counter
|
||||
models.Field.creation_counter += 1
|
||||
|
||||
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 = _TaggableManager(
|
||||
through=self.through, model=model, instance=instance
|
||||
)
|
||||
return manager
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
self.name = self.column = name
|
||||
self.model = cls
|
||||
cls._meta.add_field(self)
|
||||
setattr(cls, name, self)
|
||||
if not cls._meta.abstract:
|
||||
if isinstance(self.through, basestring):
|
||||
def resolve_related_class(field, model, cls):
|
||||
self.through = model
|
||||
self.post_through_setup(cls)
|
||||
add_lazy_relation(
|
||||
cls, self, self.through, resolve_related_class
|
||||
)
|
||||
else:
|
||||
self.post_through_setup(cls)
|
||||
|
||||
def post_through_setup(self, cls):
|
||||
self.use_gfk = (
|
||||
self.through is None or issubclass(self.through, GenericTaggedItemBase)
|
||||
)
|
||||
self.rel.to = self.through._meta.get_field("tag").rel.to
|
||||
if self.use_gfk:
|
||||
tagged_items = GenericRelation(self.through)
|
||||
tagged_items.contribute_to_class(cls, "tagged_items")
|
||||
|
||||
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 self.model._meta.module_name
|
||||
|
||||
def m2m_reverse_name(self):
|
||||
return self.through._meta.get_field_by_name("tag")[0].column
|
||||
|
||||
def m2m_target_field_name(self):
|
||||
return self.model._meta.pk.name
|
||||
|
||||
def m2m_reverse_target_field_name(self):
|
||||
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 extra_filters(self, pieces, pos, negate):
|
||||
if negate or not self.use_gfk:
|
||||
return []
|
||||
prefix = "__".join(["tagged_items"] + pieces[:pos-2])
|
||||
cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model))
|
||||
if len(cts) == 1:
|
||||
return [("%s__content_type" % prefix, cts[0])]
|
||||
return [("%s__content_type__in" % prefix, cts)]
|
||||
|
||||
def bulk_related_objects(self, new_objs, using):
|
||||
return []
|
||||
|
||||
|
||||
class _TaggableManager(models.Manager):
|
||||
def __init__(self, through, model, instance):
|
||||
self.through = through
|
||||
self.model = model
|
||||
self.instance = instance
|
||||
|
||||
def get_query_set(self):
|
||||
return self.through.tags_for(self.model, self.instance)
|
||||
|
||||
def _lookup_kwargs(self):
|
||||
return self.through.lookup_kwargs(self.instance)
|
||||
|
||||
@require_instance_manager
|
||||
def add(self, *tags):
|
||||
str_tags = set([
|
||||
t
|
||||
for t in tags
|
||||
if not isinstance(t, self.through.tag_model())
|
||||
])
|
||||
tag_objs = set(tags) - str_tags
|
||||
# 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
|
||||
)
|
||||
tag_objs.update(existing)
|
||||
|
||||
for new_tag in str_tags - set(t.name for t in existing):
|
||||
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 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_query_set().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(*lookup_kwargs.keys())
|
||||
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 = self.through._meta.get_field_by_name(lookup_keys[0])[0]
|
||||
objs = f.rel.to._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.iteritems():
|
||||
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
|
||||
|
||||
|
||||
def _get_subclasses(model):
|
||||
subclasses = [model]
|
||||
for f in model._meta.get_all_field_names():
|
||||
field = model._meta.get_field_by_name(f)[0]
|
||||
if (isinstance(field, RelatedObject) and
|
||||
getattr(field.field.rel, "parent_link", None)):
|
||||
subclasses.extend(_get_subclasses(field.model))
|
||||
return subclasses
|
|
@ -1,160 +0,0 @@
|
|||
import django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||
from django.db import models, IntegrityError, transaction
|
||||
from django.template.defaultfilters import slugify as default_slugify
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
|
||||
|
||||
class TagBase(models.Model):
|
||||
name = models.CharField(verbose_name=_('Name'), max_length=100)
|
||||
slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100)
|
||||
|
||||
def __unicode__(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)
|
||||
if django.VERSION >= (1, 2):
|
||||
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
|
||||
trans_kwargs = {"using": using}
|
||||
else:
|
||||
trans_kwargs = {}
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
try:
|
||||
sid = transaction.savepoint(**trans_kwargs)
|
||||
res = super(TagBase, self).save(*args, **kwargs)
|
||||
transaction.savepoint_commit(sid, **trans_kwargs)
|
||||
return res
|
||||
except IntegrityError:
|
||||
transaction.savepoint_rollback(sid, **trans_kwargs)
|
||||
self.slug = self.slugify(self.name, i)
|
||||
else:
|
||||
return super(TagBase, self).save(*args, **kwargs)
|
||||
|
||||
def slugify(self, tag, i=None):
|
||||
slug = default_slugify(tag)
|
||||
if i is not None:
|
||||
slug += "_%d" % i
|
||||
return slug
|
||||
|
||||
|
||||
class Tag(TagBase):
|
||||
class Meta:
|
||||
verbose_name = _("Tag")
|
||||
verbose_name_plural = _("Tags")
|
||||
|
||||
|
||||
|
||||
class ItemBase(models.Model):
|
||||
def __unicode__(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 cls._meta.get_field_by_name("tag")[0].rel.to
|
||||
|
||||
@classmethod
|
||||
def tag_relname(cls):
|
||||
return cls._meta.get_field_by_name('tag')[0].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):
|
||||
if django.VERSION < (1, 2):
|
||||
tag = models.ForeignKey(Tag, related_name="%(class)s_items")
|
||||
else:
|
||||
tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def tags_for(cls, model, instance=None):
|
||||
if instance is not None:
|
||||
return cls.tag_model().objects.filter(**{
|
||||
'%s__content_object' % cls.tag_relname(): instance
|
||||
})
|
||||
return cls.tag_model().objects.filter(**{
|
||||
'%s__content_object__isnull' % cls.tag_relname(): False
|
||||
}).distinct()
|
||||
|
||||
|
||||
class GenericTaggedItemBase(ItemBase):
|
||||
object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True)
|
||||
if django.VERSION < (1, 2):
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
verbose_name=_('Content type'),
|
||||
related_name="%(class)s_tagged_items"
|
||||
)
|
||||
else:
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
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):
|
||||
# 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):
|
||||
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
|
||||
return cls.tag_model().objects.filter(**kwargs).distinct()
|
||||
|
||||
|
||||
class TaggedItem(GenericTaggedItemBase, TaggedItemBase):
|
||||
class Meta:
|
||||
verbose_name = _("Tagged Item")
|
||||
verbose_name_plural = _("Tagged Items")
|
|
@ -1,20 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood
|
||||
|
||||
|
||||
class FoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Food
|
||||
|
||||
class DirectFoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = DirectFood
|
||||
|
||||
class CustomPKFoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomPKFood
|
||||
|
||||
class OfficialFoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = OfficialFood
|
|
@ -1,143 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
from taggit.models import (TaggedItemBase, GenericTaggedItemBase, TaggedItem,
|
||||
TagBase, Tag)
|
||||
|
||||
|
||||
class Food(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Pet(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class HousePet(Pet):
|
||||
trained = models.BooleanField()
|
||||
|
||||
|
||||
# Test direct-tagging with custom through model
|
||||
|
||||
class TaggedFood(TaggedItemBase):
|
||||
content_object = models.ForeignKey('DirectFood')
|
||||
|
||||
class TaggedPet(TaggedItemBase):
|
||||
content_object = models.ForeignKey('DirectPet')
|
||||
|
||||
class DirectFood(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager(through="TaggedFood")
|
||||
|
||||
class DirectPet(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager(through=TaggedPet)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class DirectHousePet(DirectPet):
|
||||
trained = models.BooleanField()
|
||||
|
||||
|
||||
# Test custom through model to model with custom PK
|
||||
|
||||
class TaggedCustomPKFood(TaggedItemBase):
|
||||
content_object = models.ForeignKey('CustomPKFood')
|
||||
|
||||
class TaggedCustomPKPet(TaggedItemBase):
|
||||
content_object = models.ForeignKey('CustomPKPet')
|
||||
|
||||
class CustomPKFood(models.Model):
|
||||
name = models.CharField(max_length=50, primary_key=True)
|
||||
|
||||
tags = TaggableManager(through=TaggedCustomPKFood)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class CustomPKPet(models.Model):
|
||||
name = models.CharField(max_length=50, primary_key=True)
|
||||
|
||||
tags = TaggableManager(through=TaggedCustomPKPet)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class CustomPKHousePet(CustomPKPet):
|
||||
trained = models.BooleanField()
|
||||
|
||||
# Test custom through model to a custom tag model
|
||||
|
||||
class OfficialTag(TagBase):
|
||||
official = models.BooleanField()
|
||||
|
||||
class OfficialThroughModel(GenericTaggedItemBase):
|
||||
tag = models.ForeignKey(OfficialTag, related_name="tagged_items")
|
||||
|
||||
class OfficialFood(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager(through=OfficialThroughModel)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class OfficialPet(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
tags = TaggableManager(through=OfficialThroughModel)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class OfficialHousePet(OfficialPet):
|
||||
trained = models.BooleanField()
|
||||
|
||||
|
||||
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)
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.configured:
|
||||
settings.configure(
|
||||
DATABASE_ENGINE='sqlite3',
|
||||
INSTALLED_APPS=[
|
||||
'django.contrib.contenttypes',
|
||||
'taggit',
|
||||
'taggit.tests',
|
||||
]
|
||||
)
|
||||
|
||||
from django.test.simple import run_tests
|
||||
|
||||
|
||||
def runtests(*test_args):
|
||||
if not test_args:
|
||||
test_args = ['tests']
|
||||
parent = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"..",
|
||||
)
|
||||
sys.path.insert(0, parent)
|
||||
failures = run_tests(test_args, verbosity=1, interactive=True)
|
||||
sys.exit(failures)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
runtests(*sys.argv[1:])
|
|
@ -1,475 +0,0 @@
|
|||
from unittest import TestCase as UnitTestCase
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
from taggit.models import Tag, TaggedItem
|
||||
from taggit.tests.forms import (FoodForm, DirectFoodForm, CustomPKFoodForm,
|
||||
OfficialFoodForm)
|
||||
from taggit.tests.models import (Food, Pet, HousePet, DirectFood, DirectPet,
|
||||
DirectHousePet, TaggedPet, CustomPKFood, CustomPKPet, CustomPKHousePet,
|
||||
TaggedCustomPKPet, OfficialFood, OfficialPet, OfficialHousePet,
|
||||
OfficialThroughModel, OfficialTag, Photo, Movie, Article)
|
||||
from taggit.utils import parse_tags, edit_string_for_tags
|
||||
|
||||
|
||||
class BaseTaggingTest(object):
|
||||
def assert_tags_equal(self, qs, tags, sort=True, attr="name"):
|
||||
got = map(lambda tag: getattr(tag, attr), qs)
|
||||
if sort:
|
||||
got.sort()
|
||||
tags.sort()
|
||||
self.assertEqual(got, tags)
|
||||
|
||||
def assert_num_queries(self, n, f, *args, **kwargs):
|
||||
original_DEBUG = settings.DEBUG
|
||||
settings.DEBUG = True
|
||||
current = len(connection.queries)
|
||||
try:
|
||||
f(*args, **kwargs)
|
||||
self.assertEqual(
|
||||
len(connection.queries) - current,
|
||||
n,
|
||||
)
|
||||
finally:
|
||||
settings.DEBUG = original_DEBUG
|
||||
|
||||
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.assertEqual(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")
|
||||
|
||||
class TagModelDirectTestCase(TagModelTestCase):
|
||||
food_model = DirectFood
|
||||
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):
|
||||
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.
|
||||
self.assert_num_queries(10, 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.
|
||||
self.assert_num_queries(5, pear.tags.add, "green", "delicious")
|
||||
|
||||
self.assert_num_queries(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")
|
||||
|
||||
self.assertEqual(
|
||||
map(lambda o: o.pk, self.pet_model.objects.filter(tags__name__in=["fuzzy"])),
|
||||
[kitty.pk, cat.pk]
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
guava = self.food_model.objects.create(name="guava")
|
||||
|
||||
self.assertEqual(
|
||||
map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"])),
|
||||
[pear.pk, guava.pk],
|
||||
)
|
||||
|
||||
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(map(lambda x: x.similar_tags, 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' % self.pet_model._meta.module_name: 'Spot'
|
||||
}
|
||||
self.assert_tags_equal(
|
||||
self.tag_model.objects.filter(**lookup_kwargs),
|
||||
['scary']
|
||||
)
|
||||
|
||||
def test_taggeditem_unicode(self):
|
||||
ross = self.pet_model.objects.create(name="ross")
|
||||
# I keep Ross Perot for a pet, what's it to you?
|
||||
ross.tags.add("president")
|
||||
|
||||
self.assertEqual(
|
||||
unicode(self.taggeditem_model.objects.all()[0]),
|
||||
"ross tagged with president"
|
||||
)
|
||||
|
||||
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"],
|
||||
)
|
||||
|
||||
|
||||
class TaggableManagerDirectTestCase(TaggableManagerTestCase):
|
||||
food_model = DirectFood
|
||||
pet_model = DirectPet
|
||||
housepet_model = DirectHousePet
|
||||
taggeditem_model = TaggedPet
|
||||
|
||||
class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
|
||||
food_model = CustomPKFood
|
||||
pet_model = CustomPKPet
|
||||
housepet_model = CustomPKHousePet
|
||||
taggeditem_model = TaggedCustomPKPet
|
||||
|
||||
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(
|
||||
map(lambda o: o.pk, self.food_model.objects.filter(tags__official=False)),
|
||||
[apple.pk],
|
||||
)
|
||||
|
||||
|
||||
class TaggableFormTestCase(BaseTaggingTestCase):
|
||||
form_class = FoodForm
|
||||
food_model = Food
|
||||
|
||||
def test_form(self):
|
||||
self.assertEqual(self.form_class.base_fields.keys(), ['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=""has,comma", 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=""has space", "has,comma", 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, u'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 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'), [u'one'])
|
||||
self.assertEqual(parse_tags('one two'), [u'one', u'two'])
|
||||
self.assertEqual(parse_tags('one two three'), [u'one', u'three', u'two'])
|
||||
self.assertEqual(parse_tags('one one two two'), [u'one', u'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'), [u'one'])
|
||||
self.assertEqual(parse_tags(',one two'), [u'one two'])
|
||||
self.assertEqual(parse_tags(',one two three'), [u'one two three'])
|
||||
self.assertEqual(parse_tags('a-one, a-two and a-three'),
|
||||
[u'a-one', u'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'), [u'one'])
|
||||
self.assertEqual(parse_tags('"one two'), [u'one', u'two'])
|
||||
self.assertEqual(parse_tags('"one two three'), [u'one', u'three', u'two'])
|
||||
self.assertEqual(parse_tags('"one two"'), [u'one two'])
|
||||
self.assertEqual(parse_tags('a-one "a-two and a-three"'),
|
||||
[u'a-one', u'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"'), [u'one', u'thr,ee', u'two'])
|
||||
|
||||
def test_with_loose_commas(self):
|
||||
"""
|
||||
Loose commas - split on commas
|
||||
"""
|
||||
self.assertEqual(parse_tags('"one", two three'), [u'one', u'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"'),
|
||||
[u'a-one', u'a-two, and a-three'])
|
||||
self.assertEqual(parse_tags('"two", one, one, two, "one"'),
|
||||
[u'one', u'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('",",",",",",","'), [u','])
|
||||
self.assertEqual(parse_tags('a-one "a-two" and "a-three'),
|
||||
[u'a-one', u'a-three', u'a-two', u'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]), u'plain')
|
||||
self.assertEqual(edit_string_for_tags([plain, spaces]), u'"spa ces", plain')
|
||||
self.assertEqual(edit_string_for_tags([plain, spaces, comma]), u'"com,ma", "spa ces", plain')
|
||||
self.assertEqual(edit_string_for_tags([plain, comma]), u'"com,ma", plain')
|
||||
self.assertEqual(edit_string_for_tags([comma, spaces]), u'"com,ma", "spa ces"')
|
|
@ -1,126 +0,0 @@
|
|||
from django.utils.encoding import force_unicode
|
||||
from django.utils.functional import wraps
|
||||
|
||||
|
||||
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_unicode(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 u',' not in tagstring and u'"' not in tagstring:
|
||||
words = list(set(split_strip(tagstring, u' ')))
|
||||
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 = i.next()
|
||||
if c == u'"':
|
||||
if buffer:
|
||||
to_be_split.append(u''.join(buffer))
|
||||
buffer = []
|
||||
# Find the matching quote
|
||||
open_quote = True
|
||||
c = i.next()
|
||||
while c != u'"':
|
||||
buffer.append(c)
|
||||
c = i.next()
|
||||
if buffer:
|
||||
word = u''.join(buffer).strip()
|
||||
if word:
|
||||
words.append(word)
|
||||
buffer = []
|
||||
open_quote = False
|
||||
else:
|
||||
if not saw_loose_comma and c == u',':
|
||||
saw_loose_comma = True
|
||||
buffer.append(c)
|
||||
except StopIteration:
|
||||
# If we were parsing an open quote which was never closed treat
|
||||
# the buffer as unquoted.
|
||||
if buffer:
|
||||
if open_quote and u',' in buffer:
|
||||
saw_loose_comma = True
|
||||
to_be_split.append(u''.join(buffer))
|
||||
if to_be_split:
|
||||
if saw_loose_comma:
|
||||
delimiter = u','
|
||||
else:
|
||||
delimiter = u' '
|
||||
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=u','):
|
||||
"""
|
||||
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 u',' in name or u' ' in name:
|
||||
names.append('"%s"' % name)
|
||||
else:
|
||||
names.append(name)
|
||||
return u', '.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
|
|
@ -1,18 +0,0 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic.list_detail import object_list
|
||||
|
||||
from taggit.models import TaggedItem, Tag
|
||||
|
||||
|
||||
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 object_list(request, qs, **kwargs)
|
|
@ -1,44 +0,0 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py24, py25, py26, py27, pypy, py24-trunk, py25-trunk, py26-trunk, py27-trunk, pypy-trunk, docs
|
||||
[testenv]
|
||||
commands =
|
||||
python setup.py test
|
||||
deps =
|
||||
django==1.2.4
|
||||
|
||||
|
||||
# We lied here, these are not really trunk, but rather the 1.3 beta-1, which
|
||||
# is close enough.
|
||||
[testenv:py24-trunk]
|
||||
basepython = python2.4
|
||||
deps =
|
||||
http://www.djangoproject.com/download/1.3-beta-1/tarball/
|
||||
|
||||
[testenv:py25-trunk]
|
||||
basepython = python2.5
|
||||
deps =
|
||||
http://www.djangoproject.com/download/1.3-beta-1/tarball/
|
||||
|
||||
[testenv:py26-trunk]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
http://www.djangoproject.com/download/1.3-beta-1/tarball/
|
||||
|
||||
[testenv:py27-trunk]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
http://www.djangoproject.com/download/1.3-beta-1/tarball/
|
||||
|
||||
[testenv:pypy-trunk]
|
||||
basepython = pypy
|
||||
deps =
|
||||
http://www.djangoproject.com/download/1.3-beta-1/tarball/
|
||||
|
||||
[testenv:docs]
|
||||
changedir = docs
|
||||
deps =
|
||||
sphinx
|
||||
commands =
|
||||
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
|
||||
sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees . {envtmpdir}/linkcheck
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 1f4ffa629c8deddea2f75779b8407c6c97fe3e4b
|
Загрузка…
Ссылка в новой задаче