summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.svn/all-wcprops41
-rw-r--r--.svn/dir-prop-base6
-rw-r--r--.svn/entries238
-rw-r--r--.svn/format1
-rw-r--r--.svn/prop-base/CHANGELOG.txt.svn-base5
-rw-r--r--.svn/prop-base/INSTALL.txt.svn-base5
-rw-r--r--.svn/prop-base/LICENSE.txt.svn-base5
-rw-r--r--.svn/prop-base/MANIFEST.in.svn-base5
-rw-r--r--.svn/prop-base/README.txt.svn-base5
-rw-r--r--.svn/prop-base/setup.py.svn-base5
-rw-r--r--.svn/text-base/CHANGELOG.txt.svn-base105
-rw-r--r--.svn/text-base/INSTALL.txt.svn-base14
-rw-r--r--.svn/text-base/LICENSE.txt.svn-base55
-rw-r--r--.svn/text-base/MANIFEST.in.svn-base7
-rw-r--r--.svn/text-base/README.txt.svn-base10
-rw-r--r--.svn/text-base/setup.py.svn-base72
-rw-r--r--CHANGELOG.txt175
-rw-r--r--INSTALL.txt26
-rw-r--r--LICENSE.txt108
-rw-r--r--MANIFEST.in14
-rw-r--r--PKG-INFO18
-rw-r--r--README.txt18
-rw-r--r--docs/.svn/all-wcprops11
-rw-r--r--docs/.svn/entries62
-rw-r--r--docs/.svn/format1
-rw-r--r--docs/.svn/prop-base/overview.txt.svn-base5
-rw-r--r--docs/.svn/text-base/overview.txt.svn-base912
-rw-r--r--docs/overview.txt1663
-rw-r--r--setup.py137
-rw-r--r--tagging/.svn/all-wcprops65
-rw-r--r--tagging/.svn/dir-prop-base6
-rw-r--r--tagging/.svn/entries374
-rw-r--r--tagging/.svn/format1
-rw-r--r--tagging/.svn/prop-base/__init__.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/fields.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/forms.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/generic.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/managers.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/models.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/settings.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/utils.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/validators.py.svn-base5
-rw-r--r--tagging/.svn/prop-base/views.py.svn-base5
-rw-r--r--tagging/.svn/text-base/__init__.py.svn-base30
-rw-r--r--tagging/.svn/text-base/fields.py.svn-base109
-rw-r--r--tagging/.svn/text-base/forms.py.svn-base23
-rw-r--r--tagging/.svn/text-base/generic.py.svn-base40
-rw-r--r--tagging/.svn/text-base/managers.py.svn-base68
-rw-r--r--tagging/.svn/text-base/models.py.svn-base502
-rw-r--r--tagging/.svn/text-base/settings.py.svn-base13
-rw-r--r--tagging/.svn/text-base/utils.py.svn-base263
-rw-r--r--tagging/.svn/text-base/validators.py.svn-base30
-rw-r--r--tagging/.svn/text-base/views.py.svn-base52
-rw-r--r--tagging/__init__.py31
-rw-r--r--tagging/fields.py219
-rw-r--r--tagging/forms.py46
-rw-r--r--tagging/generic.py40
-rw-r--r--tagging/managers.py476
-rw-r--r--tagging/models.py554
-rw-r--r--tagging/settings.py26
-rw-r--r--tagging/templatetags/.svn/all-wcprops17
-rw-r--r--tagging/templatetags/.svn/dir-prop-base6
-rw-r--r--tagging/templatetags/.svn/entries96
-rw-r--r--tagging/templatetags/.svn/format1
-rw-r--r--tagging/templatetags/.svn/prop-base/__init__.py.svn-base5
-rw-r--r--tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base5
-rw-r--r--tagging/templatetags/.svn/text-base/__init__.py.svn-base0
-rw-r--r--tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base231
-rw-r--r--tagging/templatetags/tagging_tags.py462
-rw-r--r--tagging/tests/.svn/all-wcprops35
-rw-r--r--tagging/tests/.svn/dir-prop-base6
-rw-r--r--tagging/tests/.svn/entries198
-rw-r--r--tagging/tests/.svn/format1
-rw-r--r--tagging/tests/.svn/prop-base/__init__.py.svn-base5
-rw-r--r--tagging/tests/.svn/prop-base/models.py.svn-base5
-rw-r--r--tagging/tests/.svn/prop-base/settings.py.svn-base5
-rw-r--r--tagging/tests/.svn/prop-base/tags.txt.svn-base5
-rw-r--r--tagging/tests/.svn/prop-base/tests.py.svn-base5
-rw-r--r--tagging/tests/.svn/text-base/__init__.py.svn-base0
-rw-r--r--tagging/tests/.svn/text-base/models.py.svn-base38
-rw-r--r--tagging/tests/.svn/text-base/settings.py.svn-base27
-rw-r--r--tagging/tests/.svn/text-base/tags.txt.svn-base122
-rw-r--r--tagging/tests/.svn/text-base/tests.py.svn-base495
-rw-r--r--tagging/tests/models.py76
-rw-r--r--tagging/tests/runtests.py8
-rw-r--r--tagging/tests/settings.py54
-rw-r--r--tagging/tests/tags.txt242
-rw-r--r--tagging/tests/tests.py929
-rw-r--r--tagging/utils.py511
-rw-r--r--tagging/validators.py60
-rw-r--r--tagging/views.py104
91 files changed, 7718 insertions, 2783 deletions
diff --git a/.svn/all-wcprops b/.svn/all-wcprops
new file mode 100644
index 0000000..dd05e4f
--- /dev/null
+++ b/.svn/all-wcprops
@@ -0,0 +1,41 @@
+K 25
+svn:wc:ra_dav:version-url
+V 23
+/svn/!svn/ver/147/trunk
+END
+LICENSE.txt
+K 25
+svn:wc:ra_dav:version-url
+V 35
+/svn/!svn/ver/114/trunk/LICENSE.txt
+END
+INSTALL.txt
+K 25
+svn:wc:ra_dav:version-url
+V 34
+/svn/!svn/ver/93/trunk/INSTALL.txt
+END
+CHANGELOG.txt
+K 25
+svn:wc:ra_dav:version-url
+V 37
+/svn/!svn/ver/131/trunk/CHANGELOG.txt
+END
+setup.py
+K 25
+svn:wc:ra_dav:version-url
+V 32
+/svn/!svn/ver/114/trunk/setup.py
+END
+MANIFEST.in
+K 25
+svn:wc:ra_dav:version-url
+V 35
+/svn/!svn/ver/121/trunk/MANIFEST.in
+END
+README.txt
+K 25
+svn:wc:ra_dav:version-url
+V 33
+/svn/!svn/ver/93/trunk/README.txt
+END
diff --git a/.svn/dir-prop-base b/.svn/dir-prop-base
new file mode 100644
index 0000000..dcdc624
--- /dev/null
+++ b/.svn/dir-prop-base
@@ -0,0 +1,6 @@
+K 10
+svn:ignore
+V 7
+*.pyc
+
+END
diff --git a/.svn/entries b/.svn/entries
new file mode 100644
index 0000000..3421631
--- /dev/null
+++ b/.svn/entries
@@ -0,0 +1,238 @@
+9
+
+dir
+147
+http://django-tagging.googlecode.com/svn/trunk
+http://django-tagging.googlecode.com/svn
+
+
+
+2008-08-20T23:28:50.903432Z
+147
+doug.napoleone
+has-props
+
+svn:special svn:externals svn:needs-lock
+
+
+
+
+
+
+
+
+
+
+
+83e7428b-ec2a-0410-86f2-bf466d0e5e72
+
+tagging
+dir
+
+LICENSE.txt
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+aeb0c61476a9b52a02584fe79e73ff84
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+2671
+
+INSTALL.txt
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+2298a5dfd7903940c220d522f991f5ed
+2007-08-20T09:19:55.193228Z
+93
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+534
+
+CHANGELOG.txt
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+0591d66a8ffdbde408f3321407270f6d
+2008-01-22T13:02:34.889764Z
+131
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+4108
+
+setup.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+b40563b2c098f951ba96a45a602fa356
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+2510
+
+docs
+dir
+
+MANIFEST.in
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+b4e06d199cc9fd3aa1e9f0fc3c6a7805
+2008-01-12T14:59:20.749002Z
+121
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+168
+
+README.txt
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+7726a0726e463ffe68549485e57f8560
+2007-08-20T09:19:55.193228Z
+93
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+309
+
diff --git a/.svn/format b/.svn/format
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/.svn/format
@@ -0,0 +1 @@
+9
diff --git a/.svn/prop-base/CHANGELOG.txt.svn-base b/.svn/prop-base/CHANGELOG.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/CHANGELOG.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/prop-base/INSTALL.txt.svn-base b/.svn/prop-base/INSTALL.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/INSTALL.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/prop-base/LICENSE.txt.svn-base b/.svn/prop-base/LICENSE.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/LICENSE.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/prop-base/MANIFEST.in.svn-base b/.svn/prop-base/MANIFEST.in.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/MANIFEST.in.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/prop-base/README.txt.svn-base b/.svn/prop-base/README.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/README.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/prop-base/setup.py.svn-base b/.svn/prop-base/setup.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/.svn/prop-base/setup.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/.svn/text-base/CHANGELOG.txt.svn-base b/.svn/text-base/CHANGELOG.txt.svn-base
new file mode 100644
index 0000000..1a8c360
--- /dev/null
+++ b/.svn/text-base/CHANGELOG.txt.svn-base
@@ -0,0 +1,105 @@
+========================
+Django Tagging Changelog
+========================
+
+
+SVN Trunk Changes:
+------------------
+
+* Added a ``tagging.generic`` module for working with list of objects
+ which have generic relations, containing a ``fetch_content_objects``
+ function for retrieving content objects for a list of ``TaggedItem``s
+ using ``number_of_content_types + 1`` queries rather than the
+ ``number_of_tagged_items * 2`` queries you'd get by iterating over the
+ list and accessing each item's ``object`` attribute.
+
+* Added a ``usage`` method to ``ModelTagManager``.
+
+* ``TaggedItemManager``'s methods now accept a ``QuerySet`` or a
+ ``Model`` class. If a ``QuerySet`` is given, it will be used as the
+ basis for the ``QuerySet``s the methods return, so can be used to
+ restrict results to a subset of a model's instances. The
+ `tagged_object_list`` generic view and ModelTaggedItemManager``
+ manager have been updated accordingly.
+
+* Removed ``tagging\tests\runtests.py``, as tests can be run with
+ ``django-admin.py test --settings=tagging.tests.settings``.
+
+* A ``tagging.TagDescriptor`` is now added to models when registered.
+ This returns a ``tagging.managers.ModelTagManager`` when accessed on a
+ model class, and provide access to and control over tags when used on
+ an instance.
+
+* Added ``tagging.register`` to register models with the tagging app.
+ Initially, a ``tagging.managers.ModelTaggedItemManager`` is added for
+ convenient access to tagged items.
+
+* Moved ``TagManager`` and ``TaggedItemManager`` to ``models.py`` - gets
+ rid of some import related silliness, as ``TagManager`` needs access
+ to ``TaggedItem``.
+
+Version 0.2.1, 16th Jan 2008:
+-----------------------------
+
+* Fixed a bug with space-delimited tag input handling - duplicates
+ weren't being removed and the list of tag names wasn't sorted.
+
+Version 0.2, 12th Jan 2008:
+---------------------------
+
+Packaged from revision 122 in Subversion; download at
+http://django-tagging.googlecode.com/files/tagging-0.2.zip
+
+* Added a ``tag_cloud_for_model`` template tag.
+
+* Added a ``MAX_TAG_LENGTH`` setting.
+
+* Multi-word tags are here - simple space-delimited input still works.
+ Double quotes and/or commas are used to delineate multi- word tags.
+ As far as valid tag contents - anything goes, at least initially.
+
+* BACKWARDS-INCOMPATIBLE CHANGE - ``django.utils.get_tag_name_list`` and
+ related regular expressions have been removed in favour of a new tag
+ input parsing function, ``django.utils.parse_tag_input``.
+
+* BACKWARDS-INCOMPATIBLE CHANGE - ``Tag`` and ``TaggedItem`` no longer
+ declare an explicit ``db_table``. If you can't rename your tables,
+ you'll have to put these back in manually.
+
+* Fixed a bug in calculation of logarithmic tag clouds - ``font_size``
+ attributes were not being set in some cases when the least used tag in
+ the cloud had been used more than once.
+
+* For consistency of return type, ``TaggedItemManager.get_by_model`` now
+ returns an empty ``QuerySet`` instead of an empty ``list`` if
+ non-existent tags were given.
+
+* Fixed a bug caused by ``cloud_for_model`` not passing its
+ ``distribution`` argument to ``calculate_cloud``.
+
+* Added ``TaggedItemManager.get_union_by_model`` for looking up items
+ tagged with any one of a list of tags.
+
+* Added ``TagManager.add_tag`` for adding a single extra tag to an
+ object.
+
+* Tag names can now be forced to lowercase before they are saved to the
+ database by adding the appropriate ``FORCE_LOWERCASE_TAGS`` setting to
+ your project's settings module. This feature defaults to being off.
+
+* Fixed a bug where passing non-existent tag names to
+ ``TaggedItemManager.get_by_model`` caused database errors with some
+ backends.
+
+* Added ``tagged_object_list`` generic view for displaying paginated
+ lists of objects for a given model which have a given tag, and
+ optionally related tags for that model.
+
+
+Version 0.1, 30th May 2007:
+---------------------------
+
+Packaged from revision 79 in Subversion; download at
+http://django-tagging.googlecode.com/files/tagging-0.1.zip
+
+* First packaged version using distutils.
diff --git a/.svn/text-base/INSTALL.txt.svn-base b/.svn/text-base/INSTALL.txt.svn-base
new file mode 100644
index 0000000..7f90cca
--- /dev/null
+++ b/.svn/text-base/INSTALL.txt.svn-base
@@ -0,0 +1,14 @@
+Thanks for downloading django-tagging.
+
+To install it, run the following command inside this directory:
+
+ python setup.py install
+
+Or if you'd prefer you can simply place the included ``tagging``
+directory somewhere on your Python path, or symlink to it from
+somewhere on your Python path; this is useful if you're working from a
+Subversion checkout.
+
+Note that this application requires Python 2.3 or later, and Django
+0.96 or later. You can obtain Python from http://www.python.org/ and
+Django from http://www.djangoproject.com/. \ No newline at end of file
diff --git a/.svn/text-base/LICENSE.txt.svn-base b/.svn/text-base/LICENSE.txt.svn-base
new file mode 100644
index 0000000..1654536
--- /dev/null
+++ b/.svn/text-base/LICENSE.txt.svn-base
@@ -0,0 +1,55 @@
+Django Tagging
+--------------
+
+Copyright (c) 2007, Jonathan Buchanan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Initially based on code from James Bennett's Cab:
+
+Cab
+---
+
+Copyright (c) 2007, James Bennett
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * 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.
+ * Neither the name of the author nor the names of other
+ 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. \ No newline at end of file
diff --git a/.svn/text-base/MANIFEST.in.svn-base b/.svn/text-base/MANIFEST.in.svn-base
new file mode 100644
index 0000000..618270b
--- /dev/null
+++ b/.svn/text-base/MANIFEST.in.svn-base
@@ -0,0 +1,7 @@
+include CHANGELOG.txt
+include INSTALL.txt
+include LICENSE.txt
+include MANIFEST.in
+include README.txt
+recursive-include docs *.txt
+recursive-include tagging/tests *.txt
diff --git a/.svn/text-base/README.txt.svn-base b/.svn/text-base/README.txt.svn-base
new file mode 100644
index 0000000..acb2d47
--- /dev/null
+++ b/.svn/text-base/README.txt.svn-base
@@ -0,0 +1,10 @@
+==============
+Django Tagging
+==============
+
+This is a generic tagging application for Django projects
+
+For installation instructions, see the file "INSTALL.txt" in this
+directory; for instructions on how to use this application, and on
+what it provides, see the file "overview.txt" in the "docs/"
+directory. \ No newline at end of file
diff --git a/.svn/text-base/setup.py.svn-base b/.svn/text-base/setup.py.svn-base
new file mode 100644
index 0000000..66daf3e
--- /dev/null
+++ b/.svn/text-base/setup.py.svn-base
@@ -0,0 +1,72 @@
+"""
+Based entirely on Django's own ``setup.py``.
+"""
+import os
+from distutils.command.install import INSTALL_SCHEMES
+from distutils.core import setup
+
+def fullsplit(path, result=None):
+ """
+ Split a pathname into components (the opposite of os.path.join) in a
+ platform-neutral way.
+ """
+ if result is None:
+ result = []
+ head, tail = os.path.split(path)
+ if head == '':
+ return [tail] + result
+ if head == path:
+ return result
+ return fullsplit(head, [tail] + result)
+
+# Tell distutils to put the data_files in platform-specific installation
+# locations. See here for an explanation:
+# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
+for scheme in INSTALL_SCHEMES.values():
+ scheme['data'] = scheme['purelib']
+
+# Compile the list of packages available, because distutils doesn't have
+# an easy way to do this.
+packages, data_files = [], []
+root_dir = os.path.dirname(__file__)
+tagging_dir = os.path.join(root_dir, 'tagging')
+pieces = fullsplit(root_dir)
+if pieces[-1] == '':
+ len_root_dir = len(pieces) - 1
+else:
+ len_root_dir = len(pieces)
+
+for dirpath, dirnames, filenames in os.walk(tagging_dir):
+ # Ignore dirnames that start with '.'
+ for i, dirname in enumerate(dirnames):
+ if dirname.startswith('.'): del dirnames[i]
+ if '__init__.py' in filenames:
+ packages.append('.'.join(fullsplit(dirpath)[len_root_dir:]))
+ elif filenames:
+ data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
+
+# Dynamically calculate the version based on tagging.VERSION
+version_tuple = __import__('tagging').VERSION
+if version_tuple[2] is not None:
+ version = "%d.%d_%s" % version_tuple
+else:
+ version = "%d.%d" % version_tuple[:2]
+
+setup(
+ name = 'tagging',
+ version = version,
+ description = 'Generic tagging application for Django',
+ author = 'Jonathan Buchanan',
+ author_email = 'jonathan.buchanan@gmail.com',
+ url = 'http://code.google.com/p/django-tagging/',
+ packages = packages,
+ data_files = data_files,
+ classifiers = ['Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Utilities'],
+)
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 698197c..1a8c360 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,70 +1,105 @@
-========================
-Django Tagging Changelog
-========================
-
-
-Version 0.2.1, 16th Jan 2008:
------------------------------
-
-* Fixed a bug with space-delimited tag input handling - duplicates
- weren't being removed and the list of tag names wasn't sorted.
-
-Version 0.2, 12th Jan 2008:
----------------------------
-
-Packaged from revision 122 in Subversion; download at
-http://django-tagging.googlecode.com/files/tagging-0.2.zip
-
-* Added a ``tag_cloud_for_model`` template tag.
-
-* Added a ``MAX_TAG_LENGTH`` setting.
-
-* Multi-word tags are here - simple space-delimited input still works.
- Double quotes and/or commas are used to delineate multi- word tags.
- As far as valid tag contents - anything goes, at least initially.
-
-* BACKWARDS-INCOMPATIBLE CHANGE - ``django.utils.get_tag_name_list`` and
- related regular expressions have been removed in favour of a new tag
- input parsing function, ``django.utils.parse_tag_input``.
-
-* BACKWARDS-INCOMPATIBLE CHANGE - ``Tag`` and ``TaggedItem`` no longer
- declare an explicit ``db_table``. If you can't rename your tables,
- you'll have to put these back in manually.
-
-* Fixed a bug in calculation of logarithmic tag clouds - ``font_size``
- attributes were not being set in some cases when the least used tag in
- the cloud had been used more than once.
-
-* For consistency of return type, ``TaggedItemManager.get_by_model`` now
- returns an empty ``QuerySet`` instead of an empty ``list`` if
- non-existent tags were given.
-
-* Fixed a bug caused by ``cloud_for_model`` not passing its
- ``distribution`` argument to ``calculate_cloud``.
-
-* Added ``TaggedItemManager.get_union_by_model`` for looking up items
- tagged with any one of a list of tags.
-
-* Added ``TagManager.add_tag`` for adding a single extra tag to an
- object.
-
-* Tag names can now be forced to lowercase before they are saved to the
- database by adding the appropriate ``FORCE_LOWERCASE_TAGS`` setting to
- your project's settings module. This feature defaults to being off.
-
-* Fixed a bug where passing non-existent tag names to
- ``TaggedItemManager.get_by_model`` caused database errors with some
- backends.
-
-* Added ``tagged_object_list`` generic view for displaying paginated
- lists of objects for a given model which have a given tag, and
- optionally related tags for that model.
-
-
-Version 0.1, 30th May 2007:
----------------------------
-
-Packaged from revision 79 in Subversion; download at
-http://django-tagging.googlecode.com/files/tagging-0.1.zip
-
-* First packaged version using distutils.
+========================
+Django Tagging Changelog
+========================
+
+
+SVN Trunk Changes:
+------------------
+
+* Added a ``tagging.generic`` module for working with list of objects
+ which have generic relations, containing a ``fetch_content_objects``
+ function for retrieving content objects for a list of ``TaggedItem``s
+ using ``number_of_content_types + 1`` queries rather than the
+ ``number_of_tagged_items * 2`` queries you'd get by iterating over the
+ list and accessing each item's ``object`` attribute.
+
+* Added a ``usage`` method to ``ModelTagManager``.
+
+* ``TaggedItemManager``'s methods now accept a ``QuerySet`` or a
+ ``Model`` class. If a ``QuerySet`` is given, it will be used as the
+ basis for the ``QuerySet``s the methods return, so can be used to
+ restrict results to a subset of a model's instances. The
+ `tagged_object_list`` generic view and ModelTaggedItemManager``
+ manager have been updated accordingly.
+
+* Removed ``tagging\tests\runtests.py``, as tests can be run with
+ ``django-admin.py test --settings=tagging.tests.settings``.
+
+* A ``tagging.TagDescriptor`` is now added to models when registered.
+ This returns a ``tagging.managers.ModelTagManager`` when accessed on a
+ model class, and provide access to and control over tags when used on
+ an instance.
+
+* Added ``tagging.register`` to register models with the tagging app.
+ Initially, a ``tagging.managers.ModelTaggedItemManager`` is added for
+ convenient access to tagged items.
+
+* Moved ``TagManager`` and ``TaggedItemManager`` to ``models.py`` - gets
+ rid of some import related silliness, as ``TagManager`` needs access
+ to ``TaggedItem``.
+
+Version 0.2.1, 16th Jan 2008:
+-----------------------------
+
+* Fixed a bug with space-delimited tag input handling - duplicates
+ weren't being removed and the list of tag names wasn't sorted.
+
+Version 0.2, 12th Jan 2008:
+---------------------------
+
+Packaged from revision 122 in Subversion; download at
+http://django-tagging.googlecode.com/files/tagging-0.2.zip
+
+* Added a ``tag_cloud_for_model`` template tag.
+
+* Added a ``MAX_TAG_LENGTH`` setting.
+
+* Multi-word tags are here - simple space-delimited input still works.
+ Double quotes and/or commas are used to delineate multi- word tags.
+ As far as valid tag contents - anything goes, at least initially.
+
+* BACKWARDS-INCOMPATIBLE CHANGE - ``django.utils.get_tag_name_list`` and
+ related regular expressions have been removed in favour of a new tag
+ input parsing function, ``django.utils.parse_tag_input``.
+
+* BACKWARDS-INCOMPATIBLE CHANGE - ``Tag`` and ``TaggedItem`` no longer
+ declare an explicit ``db_table``. If you can't rename your tables,
+ you'll have to put these back in manually.
+
+* Fixed a bug in calculation of logarithmic tag clouds - ``font_size``
+ attributes were not being set in some cases when the least used tag in
+ the cloud had been used more than once.
+
+* For consistency of return type, ``TaggedItemManager.get_by_model`` now
+ returns an empty ``QuerySet`` instead of an empty ``list`` if
+ non-existent tags were given.
+
+* Fixed a bug caused by ``cloud_for_model`` not passing its
+ ``distribution`` argument to ``calculate_cloud``.
+
+* Added ``TaggedItemManager.get_union_by_model`` for looking up items
+ tagged with any one of a list of tags.
+
+* Added ``TagManager.add_tag`` for adding a single extra tag to an
+ object.
+
+* Tag names can now be forced to lowercase before they are saved to the
+ database by adding the appropriate ``FORCE_LOWERCASE_TAGS`` setting to
+ your project's settings module. This feature defaults to being off.
+
+* Fixed a bug where passing non-existent tag names to
+ ``TaggedItemManager.get_by_model`` caused database errors with some
+ backends.
+
+* Added ``tagged_object_list`` generic view for displaying paginated
+ lists of objects for a given model which have a given tag, and
+ optionally related tags for that model.
+
+
+Version 0.1, 30th May 2007:
+---------------------------
+
+Packaged from revision 79 in Subversion; download at
+http://django-tagging.googlecode.com/files/tagging-0.1.zip
+
+* First packaged version using distutils.
diff --git a/INSTALL.txt b/INSTALL.txt
index 7104235..7f90cca 100644
--- a/INSTALL.txt
+++ b/INSTALL.txt
@@ -1,14 +1,14 @@
-Thanks for downloading django-tagging.
-
-To install it, run the following command inside this directory:
-
- python setup.py install
-
-Or if you'd prefer you can simply place the included ``tagging``
-directory somewhere on your Python path, or symlink to it from
-somewhere on your Python path; this is useful if you're working from a
-Subversion checkout.
-
-Note that this application requires Python 2.3 or later, and Django
-0.96 or later. You can obtain Python from http://www.python.org/ and
+Thanks for downloading django-tagging.
+
+To install it, run the following command inside this directory:
+
+ python setup.py install
+
+Or if you'd prefer you can simply place the included ``tagging``
+directory somewhere on your Python path, or symlink to it from
+somewhere on your Python path; this is useful if you're working from a
+Subversion checkout.
+
+Note that this application requires Python 2.3 or later, and Django
+0.96 or later. You can obtain Python from http://www.python.org/ and
Django from http://www.djangoproject.com/. \ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index 4009dfe..1654536 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,55 +1,55 @@
-Django Tagging
---------------
-
-Copyright (c) 2007, Jonathan Buchanan
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-Initially based on code from James Bennett's Cab:
-
-Cab
----
-
-Copyright (c) 2007, James Bennett
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * 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.
- * Neither the name of the author nor the names of other
- 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
+Django Tagging
+--------------
+
+Copyright (c) 2007, Jonathan Buchanan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Initially based on code from James Bennett's Cab:
+
+Cab
+---
+
+Copyright (c) 2007, James Bennett
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * 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.
+ * Neither the name of the author nor the names of other
+ 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. \ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index a326b58..618270b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,7 @@
-include CHANGELOG.txt
-include INSTALL.txt
-include LICENSE.txt
-include MANIFEST.in
-include README.txt
-recursive-include docs *.txt
-recursive-include tagging/tests *.txt
+include CHANGELOG.txt
+include INSTALL.txt
+include LICENSE.txt
+include MANIFEST.in
+include README.txt
+recursive-include docs *.txt
+recursive-include tagging/tests *.txt
diff --git a/PKG-INFO b/PKG-INFO
deleted file mode 100644
index 8e35e54..0000000
--- a/PKG-INFO
+++ /dev/null
@@ -1,18 +0,0 @@
-Metadata-Version: 1.0
-Name: tagging
-Version: 0.2.1
-Summary: Generic tagging application for Django
-Home-page: http://code.google.com/p/django-tagging/
-Author: Jonathan Buchanan
-Author-email: jonathan.buchanan@gmail.com
-License: UNKNOWN
-Description: UNKNOWN
-Platform: UNKNOWN
-Classifier: Development Status :: 4 - Beta
-Classifier: Environment :: Web Environment
-Classifier: Framework :: Django
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Topic :: Utilities
diff --git a/README.txt b/README.txt
index 9f64a06..acb2d47 100644
--- a/README.txt
+++ b/README.txt
@@ -1,10 +1,10 @@
-==============
-Django Tagging
-==============
-
-This is a generic tagging application for Django projects
-
-For installation instructions, see the file "INSTALL.txt" in this
-directory; for instructions on how to use this application, and on
-what it provides, see the file "overview.txt" in the "docs/"
+==============
+Django Tagging
+==============
+
+This is a generic tagging application for Django projects
+
+For installation instructions, see the file "INSTALL.txt" in this
+directory; for instructions on how to use this application, and on
+what it provides, see the file "overview.txt" in the "docs/"
directory. \ No newline at end of file
diff --git a/docs/.svn/all-wcprops b/docs/.svn/all-wcprops
new file mode 100644
index 0000000..6104b64
--- /dev/null
+++ b/docs/.svn/all-wcprops
@@ -0,0 +1,11 @@
+K 25
+svn:wc:ra_dav:version-url
+V 28
+/svn/!svn/ver/133/trunk/docs
+END
+overview.txt
+K 25
+svn:wc:ra_dav:version-url
+V 41
+/svn/!svn/ver/133/trunk/docs/overview.txt
+END
diff --git a/docs/.svn/entries b/docs/.svn/entries
new file mode 100644
index 0000000..643c736
--- /dev/null
+++ b/docs/.svn/entries
@@ -0,0 +1,62 @@
+9
+
+dir
+147
+http://django-tagging.googlecode.com/svn/trunk/docs
+http://django-tagging.googlecode.com/svn
+
+
+
+2008-04-30T22:29:01.189094Z
+133
+jonathan.buchanan
+
+
+svn:special svn:externals svn:needs-lock
+
+
+
+
+
+
+
+
+
+
+
+83e7428b-ec2a-0410-86f2-bf466d0e5e72
+
+overview.txt
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+6ff98ba2ec163f1556bc41693647443a
+2008-04-30T22:29:01.189094Z
+133
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+29385
+
diff --git a/docs/.svn/format b/docs/.svn/format
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/docs/.svn/format
@@ -0,0 +1 @@
+9
diff --git a/docs/.svn/prop-base/overview.txt.svn-base b/docs/.svn/prop-base/overview.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/docs/.svn/prop-base/overview.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/docs/.svn/text-base/overview.txt.svn-base b/docs/.svn/text-base/overview.txt.svn-base
new file mode 100644
index 0000000..9641f03
--- /dev/null
+++ b/docs/.svn/text-base/overview.txt.svn-base
@@ -0,0 +1,912 @@
+==============
+Django Tagging
+==============
+
+A generic tagging application for `Django`_ projects, which allows
+association of a number of tags with any Django model instance and makes
+retrieval of tags simple.
+
+.. _`Django`: http://www.djangoproject.com
+
+.. contents::
+ :depth: 3
+
+
+Installation
+============
+
+Installing an official release
+------------------------------
+
+Official releases are made available from
+http://code.google.com/p/django-tagging/
+
+Source distribution
+~~~~~~~~~~~~~~~~~~~
+
+Download the .zip distribution file and unpack it. Inside is a script
+named ``setup.py``. Enter this command::
+
+ python setup.py install
+
+...and the package will install automatically.
+
+Windows installer
+~~~~~~~~~~~~~~~~~
+
+A Windows installer is also made available - download the .exe
+distribution file and launch it to install the application.
+
+An uninstaller will also be created, accessible through Add/Remove
+Programs in your Control Panel.
+
+Installing the development version
+----------------------------------
+
+Alternatively, if you'd like to update Django Tagging occasionally to pick
+up the latest bug fixes and enhancements before they make it into an
+official release, perform a `Subversion`_ checkout instead. The following
+command will check the application's development branch out to an
+``tagging-trunk`` directory::
+
+ svn checkout http://django-tagging.googlecode.com/svn/trunk/ tagging-trunk
+
+Add the resulting folder to your `PYTHONPATH`_ or symlink (`junction`_,
+if you're on Windows) the ``tagging`` directory inside it into a
+directory which is on your PYTHONPATH, such as your Python
+installation's ``site-packages`` directory.
+
+You can verify that the application is available on your PYTHONPATH by
+opening a Python interpreter and entering the following commands::
+
+ >>> import tagging
+ >>> tagging.VERSION
+ (0, 3, 'pre')
+
+When you want to update your copy of the Django Tagging source code, run
+the command ``svn update`` from within the ``tagging-trunk`` directory.
+
+.. caution::
+
+ The development version may contain bugs which are not present in the
+ release version and introduce backwards-incompatible changes.
+
+ If you're tracking trunk, keep an eye on the `CHANGELOG`_ and the
+ `backwards-incompatible changes wiki page`_ before you update your
+ copy of the source code.
+
+.. _`Subversion`: http://subversion.tigris.org
+.. _`PYTHONPATH`: http://docs.python.org/tut/node8.html#SECTION008110000000000000000
+.. _`junction`: http://www.microsoft.com/technet/sysinternals/FileAndDisk/Junction.mspx
+.. _`CHANGELOG`: http://django-tagging.googlecode.com/svn/trunk/CHANGELOG.txt
+.. _`backwards-incompatible changes wiki page`: http://code.google.com/p/django-tagging/wiki/BackwardsIncompatibleChanges
+
+Using Django Tagging in your applications
+-----------------------------------------
+
+Once you've installed Django Tagging and want to use it in your Django
+applications, do the following:
+
+ 1. Put ``'tagging'`` in your ``INSTALLED_APPS`` setting.
+ 2. Run the command ``manage.py syncdb``.
+
+The ``syncdb`` command creates the necessary database tables and
+creates permission objects for all installed apps that need them.
+
+That's it!
+
+
+Settings
+========
+
+Some of the application's behaviour can be configured by adding the
+appropriate settings to your project's settings file.
+
+The following settings are available:
+
+FORCE_LOWERCASE_TAGS
+--------------------
+
+Default: ``False``
+
+A boolean that turns on/off forcing of all tag names to lowercase before
+they are saved to the database.
+
+MAX_TAG_LENGTH
+--------------
+
+Default: ``50``
+
+An integer which specifies the maximum length which any tag is allowed
+to have. This is used for validation in the ``django.contrib.admin``
+application and in any ``newforms`` forms automatically generated using
+``ModelForm``.
+
+
+Registering your models
+=======================
+
+**New in developement version**
+
+Your Django models can be registered with the tagging application to
+access some additional tagging-related features.
+
+.. note::
+
+ You don't *have* to register your models in order to use them with
+ the tagging application - many of the features added by registration
+ are just convenience wrappers around the tagging API provided by the
+ ``Tag`` and ``TaggedItem`` models and their managers, as documented
+ further below.
+
+The ``register`` function
+-------------------------
+
+To register a model, import the ``tagging`` module and call its
+``register`` function, like so::
+
+ from django.db import models
+
+ import tagging
+
+ class Widget(models.Model):
+ name = models.CharField(max_length=50)
+
+ tagging.register(Widget)
+
+The following argument is required:
+
+``model``
+ The model class to be registered.
+
+ An exception will be raised if you attempt to register the same class
+ more than once.
+
+The following arguments are optional, with some recommended defaults -
+take care to specify different attribute names if the defaults clash
+with your model class' definition:
+
+``tag_descriptor_attr``
+ The name of an attribute in the model class which will hold a tag
+ descriptor for the model. Default: ``'tags'``
+
+ See `TagDescriptor`_ below for details about the use of this
+ descriptor.
+
+``tagged_item_manger_attr``
+ The name of an attribute in the model class which will hold a custom
+ manager for accessing tagged items for the model. Default:
+ ``'tagged'``.
+
+ See `ModelTaggedItemManager`_ below for details about the use of this
+ manager.
+
+``TagDescriptor``
+-----------------
+
+When accessed through the model class itself, this descriptor will return
+a ``ModelTagManager`` for the model. See `ModelTagManager`_ below for
+more details about its use.
+
+When accessed through a model instance, this descriptor provides a handy
+means of retrieving, updating and deleting the instance's tags. For
+example::
+
+ >>> widget = Widget.objects.create(name='Testing descriptor')
+ >>> widget.tags
+ []
+ >>> widget.tags = 'toast, melted cheese, butter'
+ >>> widget.tags
+ [<Tag: butter>, <Tag: melted cheese>, <Tag: toast>]
+ >>> del widget.tags
+ >>> widget.tags
+ []
+
+``ModelTagManager``
+-------------------
+
+A manager for retrieving tags used by a particular model.
+
+Defines the following methods:
+
+* ``get_query_set()`` -- as this method is redefined, any ``QuerySets``
+ created by this model will be initially restricted to contain the
+ distinct tags used by all the model's instances.
+
+* ``cloud(*args, **kwargs)`` -- creates a list of tags used by the
+ model's instances, with ``count`` and ``font_size`` attributes set for
+ use in displaying a tag cloud.
+
+ See the documentation on ``Tag``'s manager's `cloud_for_model method`_
+ for information on additional arguments which can be given.
+
+* ``related(self, tags, *args, **kwargs)`` -- creates a list of tags
+ used by the model's instances, which are also used by all instance
+ which have the given ``tags``.
+
+ See the documentation on ``Tag``'s manager's
+ `related_for_model method`_ for information on additional arguments
+ which can be given.
+
+* ``usage(self, *args, **kwargs))`` -- creates a list of tags used by
+ the model's instances, with optional usages counts, restriction based
+ on usage counts and restriction of the model instances from which
+ usage and counts are determined.
+
+ See the documentation on ``Tag``'s manager's `usage_for_model method`_
+ for information on additional arguments which can be given.
+
+Example usage::
+
+ # Create a ``QuerySet`` of tags used by Widget instances
+ Widget.tags.all()
+
+ # Retrieve a list of tags used by Widget instances with usage counts
+ Widget.tags.usage(counts=True)
+
+ # Retrieve tags used by instances of WIdget which are also tagged with
+ # 'cheese' and 'toast'
+ Widget.tags.related(['cheese', 'toast'], counts=True, min_count=3)
+
+``ModelTaggedItemManager``
+--------------------------
+
+A manager for retrieving model instance for a particular model, based on
+their tags.
+
+* ``related_to(obj, queryset=None, num=None)`` -- creates a list
+ of model instances which are related to ``obj``, based on its tags. If
+ a ``queryset`` argument is provided, it will be used to restrict the
+ resulting list of model instances.
+
+ If ``num`` is given, a maximum of ``num`` instances will be returned.
+
+* ``with_all(tags, queryset=None)`` -- creates a ``QuerySet`` containing
+ model instances which are tagged with *all* the given tags. If a
+ ``queryset`` argument is provided, it will be used as the basis for
+ the resulting ``QuerySet``.
+
+* ``with_any(tags, queryset=None)`` -- creates a ``QuerySet`` containing model
+ instances which are tagged with *any* the given tags. If a ``queryset``
+ argument is provided, it will be used as the basis for the resulting
+ ``QuerySet``.
+
+
+Tags
+====
+
+Tags are represented by the ``Tag`` model, which lives in the
+``tagging.models`` module.
+
+API reference
+-------------
+
+Fields
+~~~~~~
+
+``Tag`` objects have the following fields:
+
+* ``name`` -- The name of the tag. This is a unique value.
+
+Manager functions
+~~~~~~~~~~~~~~~~~
+
+The ``Tag`` model has a custom manager which has the following helper
+methods:
+
+* ``update_tags(obj, tag_names)`` -- updates tags associated with an
+ object.
+
+ ``tag_names`` is a string containing tag names with which ``obj``
+ should be tagged.
+
+ If ``tag_names`` is ``None`` or ``''``, the object's tags will be
+ cleared.
+
+* ``add_tag(obj, tag_name)`` -- associates a tag with an an object.
+
+ ``tag_name`` is a string containing a tag name with which ``obj``
+ should be tagged.
+
+* ``get_for_object(obj)`` -- returns a ``QuerySet`` containing all
+ ``Tag`` objects associated with ``obj``.
+
+.. _`usage_for_model method`:
+
+* ``usage_for_model(model, counts=False, min_count=None, filters=None)``
+ -- returns a list of ``Tag`` objects associated with instances of
+ ``model``.
+
+ If ``counts`` is ``True``, a ``count`` attribute will be added to each
+ tag, indicating how many times it has been associated with instances
+ of ``model``.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned. Passing a value for
+ ``min_count`` implies ``counts=True``.
+
+ To limit the tags (and counts, if specified) returned to those used by
+ a subset of the model's instances, pass a dictionary of field lookups
+ to be applied to ``model`` as the ``filters`` argument.
+
+.. _`related_for_model method`:
+
+* ``related_for_model(tags, Model, counts=False, min_count=None)``
+ -- returns a list of tags related to a given list of tags - that is,
+ other tags used by items which have all the given tags.
+
+ If ``counts`` is ``True``, a ``count`` attribute will be added to each
+ tag, indicating the number of items which have it in addition to the
+ given list of tags.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned. Passing a value for
+ ``min_count`` implies ``counts=True``.
+
+.. _`cloud_for_model method`:
+
+* ``cloud_for_model(Model, steps=4, distribution=LOGARITHMIC,
+ filters=None, min_count=None)`` -- returns a list of the distinct
+ ``Tag`` objects associated with instances of ``Model``, each having a
+ ``count`` attribute as above and an additional ``font_size``
+ attribute, for use in creation of a tag cloud (a type of weighted
+ list).
+
+ ``steps`` defines the number of font sizes available - ``font_size``
+ may be an integer between ``1`` and ``steps``, inclusive.
+
+ ``distribution`` defines the type of font size distribution algorithm
+ which will be used - logarithmic or linear. It must be either
+ ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+
+ To limit the tags displayed in the cloud to those associated with a
+ subset of the Model's instances, pass a dictionary of field lookups to
+ be applied to the given Model as the ``filters`` argument.
+
+ To limit the tags displayed in the cloud to those with a ``count``
+ greater than or equal to ``min_count``, pass a value for the
+ ``min_count`` argument.
+
+**New in development version**
+
+* ``usage_for_queryset(queryset, counts=False, min_count=None)`` --
+ Obtains a list of tags associated with instances of a model contained
+ in the given queryset.
+
+ If ``counts`` is True, a ``count`` attribute will be added to each tag,
+ indicating how many times it has been used against the Model class in
+ question.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned.
+
+ Passing a value for ``min_count`` implies ``counts=True``.
+
+Basic usage
+-----------
+
+Tagging objects and retrieving an object's tags
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Objects may be tagged using the ``update_tags`` helper function::
+
+ >>> from shop.apps.products.models import Widget
+ >>> from tagging.models import Tag
+ >>> widget = Widget.objects.get(pk=1)
+ >>> Tag.objects.update_tags(widget, 'house thing')
+
+Retrieve tags for an object using the ``get_for_object`` helper
+function::
+
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: thing>]
+
+Tags are created, associated and unassociated accordingly when you use
+``update_tags`` and ``add_tags``::
+
+ >>> Tag.objects.update_tags(widget, 'house monkey')
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: monkey>]
+ >>> Tag.objects.add_tag(widget, 'tiles')
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: monkey>, <Tag: tiles>]
+
+Clear an object's tags by passing ``None`` or ``''`` to
+``update_tags``::
+
+ >>> Tag.objects.update_tags(widget, None)
+ >>> Tag.objects.get_for_object(widget)
+ []
+
+Retrieving tags used by a particular model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To retrieve all tags used for a particular model, use the
+``get_for_model`` helper function::
+
+ >>> widget1 = Widget.objects.get(pk=1)
+ >>> Tag.objects.update_tags(widget1, 'house thing')
+ >>> widget2 = Widget.objects.get(pk=2)
+ >>> Tag.objects.update_tags(widget2, 'cheese toast house')
+ >>> Tag.objects.usage_for_model(Widget)
+ [<Tag: cheese>, <Tag: house>, <Tag: thing>, <Tag: toast>]
+
+To get a count of how many times each tag was used for a particular
+model, pass in ``True`` for the ``counts`` argument::
+
+ >>> tags = Tag.objects.usage_for_model(Widget, counts=True)
+ >>> [(tag.name, tag.count) for tag in tags]
+ [('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)]
+
+To get counts and limit the tags returned to those with counts above a
+certain size, pass in a ``min_count`` argument::
+
+ >>> tags = Tag.objects.usage_for_model(Widget, min_count=2)
+ >>> [(tag.name, tag.count) for tag in tags]
+ [('house', 2)]
+
+You can also specify a dictionary of `field lookups`_ to be used to
+restrict the tags and counts returned based on a subset of the
+model's instances. For example, the following would retrieve all tags
+used on Widgets created by a user named Alan which have a size
+greater than 99::
+
+ >>> Tag.objects.usage_for_model(Widget, filters=dict(size__gt=99, user__username='Alan'))
+
+.. _`field lookups`: http://www.djangoproject.com/documentation/db-api/#field-lookups
+
+**New in development version**
+
+The ``usage_for_queryset`` method allows you to pass a pre-filtered
+queryset to be used when determining tag usage::
+
+ >>> Tag.objects.usage_for_queryset(Widget.objects.filter(size__gt=99, user__username='Alan'))
+
+Tag input
+---------
+
+Tag input from users is treated 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, we parse the
+ input like so:
+
+ * 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.
+
+ * For the remaining input, if there are any unquoted commas in the
+ input, the remainder will be treated as comma-delimited. Otherwise,
+ 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
+====================== ======================================= ================================================
+
+
+Tagged items
+============
+
+The relationship between a ``Tag`` and an object is represented by
+the ``TaggedItem`` model, which lives in the ``tagging.models``
+module.
+
+API reference
+-------------
+
+Fields
+~~~~~~
+
+``TaggedItem`` objects have the following fields:
+
+* ``tag`` -- The ``Tag`` an object is associated with.
+* ``content_type`` -- The ``ContentType`` of the associated model
+ instance.
+* ``object_id`` -- The id of the associated object.
+* ``object`` -- The associated object itself, accessible via the
+ Generic Relations API.
+
+Manager functions
+~~~~~~~~~~~~~~~~~
+
+The ``TaggedItem`` model has a custom manager which has the following
+helper methods, which accept either a ``QuerySet`` or a ``Model``
+class as one of their arguments. To restrict the objects which are
+returned, pass in a filtered ``QuerySet`` for this argument:
+
+* ``get_by_model(queryset_or_model, tag)`` -- creates a ``QuerySet``
+ containing instances of the specififed model which are tagged with
+ the given tag or tags.
+
+* ``get_intersection_by_model(queryset_or_model, tags)`` -- creates a
+ ``QuerySet`` containing instances of the specified model which are
+ tagged with every tag in a list of tags.
+
+ ``get_by_model`` will call this function behind the scenes when you
+ pass it a list, so you can use ``get_by_model`` instead of calling
+ this method directly.
+
+* ``get_union_by_model(queryset_or_model, tags)`` -- creates a
+ ``QuerySet`` containing instances of the specified model which are
+ tagged with any tag in a list of tags.
+
+.. _`get_related method`:
+
+* ``get_related(obj, queryset_or_model, num=None)`` - returns a list of
+ instances of the specified model which share tags with the model
+ instance ``obj``, ordered by the number of shared tags in descending
+ order.
+
+ If ``num`` is given, a maximum of ``num`` instances will be returned.
+
+Basic usage
+-----------
+
+Retrieving tagged objects
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Objects may be retrieved based on their tags using the ``get_by_model``
+manager method::
+
+ >>> from shop.apps.products.models import Widget
+ >>> from tagging.models import Tag
+ >>> house_tag = Tag.objects.get(name='house')
+ >>> TaggedItem.objects.get_by_model(Widget, house_tag)
+ [<Widget: pk=1>, <Widget: pk=2>]
+
+Passing a list of tags to ``get_by_model`` returns an intersection of
+objects which have those tags, i.e. tag1 AND tag2 ... AND tagN::
+
+ >>> thing_tag = Tag.objects.get(name='thing')
+ >>> TaggedItem.objects.get_by_model(Widget, [house_tag, thing_tag])
+ [<Widget: pk=1>]
+
+Functions which take tags are flexible when it comes to tag input::
+
+ >>> TaggedItem.objects.get_by_model(Widget, Tag.objects.filter(name__in=['house', 'thing']))
+ [<Widget: pk=1>]
+ >>> TaggedItem.objects.get_by_model(Widget, 'house thing')
+ [<Widget: pk=1>]
+ >>> TaggedItem.objects.get_by_model(Widget, ['house', 'thing'])
+ [<Widget: pk=1>]
+
+Restricting objects returned
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Pass in a ``QuerySet`` to restrict the objects returned::
+
+ # Retrieve all Widgets which have a price less than 50, tagged with 'house'
+ TaggedItem.objects.get_by_model(Widget.objects.filter(price__lt=50), 'house')
+
+ # Retrieve all Widgets which have a name starting with 'a', tagged with any
+ # of 'house', 'garden' or 'water'.
+ TaggedItem.objects.get_union_by_model(Widget.objects.filter(name__startswith='a'),
+ ['house', 'garden', 'water'])
+
+
+Utilities
+=========
+
+Tag-related utility functions are defined in the ``tagging.utils``
+module:
+
+``parse_tag_input(input)``
+--------------------------
+
+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.
+
+See `tag input`_ for more details.
+
+``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.
+
+``get_tag_list(tags)``
+----------------------
+
+Utility function for accepting tag input in a flexible manner.
+
+If a ``Tag`` object is given, it will be returned in a list as its
+single occupant.
+
+If given, the tag names in the following will be used to create a
+``Tag`` ``QuerySet``:
+
+ * A string, which may contain multiple tag names.
+ * A list or tuple of strings corresponding to tag names.
+ * A list or tuple of integers corresponding to tag ids.
+
+If given, the following will be returned as-is:
+
+ * A list or tuple of ``Tag`` objects.
+ * A ``Tag`` ``QuerySet``.
+
+``calculate_cloud(tags, steps=4, distribution=tagging.utils.LOGARITHMIC)``
+--------------------------------------------------------------------------
+
+Add a ``font_size`` attribute to each tag according to the frequency of
+its use, as indicated by its ``count`` attribute.
+
+``steps`` defines the range of font sizes - ``font_size`` will be an
+integer between 1 and ``steps`` (inclusive).
+
+``distribution`` defines the type of font size distribution algorithm
+which will be used - logarithmic or linear. It must be one of
+``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+
+
+Model Fields
+============
+
+The ``tagging.fields`` module contains fields which make it easy to
+integrate tagging into your models and into the
+``django.contrib.admin`` application.
+
+Field types
+-----------
+
+``TagField``
+~~~~~~~~~~~~
+
+A ``CharField`` that actually works as a relationship to tags "under
+the hood".
+
+Using this example model::
+
+ class Link(models.Model):
+ ...
+ tags = TagField()
+
+Setting tags::
+
+ >>> l = Link.objects.get(...)
+ >>> l.tags = 'tag1 tag2 tag3'
+
+Getting tags for an instance::
+
+ >>> l.tags
+ 'tag1 tag2 tag3'
+
+Getting tags for a model - i.e. all tags used by all instances of the
+model::
+
+ >>> Link.tags
+ 'tag1 tag2 tag3 tag4 tag5'
+
+This field will also validate that it has been given a valid list of
+tag names, separated by a single comma, a single space or a comma
+followed by a space, using the ``isTagList`` validator from
+``tagging.validators``.
+
+
+Form fields
+===========
+
+The ``tagging.forms`` module contains a ``Field`` for use with
+Django's `newforms library`_ which takes care of validating tag name
+input when used in your forms.
+
+.. _`newforms library`: http://www.djangoproject.com/documentation/newforms/
+
+Field types
+-----------
+
+``TagField``
+~~~~~~~~~~~~
+
+A form ``Field`` which is displayed as a single-line text input, which
+validates that the input it receives is a valid list of tag names.
+
+When you generate a form for one of your models automatically, using
+the ``ModelForm`` class provided by newforms, any
+``tagging.fields.TagField`` fields in your model will automatically be
+represented by a ``tagging.forms.TagField`` in the generated form.
+
+
+Generic views
+=============
+
+The ``tagging.views`` module contains views to handle simple cases of
+common display logic related to tagging.
+
+``tagging.views.tagged_object_list``
+------------------------------------
+
+**Description:**
+
+A view that displays a list of objects for a given model which have a
+given tag. This is a thin wrapper around the
+``django.views.generic.list_detail.object_list`` view, which takes a
+model and a tag as its arguments (in addition to the other optional
+arguments supported by ``object_list``), building the appropriate
+``QuerySet`` for you instead of expecting one to be passed in.
+
+**Required arguments:**
+
+ * ``queryset_or_model``: A ``QuerySet`` or Django model class for the
+ object which will be listed.
+
+ * ``tag``: The tag which objects of the given model must have in
+ order to be listed.
+
+**Optional arguments:**
+
+Please refer to the `object_list documentation`_ for additional optional
+arguments which may be given.
+
+ * ``related_tags``: If ``True``, a ``related_tags`` context variable
+ will also contain tags related to the given tag for the given
+ model.
+
+ * ``related_tag_counts``: If ``True`` and ``related_tags`` is
+ ``True``, each related tag will have a ``count`` attribute
+ indicating the number of items which have it in addition to the
+ given tag.
+
+**Template context:**
+
+Please refer to the `object_list documentation`_ for additional
+template context variables which may be provided.
+
+ * ``tag``: The ``Tag`` instance for the given tag.
+
+.. _`object_list documentation`: http://www.djangoproject.com/documentation/generic_views/#django-views-generic-list-detail-object-list
+
+Example usage
+~~~~~~~~~~~~~
+
+The following sample URLconf demonstrates using this generic view to
+list items of a particular model class which have a given tag::
+
+ from django.conf.urls.defaults import *
+
+ from tagging.views import tagged_object_list
+
+ from shop.apps.products.models import Widget
+
+ urlpatterns = patterns('',
+ url(r'^widgets/tag/(?P<tag>[^/]+)/$',
+ tagged_object_list,
+ dict(model=Widget, paginate_by=10, allow_empty=True,
+ template_object_name='widget'),
+ name='widget_tag_detail'),
+ )
+
+The following sample view demonstrates wrapping this generic view to
+perform filtering of the objects which are listed::
+
+ from myapp.models import People
+
+ from tagging.views import tagged_object_list
+
+ def tagged_people(request, country_code, tag):
+ queryset = People.objects.filter(country__code=country_code)
+ return tagged_object_list(request, queryset, tag, paginate_by=25,
+ allow_empty=True, template_object_name='people')
+
+
+Template tags
+=============
+
+The ``tagging.templatetags.tagging_tags`` module defines a number of
+template tags which may be used to work with tags.
+
+Tag reference
+-------------
+
+tags_for_model
+~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects associated with a given model and
+stores them in a context variable.
+
+Usage::
+
+ {% tags_for_model [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+Extended usage::
+
+ {% tags_for_model [model] as [varname] with counts %}
+
+If specified - by providing extra ``with counts`` arguments - adds a
+``count`` attribute to each tag containing the number of instances of
+the given model which have been tagged with it.
+
+Examples::
+
+ {% tags_for_model products.Widget as widget_tags %}
+ {% tags_for_model products.Widget as widget_tags with counts %}
+
+tag_cloud_for_model
+~~~~~~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects for a given model, with tag cloud
+attributes set, and stores them in a context variable.
+
+Usage::
+
+ {% tag_cloud_for_model [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+Extended usage::
+
+ {% tag_cloud_for_model [model] as [varname] with [options] %}
+
+Extra options can be provided after an optional ``with`` argument, with
+each option being specified in ``[name]=[value]`` format. Valid extra
+options are:
+
+ ``steps``
+ Integer. Defines the range of font sizes.
+
+ ``min_count``
+ Integer. Defines the minimum number of times a tag must have
+ been used to appear in the cloud.
+
+ ``distribution``
+ One of ``linear`` or ``log``. Defines the font-size
+ distribution algorithm to use when generating the tag cloud.
+
+Examples::
+
+ {% tag_cloud_for_model products.Widget as widget_tags %}
+ {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
+
+tags_for_object
+~~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects associated with an object and stores
+them in a context variable.
+
+Usage::
+
+ {% tags_for_object [object] as [varname] %}
+
+Example::
+
+ {% tags_for_object foo_object as tag_list %}
+
+tagged_objects
+~~~~~~~~~~~~~~
+
+Retrieves a list of instances of a given model which are tagged with a
+given ``Tag`` and stores them in a context variable.
+
+Usage::
+
+ {% tagged_objects [tag] in [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+The tag must be an instance of a ``Tag``, not the name of a tag.
+
+Example::
+
+ {% tagged_objects comedy_tag in tv.Show as comedies %}
diff --git a/docs/overview.txt b/docs/overview.txt
index c5c72e6..9641f03 100644
--- a/docs/overview.txt
+++ b/docs/overview.txt
@@ -1,751 +1,912 @@
-==============
-Django Tagging
-==============
-
-A generic tagging application for `Django`_ projects, which allows
-association of a number of tags with any Django model instance and makes
-retrieval of tags simple.
-
-.. _`Django`: http://www.djangoproject.com
-
-.. contents::
- :depth: 3
-
-
-Installation
-============
-
-Installing an official release
-------------------------------
-
-Official releases are made available from
-http://code.google.com/p/django-tagging/
-
-Source distribution
-~~~~~~~~~~~~~~~~~~~
-
-Download the .zip distribution file and unpack it. Inside is a script
-named ``setup.py``. Enter this command::
-
- python setup.py install
-
-...and the package will install automatically.
-
-Windows installer
-~~~~~~~~~~~~~~~~~
-
-A Windows installer is also made available - download the .exe
-distribution file and launch it to install the application.
-
-An uninstaller will also be created, accessible through Add/Remove
-Programs in your Control Panel.
-
-Installing the development version
-----------------------------------
-
-Alternatively, if you'd like to update Django Tagging occasionally to pick
-up the latest bug fixes and enhancements before they make it into an
-offical release, perform a `Subversion`_ checkout instead. The following
-command will check the application's development branch out to an
-``mptt-trunk`` directory::
-
- svn checkout http://django-tagging.googlecode.com/svn/trunk/ tagging-trunk
-
-Add the resulting folder to your `PYTHONPATH`_ or symlink (`junction`_,
-if you're on Windows) the ``tagging`` directory inside it into a
-directory which is on your PYTHONPATH, such as your Python
-installation's ``site-packages`` directory.
-
-You can verify that the application is available on your PYTHONPATH by
-opening a Python interpreter and entering the following commands::
-
- >>> import tagging
- >>> tagging.VERSION
- (0, 2, 'pre')
-
-When you want to update your copy of the Django Tagging source code, run
-the command ``svn update`` from within the ``tagging-trunk`` directory.
-
-.. caution::
-
- The development version may contain bugs which are not present in the
- release version and introduce backwards-incompatible changes.
-
- If you're tracking trunk, keep an eye on the `CHANGELOG`_ and the
- `backwards-incompatible changes wiki page`_ before you update your
- copy of the source code.
-
-.. _`Subversion`: http://subversion.tigris.org
-.. _`PYTHONPATH`: http://docs.python.org/tut/node8.html#SECTION008110000000000000000
-.. _`junction`: http://www.microsoft.com/technet/sysinternals/FileAndDisk/Junction.mspx
-.. _`CHANGELOG`: http://django-tagging.googlecode.com/svn/trunk/CHANGELOG.txt
-.. _`backwards-incompatible changes wiki page`: http://code.google.com/p/django-tagging/wiki/BackwardsIncompatibleChanges
-
-Using Django Tagging in your applications
------------------------------------------
-
-Once you've installed Django Tagging and want to use it in your Django
-applications, do the following:
-
- 1. Put ``'tagging'`` in your ``INSTALLED_APPS`` setting.
- 2. Run the command ``manage.py syncdb``.
-
-The ``syncdb`` command creates the necessary database tables and
-creates permission objects for all installed apps that need them.
-
-That's it!
-
-
-Settings
-========
-
-Some of Django Tagging's behaviour may be configured by adding the
-appropriate settings to your project's settings file.
-
-The following settings are available:
-
-FORCE_LOWERCASE_TAGS
---------------------
-
-Default: ``False``
-
-A boolean that turns on/off forcing of all tag names to lowercase before
-they are saved to the database.
-
-MAX_TAG_LENGTH
---------------
-
-Default: ``50``
-
-An integer which specifies the maxiumum length which any tag is allowed
-to have. This is used for validation in the ``django.contrib.admin``
-application and in any ``newforms`` forms automatically generated using
-``ModelForm``.
-
-
-Tags
-====
-
-Tags are represented by the ``Tag`` model, which lives in the
-``tagging.models`` module.
-
-API reference
--------------
-
-Fields
-~~~~~~
-
-``Tag`` objects have the following fields:
-
- * ``name`` -- The name of the tag. This is a unique value.
-
-Manager functions
-~~~~~~~~~~~~~~~~~
-
-The ``Tag`` model has a custom manager which has the following helper
-functions:
-
- * ``update_tags(obj, tag_names)`` -- updates tags associated with an
- object.
-
- ``tag_names`` is a string containing tag names with which ``obj``
- should be tagged.
-
- If ``tag_names`` is ``None`` or ``''``, the object's tags will be
- cleared.
-
- * ``add_tag(obj, tag_name)`` -- associates a tag with an an object.
-
- ``tag_name`` is a string containing a tag name with which ``obj``
- should be tagged.
-
- * ``get_for_object(obj)`` -- returns a ``QuerySet`` containing all
- ``Tag`` objects associated with ``obj``.
-
- * ``usage_for_model(Model, counts=False, min_count=None, filters=None)``
- -- returns a list of ``Tag`` objects associated with instances of
- ``Model``.
-
- If ``counts`` is ``True``, a ``count`` attribute will be added to
- each tag, indicating how many times it has been associated with
- instances of ``Model``.
-
- If ``min_count`` is given, only tags which have a ``count`` greater
- than or equal to ``min_count`` will be returned. Passing a value
- for ``min_count`` implies ``counts=True``.
-
- To limit the tags (and counts, if specified) returned to those used
- by a subset of the model's instances, pass a dictionary of field
- lookups to be applied to ``Model`` as the ``filters`` argument.
-
- * ``related_for_model(tags, Model, counts=False, min_count=None)``
- -- returns a list of tags related to a given list of tags - that
- is, other tags used by items which have all the given tags.
-
- If ``counts`` is ``True``, a ``count`` attribute will be added to
- each tag, indicating the number of items which have it in addition
- to the given list of tags.
-
- If ``min_count`` is given, only tags which have a ``count`` greater
- than or equal to ``min_count`` will be returned. Passing a value
- for ``min_count`` implies ``counts=True``.
-
- * ``cloud_for_model(Model, steps=4, distribution=LOGARITHMIC,
- filters=None, min_count=None)`` -- returns a list of the
- distinct ``Tag`` objects associated with instances of ``Model``,
- each having a ``count`` attribute as above and an additional
- ``font_size`` attribute, for use in creation of a tag cloud (a
- type of weighted list).
-
- ``steps`` defines the number of font sizes available -
- ``font_size`` may be an integer between ``1`` and ``steps``,
- inclusive.
-
- ``distribution`` defines the type of font size distribution
- algorithm which will be used - logarithmic or linear. It must be
- either ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
-
- To limit the tags displayed in the cloud to those associated
- with a subset of the Model's instances, pass a dictionary of
- field lookups to be applied to the given Model as the ``filters``
- argument.
-
- To limit the tags displayed in the cloud to those with a ``count``
- greater than or equal to ``min_count``, pass a value for the
- ``min_count`` argument.
-
-Basic usage
------------
-
-Tag input
-~~~~~~~~~
-
-Tag input from users is treated as follows:
-
-* If the tag input doesn't contain any commas or double quotes, it is
- simply treated as a space-delimited list of tag names.
-
-* If the tag input does contain either of these characters, we parse the
- input like so:
-
- * 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.
-
- * For the remaining input, if there are any unquoted commas in the
- input, the remainder will be treated as comma-delimited.
- Otherwise, it will be treated as space-delimited.
-
-Examples:
-
-====================== ======================================= ================================================
-Tag input Resulting tag names Notes
-====================== ======================================= ================================================
-apple ball cat [``apple``], [``ball``], [``cat``] No commas or quotes, 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
-====================== ======================================= ================================================
-
-Tagging objects and retrieving an object's tags
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Objects may be tagged using the ``update_tags`` helper function::
-
- >>> from shop.apps.products.models import Widget
- >>> from tagging.models import Tag
- >>> widget = Widget.objects.get(pk=1)
- >>> Tag.objects.update_tags(widget, 'house thing')
-
-Retrieve tags for an object using the ``get_for_object`` helper
-function::
-
- >>> Tag.objects.get_for_object(widget)
- [<Tag: house>, <Tag: thing>]
-
-Tags are created, associated and unassociated accordingly when you use
-``update_tags`` and ``add_tags``::
-
- >>> Tag.objects.update_tags(widget, 'house monkey')
- >>> Tag.objects.get_for_object(widget)
- [<Tag: house>, <Tag: monkey>]
- >>> Tag.objects.add_tag(widget, 'tiles')
- >>> Tag.objects.get_for_object(widget)
- [<Tag: house>, <Tag: monkey>, <Tag: tiles>]
-
-Clear an object's tags by passing ``None`` or ``''`` to
-``update_tags``::
-
- >>> Tag.objects.update_tags(widget, None)
- >>> Tag.objects.get_for_object(widget)
- []
-
-Retrieving tags used by a particular model
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To retrieve all tags used for a particular model, use the
-``get_for_model`` helper function::
-
- >>> widget1 = Widget.objects.get(pk=1)
- >>> Tag.objects.update_tags(widget1, 'house thing')
- >>> widget2 = Widget.objects.get(pk=2)
- >>> Tag.objects.update_tags(widget2, 'cheese toast house')
- >>> Tag.objects.usage_for_model(Widget)
- [<Tag: cheese>, <Tag: house>, <Tag: thing>, <Tag: toast>]
-
-To get a count of how many times each tag was used for a particular
-model, pass in ``True`` for the ``counts`` argument::
-
- >>> tags = Tag.objects.usage_for_model(Widget, counts=True)
- >>> [(tag.name, tag.count) for tag in tags]
- [('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)]
-
-To get counts and limit the tags returned to those with counts above a
-certain size, pass in a ``min_count`` argument::
-
- >>> tags = Tag.objects.usage_for_model(Widget, min_count=2)
- >>> [(tag.name, tag.count) for tag in tags]
- [('house', 2)]
-
-You can also specify a dictionary of `field lookups`_ to be used to
-restrict the tags and counts returned based on a subset of the
-model's instances. For example, the following would retrieve all tags
-used on Widgets created by a user named Alan which have a size
-greater than 99::
-
- >>> Tag.objects.usage_for_model(Widget, filters=dict(size__gt=99, user__username='Alan'))
-
-.. _`field lookups`: http://www.djangoproject.com/documentation/db-api/#field-lookups
-
-
-Tagged items
-============
-
-The relationship between a ``Tag`` and an object is represented by
-the ``TaggedItem`` model, which lives in the ``tagging.models``
-module.
-
-API reference
--------------
-
-Fields
-~~~~~~
-
-``TaggedItem`` objects have the following fields:
-
- * ``tag`` -- The ``Tag`` an object is associated with.
- * ``content_type`` -- The ``ContentType`` of the associated model
- instance.
- * ``object_id`` -- The id of the associated object.
- * ``object`` -- The associated object itself, accessible via the
- Generic Relations API.
-
-Manager functions
-~~~~~~~~~~~~~~~~~
-
-The ``TaggedItem`` model has a custom manager which has the following
-helper functions:
-
- * ``get_by_model(Model, tag)`` -- If ``tag`` is an instance of a
- ``Tag``, returns a ``QuerySet`` containing all instances of
- ``Model`` which are tagged with it.
-
- If ``tag`` is a list of tags, returns a ``QuerySet`` containing
- all instances of ``Model`` which are tagged with every tag in
- the list.
-
- * ``get_intersection_by_model(Model, tags)`` -- Returns a
- ``QuerySet`` containing all instances of ``Model`` which are
- tagged with every tag in the list.
-
- ``get_by_model`` will call this function behind the scenes when
- you pass it a list, so it's recommended that you use
- ``get_by_model`` instead of calling this function directly.
-
- * ``get_union_by_model(Model, tags)`` -- Returns a ``QuerySet``
- containing all instances of ``Model`` which are tagged with any tag
- in the list.
-
- * ``get_related(obj, Model, num=None)`` - Returns instances of
- ``Model`` which share tags with the model instance ``obj``,
- ordered by the number of shared tags in descending order.
-
- If ``num`` is given, a maximum of ``num`` instances will be
- returned.
-
-Basic usage
------------
-
-Retrieving tagged objects
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Objects may be retrieved based on their tags using the ``get_by_model``
-manager method::
-
- >>> from shop.apps.products.models import Widget
- >>> from tagging.models import Tag
- >>> house_tag = Tag.objects.get(name='house')
- >>> TaggedItem.objects.get_by_model(Widget, house_tag)
- [<Widget: pk=1>, <Widget: pk=2>]
-
-Passing a list of tags to ``get_by_model`` returns an intersection of
-objects which have those tags, i.e. tag1 AND tag2 ... AND tagN::
-
- >>> thing_tag = Tag.objects.get(name='thing')
- >>> TaggedItem.objects.get_by_model(Widget, [house_tag, thing_tag])
- [<Widget: pk=1>]
-
-Functions which take tags are flexible when it comes to tag input::
-
- >>> TaggedItem.objects.get_by_model(Widget, Tag.objects.filter(name__in=['house', 'thing']))
- [<Widget: pk=1>]
- >>> TaggedItem.objects.get_by_model(Widget, 'house thing')
- [<Widget: pk=1>]
- >>> TaggedItem.objects.get_by_model(Widget, ['house', 'thing'])
- [<Widget: pk=1>]
-
-
-Utilities
-=========
-
-Tag-related utility functions are defined in the ``tagging.utils``
-module:
-
-``parse_tag_input(input)``
---------------------------
-
-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.
-
-See `tag input`_ for more details.
-
-``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.
-
-``get_tag_list(tags)``
-----------------------
-
-Utility function for accepting tag input in a flexible manner.
-
-If a ``Tag`` object is given, it will be returned in a list as its
-single occupant.
-
-If given, the tag names in the following will be used to create a
-``Tag`` ``QuerySet``:
-
- * A string, which may contain multiple tag names.
- * A list or tuple of strings corresponding to tag names.
- * A list or tuple of integers corresponding to tag ids.
-
-If given, the following will be returned as-is:
-
- * A list or tuple of ``Tag`` objects.
- * A ``Tag`` ``QuerySet``.
-
-``calculate_cloud(tags, steps=4, distribution=tagging.utils.LOGARITHMIC)``
---------------------------------------------------------------------------
-
-Add a ``font_size`` attribute to each tag according to the frequency of
-its use, as indicated by its ``count`` attribute.
-
-``steps`` defines the range of font sizes - ``font_size`` will be an
-integer between 1 and ``steps`` (inclusive).
-
-``distribution`` defines the type of font size distribution algorithm
-which will be used - logarithmic or linear. It must be one of
-``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
-
-
-Model Fields
-============
-
-The ``tagging.fields`` module contains fields which make it easy to
-integrate tagging into your models and into the
-``django.contrib.admin`` application.
-
-Field types
------------
-
-``TagField``
-~~~~~~~~~~~~
-
-A ``CharField`` that actually works as a relationship to tags "under
-the hood".
-
-Using this example model::
-
- class Link(models.Model):
- ...
- tags = TagField()
-
-Setting tags::
-
- >>> l = Link.objects.get(...)
- >>> l.tags = 'tag1 tag2 tag3'
-
-Getting tags for an instance::
-
- >>> l.tags
- 'tag1 tag2 tag3'
-
-Getting tags for a model - i.e. all tags used by all instances of the
-model::
-
- >>> Link.tags
- 'tag1 tag2 tag3 tag4 tag5'
-
-This field will also validate that it has been given a valid list of
-tag names, separated by a single comma, a single space or a comma
-followed by a space, using the ``isTagList`` validator from
-``tagging.validators``.
-
-
-Form fields
-===========
-
-The ``tagging.forms`` module contains a ``Field`` for use with
-Django's `newforms library`_ which takes care of validating tag name
-input when used in your forms.
-
-.. _`newforms library`: http://www.djangoproject.com/documentation/newforms/
-
-Field types
------------
-
-``TagField``
-~~~~~~~~~~~~
-
-A form ``Field`` which is displayed as a single-line text input, which
-validates that the input it receives is a valid list of tag names.
-
-When you generate a form for one of your models automatically, using
-the ``ModelForm`` class provided by newforms, any
-``tagging.fields.TagField`` fields in your model will automatically be
-represented by a ``tagging.forms.TagField`` in the generated form.
-
-
-Simplified tagging and retrieval of tags with properties
-========================================================
-
-If you're not using ``TagField``, a useful method for simplifying
-tagging and retrieval of tags for your models is to set up a
-property::
-
- from django.db import models
- from tagging.models import Tag
-
- class MyModel(models.Model):
- name = models.CharField(maxlength=100)
- tag_list = models.CharField(maxlength=255)
-
- def save(self):
- super(MyModel, self).save()
- self.tags = self.tag_list
-
- def _get_tags(self):
- return Tag.objects.get_for_object(self)
-
- def _set_tags(self, tag_list):
- Tag.objects.update_tags(self, tag_list)
-
- tags = property(_get_tags, _set_tags)
-
- def __unicode__(self):
- return self.name
-
-Once you've set this up, you can access and set tags in a fairly
-natural way::
-
- >>> obj = MyModel.objects.get(pk=1)
- >>> obj.tags = 'foo bar'
- >>> obj.tags
- [<Tag: bar>, <Tag: foo>]
-
-Remember that ``obj.tags`` will return a ``QuerySet``, so you can
-perform further filtering on it, should you need to.
-
-
-Generic views
-=============
-
-The ``tagging.views`` module contains views to handle simple cases of
-common display logic related to tagging.
-
-``tagging.views.tagged_object_list``
-------------------------------------
-
-**Description:**
-
-A view that displays a list of objects for a given model which have a
-given tag. This is a thin wrapper around the
-``django.views.generic.list_detail.object_list`` view, which takes a
-model and a tag as its arguments (in addition to the other optional
-arguments supported by ``object_list``), building the appropriate
-``QuerySet`` for you instead of expecting one to be passed in.
-
-**Required arguments:**
-
- * ``model``: The Django model class of the object that will be
- listed.
-
- * ``tag``: The tag which objects of the given model must have in
- order to be listed.
-
-**Optional arguments:**
-
-Please refer to the `object_list documentation`_ for additional optional
-arguments which may be given.
-
- * ``related_tags``: If ``True``, a ``related_tags`` context variable
- will also contain tags related to the given tag for the given
- model.
-
- * ``related_tag_counts``: If ``True`` and ``related_tags`` is
- ``True``, each related tag will have a ``count`` attribute
- indicating the number of items which have it in addition to the
- given tag.
-
-**Template context:**
-
-Please refer to the `object_list documentation`_ for additional
-template context variables which may be provided.
-
- * ``tag``: The ``Tag`` instance for the given tag.
-
-.. _`object_list documentation`: http://www.djangoproject.com/documentation/generic_views/#django-views-generic-list-detail-object-list
-
-Example usage
-~~~~~~~~~~~~~
-
-The following sample URLconf demonstrates using this generic view to
-list items of a particular model class which have a given tag::
-
- from django.conf.urls.defaults import *
- from tagging.views import tagged_object_list
- from shop.apps.products.models import Widget
-
- urlpatterns = patterns('',
- url(r'^widgets/tag/(?P<tag>[^/]+)/$',
- tagged_object_list,
- dict(model=Widget, paginate_by=10, allow_empty=True,
- template_object_name='widget'),
- name='widget_tag_detail'),
- )
-
-
-Template tags
-=============
-
-The ``tagging.templatetags.tagging_tags`` module defines a number of
-template tags which may be used to work with tags.
-
-Tag reference
--------------
-
-tags_for_model
-~~~~~~~~~~~~~~
-
-Retrieves a list of ``Tag`` objects associated with a given model and
-stores them in a context variable.
-
-Usage::
-
- {% tags_for_model [model] as [varname] %}
-
-The model is specified in ``[appname].[modelname]`` format.
-
-Extended usage::
-
- {% tags_for_model [model] as [varname] with counts %}
-
-If specified - by providing extra ``with counts`` arguments - adds a
-``count`` attribute to each tag containing the number of instances of
-the given model which have been tagged with it.
-
-Examples::
-
- {% tags_for_model products.Widget as widget_tags %}
- {% tags_for_model products.Widget as widget_tags with counts %}
-
-tag_cloud_for_model
-~~~~~~~~~~~~~~~~~~~
-
-Retrieves a list of ``Tag`` objects for a given model, with tag cloud
-attributes set, and stores them in a context variable.
-
-Usage::
-
- {% tag_cloud_for_model [model] as [varname] %}
-
-The model is specified in ``[appname].[modelname]`` format.
-
-Extended usage::
-
- {% tag_cloud_for_model [model] as [varname] with [options] %}
-
-Extra options can be provided after an optional ``with`` argument, with
-each option being specified in ``[name]=[value]`` format. Valid extra
-options are:
-
- ``steps``
- Integer. Defines the range of font sizes.
-
- ``min_count``
- Integer. Defines the minimum number of times a tag must have
- been used to appear in the cloud.
-
- ``distribution``
- One of ``linear`` or ``log``. Defines the font-size
- distribution algorithm to use when generating the tag cloud.
-
-Examples::
-
- {% tag_cloud_for_model products.Widget as widget_tags %}
- {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
-
-tags_for_object
-~~~~~~~~~~~~~~~
-
-Retrieves a list of ``Tag`` objects associated with an object and stores
-them in a context variable.
-
-Usage::
-
- {% tags_for_object [object] as [varname] %}
-
-Example::
-
- {% tags_for_object foo_object as tag_list %}
-
-tagged_objects
-~~~~~~~~~~~~~~
-
-Retrieves a list of instances of a given model which are tagged with a
-given ``Tag`` and stores them in a context variable.
-
-Usage::
-
- {% tagged_objects [tag] in [model] as [varname] %}
-
-The model is specified in ``[appname].[modelname]`` format.
-
-The tag must be an instance of a ``Tag``, not the name of a tag.
-
-Example::
-
- {% tagged_objects comedy_tag in tv.Show as comedies %}
+==============
+Django Tagging
+==============
+
+A generic tagging application for `Django`_ projects, which allows
+association of a number of tags with any Django model instance and makes
+retrieval of tags simple.
+
+.. _`Django`: http://www.djangoproject.com
+
+.. contents::
+ :depth: 3
+
+
+Installation
+============
+
+Installing an official release
+------------------------------
+
+Official releases are made available from
+http://code.google.com/p/django-tagging/
+
+Source distribution
+~~~~~~~~~~~~~~~~~~~
+
+Download the .zip distribution file and unpack it. Inside is a script
+named ``setup.py``. Enter this command::
+
+ python setup.py install
+
+...and the package will install automatically.
+
+Windows installer
+~~~~~~~~~~~~~~~~~
+
+A Windows installer is also made available - download the .exe
+distribution file and launch it to install the application.
+
+An uninstaller will also be created, accessible through Add/Remove
+Programs in your Control Panel.
+
+Installing the development version
+----------------------------------
+
+Alternatively, if you'd like to update Django Tagging occasionally to pick
+up the latest bug fixes and enhancements before they make it into an
+official release, perform a `Subversion`_ checkout instead. The following
+command will check the application's development branch out to an
+``tagging-trunk`` directory::
+
+ svn checkout http://django-tagging.googlecode.com/svn/trunk/ tagging-trunk
+
+Add the resulting folder to your `PYTHONPATH`_ or symlink (`junction`_,
+if you're on Windows) the ``tagging`` directory inside it into a
+directory which is on your PYTHONPATH, such as your Python
+installation's ``site-packages`` directory.
+
+You can verify that the application is available on your PYTHONPATH by
+opening a Python interpreter and entering the following commands::
+
+ >>> import tagging
+ >>> tagging.VERSION
+ (0, 3, 'pre')
+
+When you want to update your copy of the Django Tagging source code, run
+the command ``svn update`` from within the ``tagging-trunk`` directory.
+
+.. caution::
+
+ The development version may contain bugs which are not present in the
+ release version and introduce backwards-incompatible changes.
+
+ If you're tracking trunk, keep an eye on the `CHANGELOG`_ and the
+ `backwards-incompatible changes wiki page`_ before you update your
+ copy of the source code.
+
+.. _`Subversion`: http://subversion.tigris.org
+.. _`PYTHONPATH`: http://docs.python.org/tut/node8.html#SECTION008110000000000000000
+.. _`junction`: http://www.microsoft.com/technet/sysinternals/FileAndDisk/Junction.mspx
+.. _`CHANGELOG`: http://django-tagging.googlecode.com/svn/trunk/CHANGELOG.txt
+.. _`backwards-incompatible changes wiki page`: http://code.google.com/p/django-tagging/wiki/BackwardsIncompatibleChanges
+
+Using Django Tagging in your applications
+-----------------------------------------
+
+Once you've installed Django Tagging and want to use it in your Django
+applications, do the following:
+
+ 1. Put ``'tagging'`` in your ``INSTALLED_APPS`` setting.
+ 2. Run the command ``manage.py syncdb``.
+
+The ``syncdb`` command creates the necessary database tables and
+creates permission objects for all installed apps that need them.
+
+That's it!
+
+
+Settings
+========
+
+Some of the application's behaviour can be configured by adding the
+appropriate settings to your project's settings file.
+
+The following settings are available:
+
+FORCE_LOWERCASE_TAGS
+--------------------
+
+Default: ``False``
+
+A boolean that turns on/off forcing of all tag names to lowercase before
+they are saved to the database.
+
+MAX_TAG_LENGTH
+--------------
+
+Default: ``50``
+
+An integer which specifies the maximum length which any tag is allowed
+to have. This is used for validation in the ``django.contrib.admin``
+application and in any ``newforms`` forms automatically generated using
+``ModelForm``.
+
+
+Registering your models
+=======================
+
+**New in developement version**
+
+Your Django models can be registered with the tagging application to
+access some additional tagging-related features.
+
+.. note::
+
+ You don't *have* to register your models in order to use them with
+ the tagging application - many of the features added by registration
+ are just convenience wrappers around the tagging API provided by the
+ ``Tag`` and ``TaggedItem`` models and their managers, as documented
+ further below.
+
+The ``register`` function
+-------------------------
+
+To register a model, import the ``tagging`` module and call its
+``register`` function, like so::
+
+ from django.db import models
+
+ import tagging
+
+ class Widget(models.Model):
+ name = models.CharField(max_length=50)
+
+ tagging.register(Widget)
+
+The following argument is required:
+
+``model``
+ The model class to be registered.
+
+ An exception will be raised if you attempt to register the same class
+ more than once.
+
+The following arguments are optional, with some recommended defaults -
+take care to specify different attribute names if the defaults clash
+with your model class' definition:
+
+``tag_descriptor_attr``
+ The name of an attribute in the model class which will hold a tag
+ descriptor for the model. Default: ``'tags'``
+
+ See `TagDescriptor`_ below for details about the use of this
+ descriptor.
+
+``tagged_item_manger_attr``
+ The name of an attribute in the model class which will hold a custom
+ manager for accessing tagged items for the model. Default:
+ ``'tagged'``.
+
+ See `ModelTaggedItemManager`_ below for details about the use of this
+ manager.
+
+``TagDescriptor``
+-----------------
+
+When accessed through the model class itself, this descriptor will return
+a ``ModelTagManager`` for the model. See `ModelTagManager`_ below for
+more details about its use.
+
+When accessed through a model instance, this descriptor provides a handy
+means of retrieving, updating and deleting the instance's tags. For
+example::
+
+ >>> widget = Widget.objects.create(name='Testing descriptor')
+ >>> widget.tags
+ []
+ >>> widget.tags = 'toast, melted cheese, butter'
+ >>> widget.tags
+ [<Tag: butter>, <Tag: melted cheese>, <Tag: toast>]
+ >>> del widget.tags
+ >>> widget.tags
+ []
+
+``ModelTagManager``
+-------------------
+
+A manager for retrieving tags used by a particular model.
+
+Defines the following methods:
+
+* ``get_query_set()`` -- as this method is redefined, any ``QuerySets``
+ created by this model will be initially restricted to contain the
+ distinct tags used by all the model's instances.
+
+* ``cloud(*args, **kwargs)`` -- creates a list of tags used by the
+ model's instances, with ``count`` and ``font_size`` attributes set for
+ use in displaying a tag cloud.
+
+ See the documentation on ``Tag``'s manager's `cloud_for_model method`_
+ for information on additional arguments which can be given.
+
+* ``related(self, tags, *args, **kwargs)`` -- creates a list of tags
+ used by the model's instances, which are also used by all instance
+ which have the given ``tags``.
+
+ See the documentation on ``Tag``'s manager's
+ `related_for_model method`_ for information on additional arguments
+ which can be given.
+
+* ``usage(self, *args, **kwargs))`` -- creates a list of tags used by
+ the model's instances, with optional usages counts, restriction based
+ on usage counts and restriction of the model instances from which
+ usage and counts are determined.
+
+ See the documentation on ``Tag``'s manager's `usage_for_model method`_
+ for information on additional arguments which can be given.
+
+Example usage::
+
+ # Create a ``QuerySet`` of tags used by Widget instances
+ Widget.tags.all()
+
+ # Retrieve a list of tags used by Widget instances with usage counts
+ Widget.tags.usage(counts=True)
+
+ # Retrieve tags used by instances of WIdget which are also tagged with
+ # 'cheese' and 'toast'
+ Widget.tags.related(['cheese', 'toast'], counts=True, min_count=3)
+
+``ModelTaggedItemManager``
+--------------------------
+
+A manager for retrieving model instance for a particular model, based on
+their tags.
+
+* ``related_to(obj, queryset=None, num=None)`` -- creates a list
+ of model instances which are related to ``obj``, based on its tags. If
+ a ``queryset`` argument is provided, it will be used to restrict the
+ resulting list of model instances.
+
+ If ``num`` is given, a maximum of ``num`` instances will be returned.
+
+* ``with_all(tags, queryset=None)`` -- creates a ``QuerySet`` containing
+ model instances which are tagged with *all* the given tags. If a
+ ``queryset`` argument is provided, it will be used as the basis for
+ the resulting ``QuerySet``.
+
+* ``with_any(tags, queryset=None)`` -- creates a ``QuerySet`` containing model
+ instances which are tagged with *any* the given tags. If a ``queryset``
+ argument is provided, it will be used as the basis for the resulting
+ ``QuerySet``.
+
+
+Tags
+====
+
+Tags are represented by the ``Tag`` model, which lives in the
+``tagging.models`` module.
+
+API reference
+-------------
+
+Fields
+~~~~~~
+
+``Tag`` objects have the following fields:
+
+* ``name`` -- The name of the tag. This is a unique value.
+
+Manager functions
+~~~~~~~~~~~~~~~~~
+
+The ``Tag`` model has a custom manager which has the following helper
+methods:
+
+* ``update_tags(obj, tag_names)`` -- updates tags associated with an
+ object.
+
+ ``tag_names`` is a string containing tag names with which ``obj``
+ should be tagged.
+
+ If ``tag_names`` is ``None`` or ``''``, the object's tags will be
+ cleared.
+
+* ``add_tag(obj, tag_name)`` -- associates a tag with an an object.
+
+ ``tag_name`` is a string containing a tag name with which ``obj``
+ should be tagged.
+
+* ``get_for_object(obj)`` -- returns a ``QuerySet`` containing all
+ ``Tag`` objects associated with ``obj``.
+
+.. _`usage_for_model method`:
+
+* ``usage_for_model(model, counts=False, min_count=None, filters=None)``
+ -- returns a list of ``Tag`` objects associated with instances of
+ ``model``.
+
+ If ``counts`` is ``True``, a ``count`` attribute will be added to each
+ tag, indicating how many times it has been associated with instances
+ of ``model``.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned. Passing a value for
+ ``min_count`` implies ``counts=True``.
+
+ To limit the tags (and counts, if specified) returned to those used by
+ a subset of the model's instances, pass a dictionary of field lookups
+ to be applied to ``model`` as the ``filters`` argument.
+
+.. _`related_for_model method`:
+
+* ``related_for_model(tags, Model, counts=False, min_count=None)``
+ -- returns a list of tags related to a given list of tags - that is,
+ other tags used by items which have all the given tags.
+
+ If ``counts`` is ``True``, a ``count`` attribute will be added to each
+ tag, indicating the number of items which have it in addition to the
+ given list of tags.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned. Passing a value for
+ ``min_count`` implies ``counts=True``.
+
+.. _`cloud_for_model method`:
+
+* ``cloud_for_model(Model, steps=4, distribution=LOGARITHMIC,
+ filters=None, min_count=None)`` -- returns a list of the distinct
+ ``Tag`` objects associated with instances of ``Model``, each having a
+ ``count`` attribute as above and an additional ``font_size``
+ attribute, for use in creation of a tag cloud (a type of weighted
+ list).
+
+ ``steps`` defines the number of font sizes available - ``font_size``
+ may be an integer between ``1`` and ``steps``, inclusive.
+
+ ``distribution`` defines the type of font size distribution algorithm
+ which will be used - logarithmic or linear. It must be either
+ ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+
+ To limit the tags displayed in the cloud to those associated with a
+ subset of the Model's instances, pass a dictionary of field lookups to
+ be applied to the given Model as the ``filters`` argument.
+
+ To limit the tags displayed in the cloud to those with a ``count``
+ greater than or equal to ``min_count``, pass a value for the
+ ``min_count`` argument.
+
+**New in development version**
+
+* ``usage_for_queryset(queryset, counts=False, min_count=None)`` --
+ Obtains a list of tags associated with instances of a model contained
+ in the given queryset.
+
+ If ``counts`` is True, a ``count`` attribute will be added to each tag,
+ indicating how many times it has been used against the Model class in
+ question.
+
+ If ``min_count`` is given, only tags which have a ``count`` greater
+ than or equal to ``min_count`` will be returned.
+
+ Passing a value for ``min_count`` implies ``counts=True``.
+
+Basic usage
+-----------
+
+Tagging objects and retrieving an object's tags
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Objects may be tagged using the ``update_tags`` helper function::
+
+ >>> from shop.apps.products.models import Widget
+ >>> from tagging.models import Tag
+ >>> widget = Widget.objects.get(pk=1)
+ >>> Tag.objects.update_tags(widget, 'house thing')
+
+Retrieve tags for an object using the ``get_for_object`` helper
+function::
+
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: thing>]
+
+Tags are created, associated and unassociated accordingly when you use
+``update_tags`` and ``add_tags``::
+
+ >>> Tag.objects.update_tags(widget, 'house monkey')
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: monkey>]
+ >>> Tag.objects.add_tag(widget, 'tiles')
+ >>> Tag.objects.get_for_object(widget)
+ [<Tag: house>, <Tag: monkey>, <Tag: tiles>]
+
+Clear an object's tags by passing ``None`` or ``''`` to
+``update_tags``::
+
+ >>> Tag.objects.update_tags(widget, None)
+ >>> Tag.objects.get_for_object(widget)
+ []
+
+Retrieving tags used by a particular model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To retrieve all tags used for a particular model, use the
+``get_for_model`` helper function::
+
+ >>> widget1 = Widget.objects.get(pk=1)
+ >>> Tag.objects.update_tags(widget1, 'house thing')
+ >>> widget2 = Widget.objects.get(pk=2)
+ >>> Tag.objects.update_tags(widget2, 'cheese toast house')
+ >>> Tag.objects.usage_for_model(Widget)
+ [<Tag: cheese>, <Tag: house>, <Tag: thing>, <Tag: toast>]
+
+To get a count of how many times each tag was used for a particular
+model, pass in ``True`` for the ``counts`` argument::
+
+ >>> tags = Tag.objects.usage_for_model(Widget, counts=True)
+ >>> [(tag.name, tag.count) for tag in tags]
+ [('cheese', 1), ('house', 2), ('thing', 1), ('toast', 1)]
+
+To get counts and limit the tags returned to those with counts above a
+certain size, pass in a ``min_count`` argument::
+
+ >>> tags = Tag.objects.usage_for_model(Widget, min_count=2)
+ >>> [(tag.name, tag.count) for tag in tags]
+ [('house', 2)]
+
+You can also specify a dictionary of `field lookups`_ to be used to
+restrict the tags and counts returned based on a subset of the
+model's instances. For example, the following would retrieve all tags
+used on Widgets created by a user named Alan which have a size
+greater than 99::
+
+ >>> Tag.objects.usage_for_model(Widget, filters=dict(size__gt=99, user__username='Alan'))
+
+.. _`field lookups`: http://www.djangoproject.com/documentation/db-api/#field-lookups
+
+**New in development version**
+
+The ``usage_for_queryset`` method allows you to pass a pre-filtered
+queryset to be used when determining tag usage::
+
+ >>> Tag.objects.usage_for_queryset(Widget.objects.filter(size__gt=99, user__username='Alan'))
+
+Tag input
+---------
+
+Tag input from users is treated 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, we parse the
+ input like so:
+
+ * 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.
+
+ * For the remaining input, if there are any unquoted commas in the
+ input, the remainder will be treated as comma-delimited. Otherwise,
+ 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
+====================== ======================================= ================================================
+
+
+Tagged items
+============
+
+The relationship between a ``Tag`` and an object is represented by
+the ``TaggedItem`` model, which lives in the ``tagging.models``
+module.
+
+API reference
+-------------
+
+Fields
+~~~~~~
+
+``TaggedItem`` objects have the following fields:
+
+* ``tag`` -- The ``Tag`` an object is associated with.
+* ``content_type`` -- The ``ContentType`` of the associated model
+ instance.
+* ``object_id`` -- The id of the associated object.
+* ``object`` -- The associated object itself, accessible via the
+ Generic Relations API.
+
+Manager functions
+~~~~~~~~~~~~~~~~~
+
+The ``TaggedItem`` model has a custom manager which has the following
+helper methods, which accept either a ``QuerySet`` or a ``Model``
+class as one of their arguments. To restrict the objects which are
+returned, pass in a filtered ``QuerySet`` for this argument:
+
+* ``get_by_model(queryset_or_model, tag)`` -- creates a ``QuerySet``
+ containing instances of the specififed model which are tagged with
+ the given tag or tags.
+
+* ``get_intersection_by_model(queryset_or_model, tags)`` -- creates a
+ ``QuerySet`` containing instances of the specified model which are
+ tagged with every tag in a list of tags.
+
+ ``get_by_model`` will call this function behind the scenes when you
+ pass it a list, so you can use ``get_by_model`` instead of calling
+ this method directly.
+
+* ``get_union_by_model(queryset_or_model, tags)`` -- creates a
+ ``QuerySet`` containing instances of the specified model which are
+ tagged with any tag in a list of tags.
+
+.. _`get_related method`:
+
+* ``get_related(obj, queryset_or_model, num=None)`` - returns a list of
+ instances of the specified model which share tags with the model
+ instance ``obj``, ordered by the number of shared tags in descending
+ order.
+
+ If ``num`` is given, a maximum of ``num`` instances will be returned.
+
+Basic usage
+-----------
+
+Retrieving tagged objects
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Objects may be retrieved based on their tags using the ``get_by_model``
+manager method::
+
+ >>> from shop.apps.products.models import Widget
+ >>> from tagging.models import Tag
+ >>> house_tag = Tag.objects.get(name='house')
+ >>> TaggedItem.objects.get_by_model(Widget, house_tag)
+ [<Widget: pk=1>, <Widget: pk=2>]
+
+Passing a list of tags to ``get_by_model`` returns an intersection of
+objects which have those tags, i.e. tag1 AND tag2 ... AND tagN::
+
+ >>> thing_tag = Tag.objects.get(name='thing')
+ >>> TaggedItem.objects.get_by_model(Widget, [house_tag, thing_tag])
+ [<Widget: pk=1>]
+
+Functions which take tags are flexible when it comes to tag input::
+
+ >>> TaggedItem.objects.get_by_model(Widget, Tag.objects.filter(name__in=['house', 'thing']))
+ [<Widget: pk=1>]
+ >>> TaggedItem.objects.get_by_model(Widget, 'house thing')
+ [<Widget: pk=1>]
+ >>> TaggedItem.objects.get_by_model(Widget, ['house', 'thing'])
+ [<Widget: pk=1>]
+
+Restricting objects returned
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Pass in a ``QuerySet`` to restrict the objects returned::
+
+ # Retrieve all Widgets which have a price less than 50, tagged with 'house'
+ TaggedItem.objects.get_by_model(Widget.objects.filter(price__lt=50), 'house')
+
+ # Retrieve all Widgets which have a name starting with 'a', tagged with any
+ # of 'house', 'garden' or 'water'.
+ TaggedItem.objects.get_union_by_model(Widget.objects.filter(name__startswith='a'),
+ ['house', 'garden', 'water'])
+
+
+Utilities
+=========
+
+Tag-related utility functions are defined in the ``tagging.utils``
+module:
+
+``parse_tag_input(input)``
+--------------------------
+
+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.
+
+See `tag input`_ for more details.
+
+``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.
+
+``get_tag_list(tags)``
+----------------------
+
+Utility function for accepting tag input in a flexible manner.
+
+If a ``Tag`` object is given, it will be returned in a list as its
+single occupant.
+
+If given, the tag names in the following will be used to create a
+``Tag`` ``QuerySet``:
+
+ * A string, which may contain multiple tag names.
+ * A list or tuple of strings corresponding to tag names.
+ * A list or tuple of integers corresponding to tag ids.
+
+If given, the following will be returned as-is:
+
+ * A list or tuple of ``Tag`` objects.
+ * A ``Tag`` ``QuerySet``.
+
+``calculate_cloud(tags, steps=4, distribution=tagging.utils.LOGARITHMIC)``
+--------------------------------------------------------------------------
+
+Add a ``font_size`` attribute to each tag according to the frequency of
+its use, as indicated by its ``count`` attribute.
+
+``steps`` defines the range of font sizes - ``font_size`` will be an
+integer between 1 and ``steps`` (inclusive).
+
+``distribution`` defines the type of font size distribution algorithm
+which will be used - logarithmic or linear. It must be one of
+``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+
+
+Model Fields
+============
+
+The ``tagging.fields`` module contains fields which make it easy to
+integrate tagging into your models and into the
+``django.contrib.admin`` application.
+
+Field types
+-----------
+
+``TagField``
+~~~~~~~~~~~~
+
+A ``CharField`` that actually works as a relationship to tags "under
+the hood".
+
+Using this example model::
+
+ class Link(models.Model):
+ ...
+ tags = TagField()
+
+Setting tags::
+
+ >>> l = Link.objects.get(...)
+ >>> l.tags = 'tag1 tag2 tag3'
+
+Getting tags for an instance::
+
+ >>> l.tags
+ 'tag1 tag2 tag3'
+
+Getting tags for a model - i.e. all tags used by all instances of the
+model::
+
+ >>> Link.tags
+ 'tag1 tag2 tag3 tag4 tag5'
+
+This field will also validate that it has been given a valid list of
+tag names, separated by a single comma, a single space or a comma
+followed by a space, using the ``isTagList`` validator from
+``tagging.validators``.
+
+
+Form fields
+===========
+
+The ``tagging.forms`` module contains a ``Field`` for use with
+Django's `newforms library`_ which takes care of validating tag name
+input when used in your forms.
+
+.. _`newforms library`: http://www.djangoproject.com/documentation/newforms/
+
+Field types
+-----------
+
+``TagField``
+~~~~~~~~~~~~
+
+A form ``Field`` which is displayed as a single-line text input, which
+validates that the input it receives is a valid list of tag names.
+
+When you generate a form for one of your models automatically, using
+the ``ModelForm`` class provided by newforms, any
+``tagging.fields.TagField`` fields in your model will automatically be
+represented by a ``tagging.forms.TagField`` in the generated form.
+
+
+Generic views
+=============
+
+The ``tagging.views`` module contains views to handle simple cases of
+common display logic related to tagging.
+
+``tagging.views.tagged_object_list``
+------------------------------------
+
+**Description:**
+
+A view that displays a list of objects for a given model which have a
+given tag. This is a thin wrapper around the
+``django.views.generic.list_detail.object_list`` view, which takes a
+model and a tag as its arguments (in addition to the other optional
+arguments supported by ``object_list``), building the appropriate
+``QuerySet`` for you instead of expecting one to be passed in.
+
+**Required arguments:**
+
+ * ``queryset_or_model``: A ``QuerySet`` or Django model class for the
+ object which will be listed.
+
+ * ``tag``: The tag which objects of the given model must have in
+ order to be listed.
+
+**Optional arguments:**
+
+Please refer to the `object_list documentation`_ for additional optional
+arguments which may be given.
+
+ * ``related_tags``: If ``True``, a ``related_tags`` context variable
+ will also contain tags related to the given tag for the given
+ model.
+
+ * ``related_tag_counts``: If ``True`` and ``related_tags`` is
+ ``True``, each related tag will have a ``count`` attribute
+ indicating the number of items which have it in addition to the
+ given tag.
+
+**Template context:**
+
+Please refer to the `object_list documentation`_ for additional
+template context variables which may be provided.
+
+ * ``tag``: The ``Tag`` instance for the given tag.
+
+.. _`object_list documentation`: http://www.djangoproject.com/documentation/generic_views/#django-views-generic-list-detail-object-list
+
+Example usage
+~~~~~~~~~~~~~
+
+The following sample URLconf demonstrates using this generic view to
+list items of a particular model class which have a given tag::
+
+ from django.conf.urls.defaults import *
+
+ from tagging.views import tagged_object_list
+
+ from shop.apps.products.models import Widget
+
+ urlpatterns = patterns('',
+ url(r'^widgets/tag/(?P<tag>[^/]+)/$',
+ tagged_object_list,
+ dict(model=Widget, paginate_by=10, allow_empty=True,
+ template_object_name='widget'),
+ name='widget_tag_detail'),
+ )
+
+The following sample view demonstrates wrapping this generic view to
+perform filtering of the objects which are listed::
+
+ from myapp.models import People
+
+ from tagging.views import tagged_object_list
+
+ def tagged_people(request, country_code, tag):
+ queryset = People.objects.filter(country__code=country_code)
+ return tagged_object_list(request, queryset, tag, paginate_by=25,
+ allow_empty=True, template_object_name='people')
+
+
+Template tags
+=============
+
+The ``tagging.templatetags.tagging_tags`` module defines a number of
+template tags which may be used to work with tags.
+
+Tag reference
+-------------
+
+tags_for_model
+~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects associated with a given model and
+stores them in a context variable.
+
+Usage::
+
+ {% tags_for_model [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+Extended usage::
+
+ {% tags_for_model [model] as [varname] with counts %}
+
+If specified - by providing extra ``with counts`` arguments - adds a
+``count`` attribute to each tag containing the number of instances of
+the given model which have been tagged with it.
+
+Examples::
+
+ {% tags_for_model products.Widget as widget_tags %}
+ {% tags_for_model products.Widget as widget_tags with counts %}
+
+tag_cloud_for_model
+~~~~~~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects for a given model, with tag cloud
+attributes set, and stores them in a context variable.
+
+Usage::
+
+ {% tag_cloud_for_model [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+Extended usage::
+
+ {% tag_cloud_for_model [model] as [varname] with [options] %}
+
+Extra options can be provided after an optional ``with`` argument, with
+each option being specified in ``[name]=[value]`` format. Valid extra
+options are:
+
+ ``steps``
+ Integer. Defines the range of font sizes.
+
+ ``min_count``
+ Integer. Defines the minimum number of times a tag must have
+ been used to appear in the cloud.
+
+ ``distribution``
+ One of ``linear`` or ``log``. Defines the font-size
+ distribution algorithm to use when generating the tag cloud.
+
+Examples::
+
+ {% tag_cloud_for_model products.Widget as widget_tags %}
+ {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
+
+tags_for_object
+~~~~~~~~~~~~~~~
+
+Retrieves a list of ``Tag`` objects associated with an object and stores
+them in a context variable.
+
+Usage::
+
+ {% tags_for_object [object] as [varname] %}
+
+Example::
+
+ {% tags_for_object foo_object as tag_list %}
+
+tagged_objects
+~~~~~~~~~~~~~~
+
+Retrieves a list of instances of a given model which are tagged with a
+given ``Tag`` and stores them in a context variable.
+
+Usage::
+
+ {% tagged_objects [tag] in [model] as [varname] %}
+
+The model is specified in ``[appname].[modelname]`` format.
+
+The tag must be an instance of a ``Tag``, not the name of a tag.
+
+Example::
+
+ {% tagged_objects comedy_tag in tv.Show as comedies %}
diff --git a/setup.py b/setup.py
index 1cc8832..66daf3e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,65 +1,72 @@
-"""
-Based entirely on Django's own ``setup.py``.
-"""
-import os
-from distutils.command.install import INSTALL_SCHEMES
-from distutils.core import setup
-
-def fullsplit(path, result=None):
- """
- Split a pathname into components (the opposite of os.path.join) in a
- platform-neutral way.
- """
- if result is None:
- result = []
- head, tail = os.path.split(path)
- if head == '':
- return [tail] + result
- if head == path:
- return result
- return fullsplit(head, [tail] + result)
-
-# Tell distutils to put the data_files in platform-specific installation
-# locations. See here for an explanation:
-# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
-for scheme in INSTALL_SCHEMES.values():
- scheme['data'] = scheme['purelib']
-
-# Compile the list of packages available, because distutils doesn't have
-# an easy way to do this.
-packages, data_files = [], []
-root_dir = os.path.dirname(__file__)
-tagging_dir = os.path.join(root_dir, 'tagging')
-pieces = fullsplit(root_dir)
-if pieces[-1] == '':
- len_root_dir = len(pieces) - 1
-else:
- len_root_dir = len(pieces)
-
-for dirpath, dirnames, filenames in os.walk(tagging_dir):
- # Ignore dirnames that start with '.'
- for i, dirname in enumerate(dirnames):
- if dirname.startswith('.'): del dirnames[i]
- if '__init__.py' in filenames:
- packages.append('.'.join(fullsplit(dirpath)[len_root_dir:]))
- elif filenames:
- data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
-
-setup(
- name = 'tagging',
- version = '0.2.1',
- description = 'Generic tagging application for Django',
- author = 'Jonathan Buchanan',
- author_email = 'jonathan.buchanan@gmail.com',
- url = 'http://code.google.com/p/django-tagging/',
- packages = packages,
- data_files = data_files,
- classifiers = ['Development Status :: 4 - Beta',
- 'Environment :: Web Environment',
- 'Framework :: Django',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: BSD License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Topic :: Utilities'],
-)
+"""
+Based entirely on Django's own ``setup.py``.
+"""
+import os
+from distutils.command.install import INSTALL_SCHEMES
+from distutils.core import setup
+
+def fullsplit(path, result=None):
+ """
+ Split a pathname into components (the opposite of os.path.join) in a
+ platform-neutral way.
+ """
+ if result is None:
+ result = []
+ head, tail = os.path.split(path)
+ if head == '':
+ return [tail] + result
+ if head == path:
+ return result
+ return fullsplit(head, [tail] + result)
+
+# Tell distutils to put the data_files in platform-specific installation
+# locations. See here for an explanation:
+# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
+for scheme in INSTALL_SCHEMES.values():
+ scheme['data'] = scheme['purelib']
+
+# Compile the list of packages available, because distutils doesn't have
+# an easy way to do this.
+packages, data_files = [], []
+root_dir = os.path.dirname(__file__)
+tagging_dir = os.path.join(root_dir, 'tagging')
+pieces = fullsplit(root_dir)
+if pieces[-1] == '':
+ len_root_dir = len(pieces) - 1
+else:
+ len_root_dir = len(pieces)
+
+for dirpath, dirnames, filenames in os.walk(tagging_dir):
+ # Ignore dirnames that start with '.'
+ for i, dirname in enumerate(dirnames):
+ if dirname.startswith('.'): del dirnames[i]
+ if '__init__.py' in filenames:
+ packages.append('.'.join(fullsplit(dirpath)[len_root_dir:]))
+ elif filenames:
+ data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
+
+# Dynamically calculate the version based on tagging.VERSION
+version_tuple = __import__('tagging').VERSION
+if version_tuple[2] is not None:
+ version = "%d.%d_%s" % version_tuple
+else:
+ version = "%d.%d" % version_tuple[:2]
+
+setup(
+ name = 'tagging',
+ version = version,
+ description = 'Generic tagging application for Django',
+ author = 'Jonathan Buchanan',
+ author_email = 'jonathan.buchanan@gmail.com',
+ url = 'http://code.google.com/p/django-tagging/',
+ packages = packages,
+ data_files = data_files,
+ classifiers = ['Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Utilities'],
+)
diff --git a/tagging/.svn/all-wcprops b/tagging/.svn/all-wcprops
new file mode 100644
index 0000000..fbf957a
--- /dev/null
+++ b/tagging/.svn/all-wcprops
@@ -0,0 +1,65 @@
+K 25
+svn:wc:ra_dav:version-url
+V 31
+/svn/!svn/ver/147/trunk/tagging
+END
+validators.py
+K 25
+svn:wc:ra_dav:version-url
+V 45
+/svn/!svn/ver/114/trunk/tagging/validators.py
+END
+generic.py
+K 25
+svn:wc:ra_dav:version-url
+V 42
+/svn/!svn/ver/138/trunk/tagging/generic.py
+END
+managers.py
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/!svn/ver/143/trunk/tagging/managers.py
+END
+views.py
+K 25
+svn:wc:ra_dav:version-url
+V 40
+/svn/!svn/ver/130/trunk/tagging/views.py
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/!svn/ver/130/trunk/tagging/__init__.py
+END
+utils.py
+K 25
+svn:wc:ra_dav:version-url
+V 40
+/svn/!svn/ver/147/trunk/tagging/utils.py
+END
+settings.py
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/!svn/ver/114/trunk/tagging/settings.py
+END
+models.py
+K 25
+svn:wc:ra_dav:version-url
+V 41
+/svn/!svn/ver/147/trunk/tagging/models.py
+END
+fields.py
+K 25
+svn:wc:ra_dav:version-url
+V 41
+/svn/!svn/ver/141/trunk/tagging/fields.py
+END
+forms.py
+K 25
+svn:wc:ra_dav:version-url
+V 40
+/svn/!svn/ver/141/trunk/tagging/forms.py
+END
diff --git a/tagging/.svn/dir-prop-base b/tagging/.svn/dir-prop-base
new file mode 100644
index 0000000..4cc643b
--- /dev/null
+++ b/tagging/.svn/dir-prop-base
@@ -0,0 +1,6 @@
+K 10
+svn:ignore
+V 6
+*.pyc
+
+END
diff --git a/tagging/.svn/entries b/tagging/.svn/entries
new file mode 100644
index 0000000..e226b2d
--- /dev/null
+++ b/tagging/.svn/entries
@@ -0,0 +1,374 @@
+9
+
+dir
+147
+http://django-tagging.googlecode.com/svn/trunk/tagging
+http://django-tagging.googlecode.com/svn
+
+
+
+2008-08-20T23:28:50.903432Z
+147
+doug.napoleone
+has-props
+
+svn:special svn:externals svn:needs-lock
+
+
+
+
+
+
+
+
+
+
+
+83e7428b-ec2a-0410-86f2-bf466d0e5e72
+
+validators.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+f94f883336937e8d8f9fb2dd0f943a54
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1126
+
+tests
+dir
+
+generic.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+18f6e0f760b377326ea81124dcf47db1
+2008-06-24T09:19:57.126367Z
+138
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1931
+
+managers.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+97fce4fc5814e3d5db5a9f89a6729bb4
+2008-08-15T05:59:12.272414Z
+143
+doug.napoleone
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+2332
+
+views.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+90a5836ed3edc70604f63fc26e09ab48
+2008-01-21T17:55:31.741058Z
+130
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+2118
+
+__init__.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+a52a98abdba2556f7eff853a2fae04a3
+2008-01-21T17:55:31.741058Z
+130
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+855
+
+utils.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+e7e102dc295758733e391e64ab58521d
+2008-08-20T23:28:50.903432Z
+147
+doug.napoleone
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+8630
+
+settings.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+0dd9381613e65273a53c05da4c901219
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+467
+
+templatetags
+dir
+
+models.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+24f36128b354a3a765b10ff2b14269d8
+2008-08-20T23:28:50.903432Z
+147
+doug.napoleone
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+20350
+
+fields.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+0496278c34f7f3130b0f1e4b7f6889ff
+2008-08-11T19:04:50.317273Z
+141
+doug.napoleone
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+3664
+
+forms.py
+file
+
+
+
+
+2008-08-21T18:05:41.000000Z
+f9725336959cda1e8861e8240b6e4cd5
+2008-08-11T19:04:50.317273Z
+141
+doug.napoleone
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+750
+
diff --git a/tagging/.svn/format b/tagging/.svn/format
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/tagging/.svn/format
@@ -0,0 +1 @@
+9
diff --git a/tagging/.svn/prop-base/__init__.py.svn-base b/tagging/.svn/prop-base/__init__.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/__init__.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/fields.py.svn-base b/tagging/.svn/prop-base/fields.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/fields.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/forms.py.svn-base b/tagging/.svn/prop-base/forms.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/forms.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/generic.py.svn-base b/tagging/.svn/prop-base/generic.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/generic.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/managers.py.svn-base b/tagging/.svn/prop-base/managers.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/managers.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/models.py.svn-base b/tagging/.svn/prop-base/models.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/models.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/settings.py.svn-base b/tagging/.svn/prop-base/settings.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/settings.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/utils.py.svn-base b/tagging/.svn/prop-base/utils.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/utils.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/validators.py.svn-base b/tagging/.svn/prop-base/validators.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/validators.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/prop-base/views.py.svn-base b/tagging/.svn/prop-base/views.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/.svn/prop-base/views.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/.svn/text-base/__init__.py.svn-base b/tagging/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..9241c20
--- /dev/null
+++ b/tagging/.svn/text-base/__init__.py.svn-base
@@ -0,0 +1,30 @@
+from django.utils.translation import ugettext as _
+
+from tagging.managers import ModelTaggedItemManager, TagDescriptor
+
+VERSION = (0, 3, 'pre')
+
+class AlreadyRegistered(Exception):
+ """
+ An attempt was made to register a model more than once.
+ """
+ pass
+
+registry = []
+
+def register(model, tag_descriptor_attr='tags',
+ tagged_item_manager_attr='tagged'):
+ """
+ Sets the given model class up for working with tags.
+ """
+ if model in registry:
+ raise AlreadyRegistered(
+ _('The model %s has already been registered.') % model.__name__)
+ registry.append(model)
+
+ # Add tag descriptor
+ setattr(model, tag_descriptor_attr, TagDescriptor())
+
+ # Add custom manager
+ ModelTaggedItemManager().contribute_to_class(model,
+ tagged_item_manager_attr)
diff --git a/tagging/.svn/text-base/fields.py.svn-base b/tagging/.svn/text-base/fields.py.svn-base
new file mode 100644
index 0000000..5b39e84
--- /dev/null
+++ b/tagging/.svn/text-base/fields.py.svn-base
@@ -0,0 +1,109 @@
+"""
+A custom Model Field for tagging.
+"""
+from django.db.models import signals
+from django.db.models.fields import CharField
+from django.utils.translation import ugettext_lazy as _
+
+from tagging import settings
+from tagging.models import Tag
+from tagging.utils import edit_string_for_tags
+from tagging.validators import isTagList
+
+class TagField(CharField):
+ """
+ A "special" character field that actually works as a relationship to tags
+ "under the hood". This exposes a space-separated string of tags, but does
+ the splitting/reordering/etc. under the hood.
+ """
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = kwargs.get('max_length', 255)
+ kwargs['blank'] = kwargs.get('blank', True)
+ kwargs['validator_list'] = [isTagList] + kwargs.get('validator_list', [])
+ super(TagField, self).__init__(*args, **kwargs)
+
+ def contribute_to_class(self, cls, name):
+ super(TagField, self).contribute_to_class(cls, name)
+
+ # Make this object the descriptor for field access.
+ setattr(cls, self.name, self)
+
+ # Save tags back to the database post-save
+ signals.post_save.connect(self._save, cls, True)
+
+ def __get__(self, instance, owner=None):
+ """
+ Tag getter. Returns an instance's tags if accessed on an instance, and
+ all of a model's tags if called on a class. That is, this model::
+
+ class Link(models.Model):
+ ...
+ tags = TagField()
+
+ Lets you do both of these::
+
+ >>> l = Link.objects.get(...)
+ >>> l.tags
+ 'tag1 tag2 tag3'
+
+ >>> Link.tags
+ 'tag1 tag2 tag3 tag4'
+
+ """
+ # Handle access on the model (i.e. Link.tags)
+ if instance is None:
+ return edit_string_for_tags(Tag.objects.usage_for_model(owner))
+
+ tags = self._get_instance_tag_cache(instance)
+ if tags is None:
+ if instance.pk is None:
+ self._set_instance_tag_cache(instance, '')
+ else:
+ self._set_instance_tag_cache(
+ instance, edit_string_for_tags(Tag.objects.get_for_object(instance)))
+ return self._get_instance_tag_cache(instance)
+
+ def __set__(self, instance, value):
+ """
+ Set an object's tags.
+ """
+ if instance is None:
+ raise AttributeError(_('%s can only be set on instances.') % self.name)
+ if settings.FORCE_LOWERCASE_TAGS and value is not None:
+ value = value.lower()
+ self._set_instance_tag_cache(instance, value)
+
+ def _save(self, **kwargs): #signal, sender, instance):
+ """
+ Save tags back to the database
+ """
+ tags = self._get_instance_tag_cache(kwargs['instance'])
+ if tags is not None:
+ Tag.objects.update_tags(kwargs['instance'], tags)
+
+ def __delete__(self, instance):
+ """
+ Clear all of an object's tags.
+ """
+ self._set_instance_tag_cache(instance, '')
+
+ def _get_instance_tag_cache(self, instance):
+ """
+ Helper: get an instance's tag cache.
+ """
+ return getattr(instance, '_%s_cache' % self.attname, None)
+
+ def _set_instance_tag_cache(self, instance, tags):
+ """
+ Helper: set an instance's tag cache.
+ """
+ setattr(instance, '_%s_cache' % self.attname, tags)
+
+ def get_internal_type(self):
+ return 'CharField'
+
+ def formfield(self, **kwargs):
+ from tagging import forms
+ defaults = {'form_class': forms.TagField}
+ defaults.update(kwargs)
+ return super(TagField, self).formfield(**defaults)
diff --git a/tagging/.svn/text-base/forms.py.svn-base b/tagging/.svn/text-base/forms.py.svn-base
new file mode 100644
index 0000000..e65aec4
--- /dev/null
+++ b/tagging/.svn/text-base/forms.py.svn-base
@@ -0,0 +1,23 @@
+"""
+Tagging components for Django's ``newforms`` form library.
+"""
+from django import forms
+from django.utils.translation import ugettext as _
+
+from tagging import settings
+from tagging.utils import parse_tag_input
+
+class TagField(forms.CharField):
+ """
+ A ``CharField`` which validates that its input is a valid list of
+ tag names.
+ """
+ def clean(self, value):
+ value = super(TagField, self).clean(value)
+ if value == u'':
+ return value
+ for tag_name in parse_tag_input(value):
+ if len(tag_name) > settings.MAX_TAG_LENGTH:
+ raise forms.ValidationError(
+ _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
+ return value
diff --git a/tagging/.svn/text-base/generic.py.svn-base b/tagging/.svn/text-base/generic.py.svn-base
new file mode 100644
index 0000000..75d1b8e
--- /dev/null
+++ b/tagging/.svn/text-base/generic.py.svn-base
@@ -0,0 +1,40 @@
+from django.contrib.contenttypes.models import ContentType
+
+def fetch_content_objects(tagged_items, select_related_for=None):
+ """
+ Retrieves ``ContentType`` and content objects for the given list of
+ ``TaggedItems``, grouping the retrieval of content objects by model
+ type to reduce the number of queries executed.
+
+ This results in ``number_of_content_types + 1`` queries rather than
+ the ``number_of_tagged_items * 2`` queries you'd get by iterating
+ over the list and accessing each item's ``object`` attribute.
+
+ A ``select_related_for`` argument can be used to specify a list of
+ of model names (corresponding to the ``model`` field of a
+ ``ContentType``) for which ``select_related`` should be used when
+ retrieving model instances.
+ """
+ if select_related_for is None: select_related_for = []
+
+ # Group content object pks by their content type pks
+ objects = {}
+ for item in tagged_items:
+ objects.setdefault(item.content_type_id, []).append(item.object_id)
+
+ # Retrieve content types and content objects in bulk
+ content_types = ContentType._default_manager.in_bulk(objects.keys())
+ for content_type_pk, object_pks in objects.iteritems():
+ model = content_types[content_type_pk].model_class()
+ if content_types[content_type_pk].model in select_related_for:
+ objects[content_type_pk] = model._default_manager.select_related().in_bulk(object_pks)
+ else:
+ objects[content_type_pk] = model._default_manager.in_bulk(object_pks)
+
+ # Set content types and content objects in the appropriate cache
+ # attributes, so accessing the 'content_type' and 'object'
+ # attributes on each tagged item won't result in further database
+ # hits.
+ for item in tagged_items:
+ item._object_cache = objects[item.content_type_id][item.object_id]
+ item._content_type_cache = content_types[item.content_type_id]
diff --git a/tagging/.svn/text-base/managers.py.svn-base b/tagging/.svn/text-base/managers.py.svn-base
new file mode 100644
index 0000000..51e2473
--- /dev/null
+++ b/tagging/.svn/text-base/managers.py.svn-base
@@ -0,0 +1,68 @@
+"""
+Custom managers for Django models registered with the tagging
+application.
+"""
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+from tagging.models import Tag, TaggedItem
+
+class ModelTagManager(models.Manager):
+ """
+ A manager for retrieving tags for a particular model.
+ """
+ def get_query_set(self):
+ ctype = ContentType.objects.get_for_model(self.model)
+ return Tag.objects.filter(
+ items__content_type__pk=ctype.pk).distinct()
+
+ def cloud(self, *args, **kwargs):
+ return Tag.objects.cloud_for_model(self.model, *args, **kwargs)
+
+ def related(self, tags, *args, **kwargs):
+ return Tag.objects.related_for_model(tags, self.model, *args, **kwargs)
+
+ def usage(self, *args, **kwargs):
+ return Tag.objects.usage_for_model(self.model, *arg, **kwargs)
+
+class ModelTaggedItemManager(models.Manager):
+ """
+ A manager for retrieving model instances based on their tags.
+ """
+ def related_to(self, obj, queryset=None, num=None):
+ if queryset is None:
+ return TaggedItem.objects.get_related(obj, self.model, num=num)
+ else:
+ return TaggedItem.objects.get_related(obj, queryset, num=num)
+
+ def with_all(self, tags, queryset=None):
+ if queryset is None:
+ return TaggedItem.objects.get_by_model(self.model, tags)
+ else:
+ return TaggedItem.objects.get_by_model(queryset, tags)
+
+ def with_any(self, tags, queryset=None):
+ if queryset is None:
+ return TaggedItem.objects.get_union_by_model(self.model, tags)
+ else:
+ return TaggedItem.objects.get_union_by_model(queryset, tags)
+
+class TagDescriptor(object):
+ """
+ A descriptor which provides access to a ``ModelTagManager`` for
+ model classes and simple retrieval, updating and deletion of tags
+ for model instances.
+ """
+ def __get__(self, instance, owner):
+ if not instance:
+ tag_manager = ModelTagManager()
+ tag_manager.model = owner
+ return tag_manager
+ else:
+ return Tag.objects.get_for_object(instance)
+
+ def __set__(self, instance, value):
+ Tag.objects.update_tags(instance, value)
+
+ def __delete__(self, instance):
+ Tag.objects.update_tags(instance, None)
diff --git a/tagging/.svn/text-base/models.py.svn-base b/tagging/.svn/text-base/models.py.svn-base
new file mode 100644
index 0000000..7463ddb
--- /dev/null
+++ b/tagging/.svn/text-base/models.py.svn-base
@@ -0,0 +1,502 @@
+"""
+Models and managers for generic tagging.
+"""
+# Python 2.3 compatibility
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
+from django.db.models.query import QuerySet
+from django.utils.translation import ugettext_lazy as _
+
+from tagging import settings
+from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input
+from tagging.utils import LOGARITHMIC
+from tagging.validators import isTag
+
+qn = connection.ops.quote_name
+
+try:
+ from django.db.models.query import parse_lookup
+except ImportError:
+ parse_lookup = None
+
+############
+# Managers #
+############
+
+class TagManager(models.Manager):
+ def update_tags(self, obj, tag_names):
+ """
+ Update tags associated with an object.
+ """
+ ctype = ContentType.objects.get_for_model(obj)
+ current_tags = list(self.filter(items__content_type__pk=ctype.pk,
+ items__object_id=obj.pk))
+ updated_tag_names = parse_tag_input(tag_names)
+ if settings.FORCE_LOWERCASE_TAGS:
+ updated_tag_names = [t.lower() for t in updated_tag_names]
+
+ # Remove tags which no longer apply
+ tags_for_removal = [tag for tag in current_tags \
+ if tag.name not in updated_tag_names]
+ if len(tags_for_removal):
+ TaggedItem._default_manager.filter(content_type__pk=ctype.pk,
+ object_id=obj.pk,
+ tag__in=tags_for_removal).delete()
+ # Add new tags
+ current_tag_names = [tag.name for tag in current_tags]
+ for tag_name in updated_tag_names:
+ if tag_name not in current_tag_names:
+ tag, created = self.get_or_create(name=tag_name)
+ TaggedItem._default_manager.create(tag=tag, object=obj)
+
+ def add_tag(self, obj, tag_name):
+ """
+ Associates the given object with a tag.
+ """
+ tag_names = parse_tag_input(tag_name)
+ if not len(tag_names):
+ raise AttributeError(_('No tags were given: "%s".') % tag_name)
+ if len(tag_names) > 1:
+ raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
+ tag_name = tag_names[0]
+ if settings.FORCE_LOWERCASE_TAGS:
+ tag_name = tag_name.lower()
+ tag, created = self.get_or_create(name=tag_name)
+ ctype = ContentType.objects.get_for_model(obj)
+ TaggedItem._default_manager.get_or_create(
+ tag=tag, content_type=ctype, object_id=obj.pk)
+
+ def get_for_object(self, obj):
+ """
+ Create a queryset matching all tags associated with the given
+ object.
+ """
+ ctype = ContentType.objects.get_for_model(obj)
+ return self.filter(items__content_type__pk=ctype.pk,
+ items__object_id=obj.pk)
+
+ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
+ """
+ Perform the custom SQL query for ``usage_for_model`` and
+ ``usage_for_queryset``.
+ """
+ if min_count is not None: counts = True
+
+ model_table = qn(model._meta.db_table)
+ model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
+ query = """
+ SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
+ FROM
+ %(tag)s
+ INNER JOIN %(tagged_item)s
+ ON %(tag)s.id = %(tagged_item)s.tag_id
+ INNER JOIN %(model)s
+ ON %(tagged_item)s.object_id = %(model_pk)s
+ %%s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ %%s
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %%s
+ ORDER BY %(tag)s.name ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
+ 'tagged_item': qn(TaggedItem._meta.db_table),
+ 'model': model_table,
+ 'model_pk': model_pk,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ }
+
+ min_count_sql = ''
+ if min_count is not None:
+ min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
+ tags = []
+ for row in cursor.fetchall():
+ t = self.model(*row[:2])
+ if counts:
+ t.count = row[2]
+ tags.append(t)
+ return tags
+
+ def usage_for_model(self, model, counts=False, min_count=None, filters=None):
+ """
+ Obtain a list of tags associated with instances of the given
+ Model class.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+
+ To limit the tags (and counts, if specified) returned to those
+ used by a subset of the Model's instances, pass a dictionary
+ of field lookups to be applied to the given Model as the
+ ``filters`` argument.
+ """
+ if filters is None: filters = {}
+
+ if not parse_lookup:
+ # post-queryset-refactor (hand off to usage_for_queryset)
+ queryset = model._default_manager.filter()
+ for f in filters.items():
+ queryset.query.add_filter(f)
+ usage = self.usage_for_queryset(queryset, counts, min_count)
+ else:
+ # pre-queryset-refactor
+ extra_joins = ''
+ extra_criteria = ''
+ params = []
+ if len(filters) > 0:
+ joins, where, params = parse_lookup(filters.items(), model._meta)
+ extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
+ for (alias, (table, join_type, condition)) in joins.items()])
+ extra_criteria = 'AND %s' % (' AND '.join(where))
+ usage = self._get_usage(model, counts, min_count, extra_joins, extra_criteria, params)
+
+ return usage
+
+ def usage_for_queryset(self, queryset, counts=False, min_count=None):
+ """
+ Obtain a list of tags associated with instances of a model
+ contained in the given queryset.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if parse_lookup:
+ raise AttributeError("'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.")
+
+ extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
+ where, params = queryset.query.where.as_sql()
+ if where:
+ extra_criteria = 'AND %s' % where
+ else:
+ extra_criteria = ''
+ return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
+
+ def related_for_model(self, tags, model, counts=False, min_count=None):
+ """
+ Obtain a list of tags related to a given list of tags - that
+ is, other tags used by items which have all the given tags.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating the number of items which have it in
+ addition to the given list of tags.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if min_count is not None: counts = True
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ tagged_item_table = qn(TaggedItem._meta.db_table)
+ query = """
+ SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
+ FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.object_id IN
+ (
+ SELECT %(tagged_item)s.object_id
+ FROM %(tagged_item)s, %(tag)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND %(tag)s.id IN (%(tag_id_placeholders)s)
+ GROUP BY %(tagged_item)s.object_id
+ HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
+ )
+ AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %(min_count_sql)s
+ ORDER BY %(tag)s.name ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
+ 'tagged_item': tagged_item_table,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'tag_count': tag_count,
+ 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
+ }
+
+ params = [tag.pk for tag in tags] * 2
+ if min_count is not None:
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query, params)
+ related = []
+ for row in cursor.fetchall():
+ tag = self.model(*row[:2])
+ if counts is True:
+ tag.count = row[2]
+ related.append(tag)
+ return related
+
+ def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
+ filters=None, min_count=None):
+ """
+ Obtain a list of tags associated with instances of the given
+ Model, giving each tag a ``count`` attribute indicating how
+ many times it has been used and a ``font_size`` attribute for
+ use in displaying a tag cloud.
+
+ ``steps`` defines the range of font sizes - ``font_size`` will
+ be an integer between 1 and ``steps`` (inclusive).
+
+ ``distribution`` defines the type of font size distribution
+ algorithm which will be used - logarithmic or linear. It must
+ be either ``tagging.utils.LOGARITHMIC`` or
+ ``tagging.utils.LINEAR``.
+
+ To limit the tags displayed in the cloud to those associated
+ with a subset of the Model's instances, pass a dictionary of
+ field lookups to be applied to the given Model as the
+ ``filters`` argument.
+
+ To limit the tags displayed in the cloud to those with a
+ ``count`` greater than or equal to ``min_count``, pass a value
+ for the ``min_count`` argument.
+ """
+ tags = list(self.usage_for_model(model, counts=True, filters=filters,
+ min_count=min_count))
+ return calculate_cloud(tags, steps, distribution)
+
+class TaggedItemManager(models.Manager):
+ """
+ FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
+ SQL clauses required by many of this manager's methods into
+ Django's ORM.
+
+ For now, we manually execute a query to retrieve the PKs of
+ objects we're interested in, then use the ORM's ``__in``
+ lookup to return a ``QuerySet``.
+
+ Once the queryset-refactor branch lands in trunk, this can be
+ tidied up significantly.
+ """
+ def get_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with a given tag or list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ if tag_count == 0:
+ # No existing tags were given
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ return model._default_manager.none()
+ elif tag_count == 1:
+ # Optimisation for single tag - fall through to the simpler
+ # query below.
+ tag = tags[0]
+ else:
+ return self.get_intersection_by_model(queryset_or_model, tags)
+
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ content_type = ContentType.objects.get_for_model(model)
+ opts = self.model._meta
+ tagged_item_table = qn(opts.db_table)
+ return queryset.extra(
+ tables=[opts.db_table],
+ where=[
+ '%s.content_type_id = %%s' % tagged_item_table,
+ '%s.tag_id = %%s' % tagged_item_table,
+ '%s.%s = %s.object_id' % (qn(model._meta.db_table),
+ qn(model._meta.pk.column),
+ tagged_item_table)
+ ],
+ params=[content_type.pk, tag.pk],
+ )
+
+ def get_intersection_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *all* of the given list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have all the
+ # given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s
+ HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'tag_count': tag_count,
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_union_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *any* of the given list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have any of
+ # the given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_related(self, obj, queryset_or_model, num=None):
+ """
+ Retrieve a list of instances of the specified model which share
+ tags with the model instance ``obj``, ordered by the number of
+ shared tags in descending order.
+
+ If ``num`` is given, a maximum of ``num`` instances will be
+ returned.
+ """
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ model_table = qn(model._meta.db_table)
+ content_type = ContentType.objects.get_for_model(obj)
+ related_content_type = ContentType.objects.get_for_model(model)
+ query = """
+ SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
+ FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
+ WHERE %(tagged_item)s.object_id = %%s
+ AND %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND related_tagged_item.content_type_id = %(related_content_type_id)s
+ AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
+ AND %(model_pk)s = related_tagged_item.object_id"""
+ if content_type.pk == related_content_type.pk:
+ # Exclude the given instance itself if determining related
+ # instances for the same model.
+ query += """
+ AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
+ query += """
+ GROUP BY %(model_pk)s
+ ORDER BY %(count)s DESC
+ %(limit_offset)s"""
+ query = query % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'count': qn('count'),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
+ 'content_type_id': content_type.pk,
+ 'related_content_type_id': related_content_type.pk,
+ 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [obj.pk])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ # Use in_bulk here instead of an id__in lookup, because id__in would
+ # clobber the ordering.
+ object_dict = queryset.in_bulk(object_ids)
+ return [object_dict[object_id] for object_id in object_ids \
+ if object_id in object_dict]
+ else:
+ return []
+
+##########
+# Models #
+##########
+
+class Tag(models.Model):
+ """
+ A tag.
+ """
+ name = models.CharField(_('name'), max_length=50, unique=True, db_index=True, validator_list=[isTag])
+
+ objects = TagManager()
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('tag')
+ verbose_name_plural = _('tags')
+
+ class Admin:
+ pass
+
+ def __unicode__(self):
+ return self.name
+
+class TaggedItem(models.Model):
+ """
+ Holds the relationship between a tag and the item being tagged.
+ """
+ tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
+ content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
+ object_id = models.PositiveIntegerField(_('object id'), db_index=True)
+ object = generic.GenericForeignKey('content_type', 'object_id')
+
+ objects = TaggedItemManager()
+
+ class Meta:
+ # Enforce unique tag association per object
+ unique_together = (('tag', 'content_type', 'object_id'),)
+ verbose_name = _('tagged item')
+ verbose_name_plural = _('tagged items')
+
+ class Admin:
+ pass
+
+ def __unicode__(self):
+ return u'%s [%s]' % (self.object, self.tag)
diff --git a/tagging/.svn/text-base/settings.py.svn-base b/tagging/.svn/text-base/settings.py.svn-base
new file mode 100644
index 0000000..1d6224c
--- /dev/null
+++ b/tagging/.svn/text-base/settings.py.svn-base
@@ -0,0 +1,13 @@
+"""
+Convenience module for access of custom tagging application settings,
+which enforces default settings when the main settings module does not
+contain the appropriate settings.
+"""
+from django.conf import settings
+
+# The maximum length of a tag's name.
+MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50)
+
+# Whether to force all tags to lowercase before they are saved to the
+# database.
+FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False)
diff --git a/tagging/.svn/text-base/utils.py.svn-base b/tagging/.svn/text-base/utils.py.svn-base
new file mode 100644
index 0000000..e89bab0
--- /dev/null
+++ b/tagging/.svn/text-base/utils.py.svn-base
@@ -0,0 +1,263 @@
+"""
+Tagging utilities - from user tag input parsing to tag cloud
+calculation.
+"""
+import math
+import types
+
+from django.db.models.query import QuerySet
+from django.utils.encoding import force_unicode
+from django.utils.translation import ugettext as _
+
+# Python 2.3 compatibility
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+def parse_tag_input(input):
+ """
+ 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.
+ """
+ if not input:
+ return []
+
+ input = force_unicode(input)
+
+ # Special case - if there are no commas or double quotes in the
+ # input, we don't *do* a recall... I mean, we know we only need to
+ # split on spaces.
+ if u',' not in input and u'"' not in input:
+ words = list(set(split_strip(input, u' ')))
+ 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(input)
+ try:
+ while 1:
+ 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(input, delimiter=u','):
+ """
+ Splits ``input`` on ``delimiter``, stripping each resulting string
+ and returning a list of non-empty strings.
+ """
+ if not input:
+ return []
+
+ words = [w.strip() for w in input.split(delimiter)]
+ return [w for w in words if w]
+
+def edit_string_for_tags(tags):
+ """
+ Given list of ``Tag`` instances, creates a string representation of
+ 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.
+ """
+ names = []
+ use_commas = False
+ for tag in tags:
+ name = tag.name
+ if u',' in name:
+ names.append('"%s"' % name)
+ continue
+ elif u' ' in name:
+ if not use_commas:
+ use_commas = True
+ names.append(name)
+ if use_commas:
+ glue = u', '
+ else:
+ glue = u' '
+ return glue.join(names)
+
+def get_queryset_and_model(queryset_or_model):
+ """
+ Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
+ (queryset, model).
+
+ If a ``Model`` is given, the ``QuerySet`` returned will be created
+ using its default manager.
+ """
+ try:
+ return queryset_or_model, queryset_or_model.model
+ except AttributeError:
+ return queryset_or_model._default_manager.all(), queryset_or_model
+
+def get_tag_list(tags):
+ """
+ Utility function for accepting tag input in a flexible manner.
+
+ If a ``Tag`` object is given, it will be returned in a list as
+ its single occupant.
+
+ If given, the tag names in the following will be used to create a
+ ``Tag`` ``QuerySet``:
+
+ * A string, which may contain multiple tag names.
+ * A list or tuple of strings corresponding to tag names.
+ * A list or tuple of integers corresponding to tag ids.
+
+ If given, the following will be returned as-is:
+
+ * A list or tuple of ``Tag`` objects.
+ * A ``Tag`` ``QuerySet``.
+
+ """
+ from tagging.models import Tag
+ if isinstance(tags, Tag):
+ return [tags]
+ elif isinstance(tags, QuerySet) and tags.model is Tag:
+ return tags
+ elif isinstance(tags, types.StringTypes):
+ return Tag.objects.filter(name__in=parse_tag_input(tags))
+ elif isinstance(tags, (types.ListType, types.TupleType)):
+ if len(tags) == 0:
+ return tags
+ contents = set()
+ for item in tags:
+ if isinstance(item, types.StringTypes):
+ contents.add('string')
+ elif isinstance(item, Tag):
+ contents.add('tag')
+ elif isinstance(item, (types.IntType, types.LongType)):
+ contents.add('int')
+ if len(contents) == 1:
+ if 'string' in contents:
+ return Tag.objects.filter(name__in=[force_unicode(tag) \
+ for tag in tags])
+ elif 'tag' in contents:
+ return tags
+ elif 'int' in contents:
+ return Tag.objects.filter(id__in=tags)
+ else:
+ raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.'))
+ else:
+ raise ValueError(_('The tag input given was invalid.'))
+
+def get_tag(tag):
+ """
+ Utility function for accepting single tag input in a flexible
+ manner.
+
+ If a ``Tag`` object is given it will be returned as-is; if a
+ string or integer are given, they will be used to lookup the
+ appropriate ``Tag``.
+
+ If no matching tag can be found, ``None`` will be returned.
+ """
+ from tagging.models import Tag
+ if isinstance(tag, Tag):
+ return tag
+
+ try:
+ if isinstance(tag, types.StringTypes):
+ return Tag.objects.get(name=tag)
+ elif isinstance(tag, (types.IntType, types.LongType)):
+ return Tag.objects.get(id=tag)
+ except Tag.DoesNotExist:
+ pass
+
+ return None
+
+# Font size distribution algorithms
+LOGARITHMIC, LINEAR = 1, 2
+
+def _calculate_thresholds(min_weight, max_weight, steps):
+ delta = (max_weight - min_weight) / float(steps)
+ return [min_weight + i * delta for i in range(1, steps + 1)]
+
+def _calculate_tag_weight(weight, max_weight, distribution):
+ """
+ Logarithmic tag weight calculation is based on code from the
+ `Tag Cloud`_ plugin for Mephisto, by Sven Fuchs.
+
+ .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud
+ """
+ if distribution == LINEAR or max_weight == 1:
+ return weight
+ elif distribution == LOGARITHMIC:
+ return math.log(weight) * max_weight / math.log(max_weight)
+ raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution)
+
+def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC):
+ """
+ Add a ``font_size`` attribute to each tag according to the
+ frequency of its use, as indicated by its ``count``
+ attribute.
+
+ ``steps`` defines the range of font sizes - ``font_size`` will
+ be an integer between 1 and ``steps`` (inclusive).
+
+ ``distribution`` defines the type of font size distribution
+ algorithm which will be used - logarithmic or linear. It must be
+ one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+ """
+ if len(tags) > 0:
+ counts = [tag.count for tag in tags]
+ min_weight = float(min(counts))
+ max_weight = float(max(counts))
+ thresholds = _calculate_thresholds(min_weight, max_weight, steps)
+ for tag in tags:
+ font_set = False
+ tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution)
+ for i in range(steps):
+ if not font_set and tag_weight <= thresholds[i]:
+ tag.font_size = i + 1
+ font_set = True
+ return tags
diff --git a/tagging/.svn/text-base/validators.py.svn-base b/tagging/.svn/text-base/validators.py.svn-base
new file mode 100644
index 0000000..e902237
--- /dev/null
+++ b/tagging/.svn/text-base/validators.py.svn-base
@@ -0,0 +1,30 @@
+"""
+Oldforms validators for tagging related fields - these are still
+required for basic ``django.contrib.admin`` application field validation
+until the ``newforms-admin`` branch lands in trunk.
+"""
+from django.core.validators import ValidationError
+from django.utils.translation import ugettext as _
+
+from tagging import settings
+from tagging.utils import parse_tag_input
+
+def isTagList(field_data, all_data):
+ """
+ Validates that ``field_data`` is a valid list of tags.
+ """
+ for tag_name in parse_tag_input(field_data):
+ if len(tag_name) > settings.MAX_TAG_LENGTH:
+ raise ValidationError(
+ _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
+
+def isTag(field_data, all_data):
+ """
+ Validates that ``field_data`` is a valid tag.
+ """
+ tag_names = parse_tag_input(field_data)
+ if len(tag_names) > 1:
+ raise ValidationError(_('Multiple tags were given.'))
+ elif len(tag_names[0]) > settings.MAX_TAG_LENGTH:
+ raise ValidationError(
+ _('A tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
diff --git a/tagging/.svn/text-base/views.py.svn-base b/tagging/.svn/text-base/views.py.svn-base
new file mode 100644
index 0000000..9e7e2f5
--- /dev/null
+++ b/tagging/.svn/text-base/views.py.svn-base
@@ -0,0 +1,52 @@
+"""
+Tagging related views.
+"""
+from django.http import Http404
+from django.utils.translation import ugettext as _
+from django.views.generic.list_detail import object_list
+
+from tagging.models import Tag, TaggedItem
+from tagging.utils import get_tag, get_queryset_and_model
+
+def tagged_object_list(request, queryset_or_model=None, tag=None,
+ related_tags=False, related_tag_counts=True, **kwargs):
+ """
+ A thin wrapper around
+ ``django.views.generic.list_detail.object_list`` which creates a
+ ``QuerySet`` containing instances of the given queryset or model
+ tagged with the given tag.
+
+ In addition to the context variables set up by ``object_list``, a
+ ``tag`` context variable will contain the ``Tag`` instance for the
+ tag.
+
+ If ``related_tags`` is ``True``, a ``related_tags`` context variable
+ will contain tags related to the given tag for the given model.
+ Additionally, if ``related_tag_counts`` is ``True``, each related
+ tag will have a ``count`` attribute indicating the number of items
+ which have it in addition to the given tag.
+ """
+ if queryset_or_model is None:
+ try:
+ queryset_or_model = kwargs.pop('queryset_or_model')
+ except KeyError:
+ raise AttributeError(_('tagged_object_list must be called with a queryset or a model.'))
+
+ if tag is None:
+ try:
+ tag = kwargs.pop('tag')
+ except KeyError:
+ raise AttributeError(_('tagged_object_list must be called with a tag.'))
+
+ tag_instance = get_tag(tag)
+ if tag_instance is None:
+ raise Http404(_('No Tag found matching "%s".') % tag)
+ queryset = TaggedItem.objects.get_by_model(queryset_or_model, tag_instance)
+ if not kwargs.has_key('extra_context'):
+ kwargs['extra_context'] = {}
+ kwargs['extra_context']['tag'] = tag_instance
+ if related_tags:
+ kwargs['extra_context']['related_tags'] = \
+ Tag.objects.related_for_model(tag_instance, queryset_or_model,
+ counts=related_tag_counts)
+ return object_list(request, queryset, **kwargs)
diff --git a/tagging/__init__.py b/tagging/__init__.py
index 28ed501..9241c20 100644
--- a/tagging/__init__.py
+++ b/tagging/__init__.py
@@ -1 +1,30 @@
-VERSION = (0, 2.1, None)
+from django.utils.translation import ugettext as _
+
+from tagging.managers import ModelTaggedItemManager, TagDescriptor
+
+VERSION = (0, 3, 'pre')
+
+class AlreadyRegistered(Exception):
+ """
+ An attempt was made to register a model more than once.
+ """
+ pass
+
+registry = []
+
+def register(model, tag_descriptor_attr='tags',
+ tagged_item_manager_attr='tagged'):
+ """
+ Sets the given model class up for working with tags.
+ """
+ if model in registry:
+ raise AlreadyRegistered(
+ _('The model %s has already been registered.') % model.__name__)
+ registry.append(model)
+
+ # Add tag descriptor
+ setattr(model, tag_descriptor_attr, TagDescriptor())
+
+ # Add custom manager
+ ModelTaggedItemManager().contribute_to_class(model,
+ tagged_item_manager_attr)
diff --git a/tagging/fields.py b/tagging/fields.py
index c9475f6..5b39e84 100644
--- a/tagging/fields.py
+++ b/tagging/fields.py
@@ -1,110 +1,109 @@
-"""
-A custom Model Field for tagging.
-"""
-from django.db.models import signals
-from django.db.models.fields import CharField
-from django.dispatch import dispatcher
-from django.utils.translation import ugettext_lazy as _
-
-from tagging import settings
-from tagging.models import Tag
-from tagging.utils import edit_string_for_tags
-from tagging.validators import isTagList
-
-class TagField(CharField):
- """
- A "special" character field that actually works as a relationship to tags
- "under the hood". This exposes a space-separated string of tags, but does
- the splitting/reordering/etc. under the hood.
- """
- def __init__(self, **kwargs):
- kwargs['max_length'] = kwargs.get('max_length', 255)
- kwargs['blank'] = kwargs.get('blank', True)
- kwargs['validator_list'] = [isTagList] + kwargs.get('validator_list', [])
- super(TagField, self).__init__(**kwargs)
-
- def contribute_to_class(self, cls, name):
- super(TagField, self).contribute_to_class(cls, name)
-
- # Make this object the descriptor for field access.
- setattr(cls, self.name, self)
-
- # Save tags back to the database post-save
- dispatcher.connect(self._save, signal=signals.post_save, sender=cls)
-
- def __get__(self, instance, owner=None):
- """
- Tag getter. Returns an instance's tags if accessed on an instance, and
- all of a model's tags if called on a class. That is, this model::
-
- class Link(models.Model):
- ...
- tags = TagField()
-
- Lets you do both of these::
-
- >>> l = Link.objects.get(...)
- >>> l.tags
- 'tag1 tag2 tag3'
-
- >>> Link.tags
- 'tag1 tag2 tag3 tag4'
-
- """
- # Handle access on the model (i.e. Link.tags)
- if instance is None:
- return edit_string_for_tags(Tag.objects.usage_for_model(owner))
-
- tags = self._get_instance_tag_cache(instance)
- if tags is None:
- if instance.pk is None:
- self._set_instance_tag_cache(instance, '')
- else:
- self._set_instance_tag_cache(
- instance, edit_string_for_tags(Tag.objects.get_for_object(instance)))
- return self._get_instance_tag_cache(instance)
-
- def __set__(self, instance, value):
- """
- Set an object's tags.
- """
- if instance is None:
- raise AttributeError(_('%s can only be set on instances.') % self.name)
- if settings.FORCE_LOWERCASE_TAGS and value is not None:
- value = value.lower()
- self._set_instance_tag_cache(instance, value)
-
- def _save(self, signal, sender, instance):
- """
- Save tags back to the database
- """
- tags = self._get_instance_tag_cache(instance)
- if tags is not None:
- Tag.objects.update_tags(instance, tags)
-
- def __delete__(self, instance):
- """
- Clear all of an object's tags.
- """
- self._set_instance_tag_cache(instance, '')
-
- def _get_instance_tag_cache(self, instance):
- """
- Helper: get an instance's tag cache.
- """
- return getattr(instance, '_%s_cache' % self.attname, None)
-
- def _set_instance_tag_cache(self, instance, tags):
- """
- Helper: set an instance's tag cache.
- """
- setattr(instance, '_%s_cache' % self.attname, tags)
-
- def get_internal_type(self):
- return 'CharField'
-
- def formfield(self, **kwargs):
- from tagging import forms
- defaults = {'form_class': forms.TagField}
- defaults.update(kwargs)
- return super(TagField, self).formfield(**defaults)
+"""
+A custom Model Field for tagging.
+"""
+from django.db.models import signals
+from django.db.models.fields import CharField
+from django.utils.translation import ugettext_lazy as _
+
+from tagging import settings
+from tagging.models import Tag
+from tagging.utils import edit_string_for_tags
+from tagging.validators import isTagList
+
+class TagField(CharField):
+ """
+ A "special" character field that actually works as a relationship to tags
+ "under the hood". This exposes a space-separated string of tags, but does
+ the splitting/reordering/etc. under the hood.
+ """
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = kwargs.get('max_length', 255)
+ kwargs['blank'] = kwargs.get('blank', True)
+ kwargs['validator_list'] = [isTagList] + kwargs.get('validator_list', [])
+ super(TagField, self).__init__(*args, **kwargs)
+
+ def contribute_to_class(self, cls, name):
+ super(TagField, self).contribute_to_class(cls, name)
+
+ # Make this object the descriptor for field access.
+ setattr(cls, self.name, self)
+
+ # Save tags back to the database post-save
+ signals.post_save.connect(self._save, cls, True)
+
+ def __get__(self, instance, owner=None):
+ """
+ Tag getter. Returns an instance's tags if accessed on an instance, and
+ all of a model's tags if called on a class. That is, this model::
+
+ class Link(models.Model):
+ ...
+ tags = TagField()
+
+ Lets you do both of these::
+
+ >>> l = Link.objects.get(...)
+ >>> l.tags
+ 'tag1 tag2 tag3'
+
+ >>> Link.tags
+ 'tag1 tag2 tag3 tag4'
+
+ """
+ # Handle access on the model (i.e. Link.tags)
+ if instance is None:
+ return edit_string_for_tags(Tag.objects.usage_for_model(owner))
+
+ tags = self._get_instance_tag_cache(instance)
+ if tags is None:
+ if instance.pk is None:
+ self._set_instance_tag_cache(instance, '')
+ else:
+ self._set_instance_tag_cache(
+ instance, edit_string_for_tags(Tag.objects.get_for_object(instance)))
+ return self._get_instance_tag_cache(instance)
+
+ def __set__(self, instance, value):
+ """
+ Set an object's tags.
+ """
+ if instance is None:
+ raise AttributeError(_('%s can only be set on instances.') % self.name)
+ if settings.FORCE_LOWERCASE_TAGS and value is not None:
+ value = value.lower()
+ self._set_instance_tag_cache(instance, value)
+
+ def _save(self, **kwargs): #signal, sender, instance):
+ """
+ Save tags back to the database
+ """
+ tags = self._get_instance_tag_cache(kwargs['instance'])
+ if tags is not None:
+ Tag.objects.update_tags(kwargs['instance'], tags)
+
+ def __delete__(self, instance):
+ """
+ Clear all of an object's tags.
+ """
+ self._set_instance_tag_cache(instance, '')
+
+ def _get_instance_tag_cache(self, instance):
+ """
+ Helper: get an instance's tag cache.
+ """
+ return getattr(instance, '_%s_cache' % self.attname, None)
+
+ def _set_instance_tag_cache(self, instance, tags):
+ """
+ Helper: set an instance's tag cache.
+ """
+ setattr(instance, '_%s_cache' % self.attname, tags)
+
+ def get_internal_type(self):
+ return 'CharField'
+
+ def formfield(self, **kwargs):
+ from tagging import forms
+ defaults = {'form_class': forms.TagField}
+ defaults.update(kwargs)
+ return super(TagField, self).formfield(**defaults)
diff --git a/tagging/forms.py b/tagging/forms.py
index 6844039..e65aec4 100644
--- a/tagging/forms.py
+++ b/tagging/forms.py
@@ -1,23 +1,23 @@
-"""
-Tagging components for Django's ``newforms`` form library.
-"""
-from django import newforms as forms
-from django.utils.translation import ugettext as _
-
-from tagging import settings
-from tagging.utils import parse_tag_input
-
-class TagField(forms.CharField):
- """
- A ``CharField`` which validates that its input is a valid list of
- tag names.
- """
- def clean(self, value):
- value = super(TagField, self).clean(value)
- if value == u'':
- return value
- for tag_name in parse_tag_input(value):
- if len(tag_name) > settings.MAX_TAG_LENGTH:
- raise forms.ValidationError(
- _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
- return value
+"""
+Tagging components for Django's ``newforms`` form library.
+"""
+from django import forms
+from django.utils.translation import ugettext as _
+
+from tagging import settings
+from tagging.utils import parse_tag_input
+
+class TagField(forms.CharField):
+ """
+ A ``CharField`` which validates that its input is a valid list of
+ tag names.
+ """
+ def clean(self, value):
+ value = super(TagField, self).clean(value)
+ if value == u'':
+ return value
+ for tag_name in parse_tag_input(value):
+ if len(tag_name) > settings.MAX_TAG_LENGTH:
+ raise forms.ValidationError(
+ _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
+ return value
diff --git a/tagging/generic.py b/tagging/generic.py
new file mode 100644
index 0000000..75d1b8e
--- /dev/null
+++ b/tagging/generic.py
@@ -0,0 +1,40 @@
+from django.contrib.contenttypes.models import ContentType
+
+def fetch_content_objects(tagged_items, select_related_for=None):
+ """
+ Retrieves ``ContentType`` and content objects for the given list of
+ ``TaggedItems``, grouping the retrieval of content objects by model
+ type to reduce the number of queries executed.
+
+ This results in ``number_of_content_types + 1`` queries rather than
+ the ``number_of_tagged_items * 2`` queries you'd get by iterating
+ over the list and accessing each item's ``object`` attribute.
+
+ A ``select_related_for`` argument can be used to specify a list of
+ of model names (corresponding to the ``model`` field of a
+ ``ContentType``) for which ``select_related`` should be used when
+ retrieving model instances.
+ """
+ if select_related_for is None: select_related_for = []
+
+ # Group content object pks by their content type pks
+ objects = {}
+ for item in tagged_items:
+ objects.setdefault(item.content_type_id, []).append(item.object_id)
+
+ # Retrieve content types and content objects in bulk
+ content_types = ContentType._default_manager.in_bulk(objects.keys())
+ for content_type_pk, object_pks in objects.iteritems():
+ model = content_types[content_type_pk].model_class()
+ if content_types[content_type_pk].model in select_related_for:
+ objects[content_type_pk] = model._default_manager.select_related().in_bulk(object_pks)
+ else:
+ objects[content_type_pk] = model._default_manager.in_bulk(object_pks)
+
+ # Set content types and content objects in the appropriate cache
+ # attributes, so accessing the 'content_type' and 'object'
+ # attributes on each tagged item won't result in further database
+ # hits.
+ for item in tagged_items:
+ item._object_cache = objects[item.content_type_id][item.object_id]
+ item._content_type_cache = content_types[item.content_type_id]
diff --git a/tagging/managers.py b/tagging/managers.py
index 612c127..51e2473 100644
--- a/tagging/managers.py
+++ b/tagging/managers.py
@@ -1,408 +1,68 @@
-"""
-Custom managers for generic tagging models.
-"""
-from django.db import connection
-from django.db.models import Manager
-from django.db.models.query import QuerySet, parse_lookup
-from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import ugettext as _
-
-from tagging import settings
-from tagging.utils import calculate_cloud, get_tag_list, parse_tag_input
-from tagging.utils import LOGARITHMIC
-
-# Python 2.3 compatibility
-if not hasattr(__builtins__, 'set'):
- from sets import Set as set
-
-qn = connection.ops.quote_name
-
-class TagManager(Manager):
- def update_tags(self, obj, tag_names):
- """
- Update tags associated with an object.
- """
- ctype = ContentType.objects.get_for_model(obj)
- current_tags = list(self.filter(items__content_type__pk=ctype.pk,
- items__object_id=obj.pk))
- updated_tag_names = parse_tag_input(tag_names)
- if settings.FORCE_LOWERCASE_TAGS:
- updated_tag_names = [t.lower() for t in updated_tag_names]
-
- TaggedItemModel = self._get_related_model_by_accessor('items')
-
- # Remove tags which no longer apply
- tags_for_removal = [tag for tag in current_tags \
- if tag.name not in updated_tag_names]
- if len(tags_for_removal):
- TaggedItemModel._default_manager.filter(content_type__pk=ctype.pk,
- object_id=obj.pk,
- tag__in=tags_for_removal).delete()
-
- # Add new tags
- current_tag_names = [tag.name for tag in current_tags]
- for tag_name in updated_tag_names:
- if tag_name not in current_tag_names:
- tag, created = self.get_or_create(name=tag_name)
- TaggedItemModel._default_manager.create(tag=tag, object=obj)
-
- def add_tag(self, obj, tag_name):
- """
- Associates the given object with a tag.
- """
- tag_names = parse_tag_input(tag_name)
- if not len(tag_names):
- raise AttributeError(_('No tags were given: "%s".') % tag_name)
- if len(tag_names) > 1:
- raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
- tag_name = tag_names[0]
- if settings.FORCE_LOWERCASE_TAGS:
- tag_name = tag_name.lower()
- tag, created = self.get_or_create(name=tag_name)
- ctype = ContentType.objects.get_for_model(obj)
- TaggedItemModel = self._get_related_model_by_accessor('items')
- TaggedItemModel._default_manager.get_or_create(
- tag=tag, content_type=ctype, object_id=obj.pk)
-
- def get_for_object(self, obj):
- """
- Create a queryset matching all tags associated with the given
- object.
- """
- ctype = ContentType.objects.get_for_model(obj)
- return self.filter(items__content_type__pk=ctype.pk,
- items__object_id=obj.pk)
-
- def usage_for_model(self, model, counts=False, min_count=None, filters=None):
- """
- Obtain a list of tags associated with instances of the given
- Model class.
-
- If ``counts`` is True, a ``count`` attribute will be added to
- each tag, indicating how many times it has been used against
- the Model class in question.
-
- If ``min_count`` is given, only tags which have a ``count``
- greater than or equal to ``min_count`` will be returned.
- Passing a value for ``min_count`` implies ``counts=True``.
-
- To limit the tags (and counts, if specified) returned to those
- used by a subset of the Model's instances, pass a dictionary
- of field lookups to be applied to the given Model as the
- ``filters`` argument.
- """
- if filters is None: filters = {}
- if min_count is not None: counts = True
-
- model_table = qn(model._meta.db_table)
- model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
- query = """
- SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
- FROM
- %(tag)s
- INNER JOIN %(tagged_item)s
- ON %(tag)s.id = %(tagged_item)s.tag_id
- INNER JOIN %(model)s
- ON %(tagged_item)s.object_id = %(model_pk)s
- %%s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- %%s
- GROUP BY %(tag)s.id, %(tag)s.name
- %%s
- ORDER BY %(tag)s.name ASC""" % {
- 'tag': qn(self.model._meta.db_table),
- 'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
- 'tagged_item': qn(self._get_related_model_by_accessor('items')._meta.db_table),
- 'model': model_table,
- 'model_pk': model_pk,
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- }
-
- extra_joins = ''
- extra_criteria = ''
- min_count_sql = ''
- params = []
- if len(filters) > 0:
- joins, where, params = parse_lookup(filters.items(), model._meta)
- extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
- for (alias, (table, join_type, condition)) in joins.items()])
- extra_criteria = 'AND %s' % (' AND '.join(where))
- if min_count is not None:
- min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
- params.append(min_count)
-
- cursor = connection.cursor()
- cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
- tags = []
- for row in cursor.fetchall():
- t = self.model(*row[:2])
- if counts:
- t.count = row[2]
- tags.append(t)
- return tags
-
- def related_for_model(self, tags, model, counts=False, min_count=None):
- """
- Obtain a list of tags related to a given list of tags - that
- is, other tags used by items which have all the given tags.
-
- If ``counts`` is True, a ``count`` attribute will be added to
- each tag, indicating the number of items which have it in
- addition to the given list of tags.
-
- If ``min_count`` is given, only tags which have a ``count``
- greater than or equal to ``min_count`` will be returned.
- Passing a value for ``min_count`` implies ``counts=True``.
- """
- if min_count is not None: counts = True
- tags = get_tag_list(tags)
- tag_count = len(tags)
- tagged_item_table = qn(self._get_related_model_by_accessor('items')._meta.db_table)
- query = """
- SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
- FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.object_id IN
- (
- SELECT %(tagged_item)s.object_id
- FROM %(tagged_item)s, %(tag)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tag)s.id = %(tagged_item)s.tag_id
- AND %(tag)s.id IN (%(tag_id_placeholders)s)
- GROUP BY %(tagged_item)s.object_id
- HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
- )
- AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
- GROUP BY %(tag)s.id, %(tag)s.name
- %(min_count_sql)s
- ORDER BY %(tag)s.name ASC""" % {
- 'tag': qn(self.model._meta.db_table),
- 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
- 'tagged_item': tagged_item_table,
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- 'tag_count': tag_count,
- 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
- }
-
- params = [tag.pk for tag in tags] * 2
- if min_count is not None:
- params.append(min_count)
-
- cursor = connection.cursor()
- cursor.execute(query, params)
- related = []
- for row in cursor.fetchall():
- tag = self.model(*row[:2])
- if counts is True:
- tag.count = row[2]
- related.append(tag)
- return related
-
- def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
- filters=None, min_count=None):
- """
- Obtain a list of tags associated with instances of the given
- Model, giving each tag a ``count`` attribute indicating how
- many times it has been used and a ``font_size`` attribute for
- use in displaying a tag cloud.
-
- ``steps`` defines the range of font sizes - ``font_size`` will
- be an integer between 1 and ``steps`` (inclusive).
-
- ``distribution`` defines the type of font size distribution
- algorithm which will be used - logarithmic or linear. It must
- be either ``tagging.utils.LOGARITHMIC`` or
- ``tagging.utils.LINEAR``.
-
- To limit the tags displayed in the cloud to those associated
- with a subset of the Model's instances, pass a dictionary of
- field lookups to be applied to the given Model as the
- ``filters`` argument.
-
- To limit the tags displayed in the cloud to those with a
- ``count`` greater than or equal to ``min_count``, pass a value
- for the ``min_count`` argument.
- """
- tags = list(self.usage_for_model(model, counts=True, filters=filters,
- min_count=min_count))
- return calculate_cloud(tags, steps, distribution)
-
- def _get_related_model_by_accessor(self, accessor):
- """
- Returns the model for the related object accessed by the
- given attribute name.
-
- Since we sometimes need to access the ``TaggedItem`` model
- when managing tagging and the``Tag`` model does not have a
- field representing this relationship, this method is used to
- retrieve the ``TaggedItem`` model, avoiding circular imports
- betweeen the ``models`` and ``managers`` modules.
- """
- for rel_obj in self.model._meta.get_all_related_objects():
- if rel_obj.get_accessor_name() == accessor:
- return rel_obj.model
- raise ValueError(_('Could not find a related object with the accessor "%s".') % accessor)
-
-class TaggedItemManager(Manager):
- def get_by_model(self, model, tags):
- """
- Create a queryset matching instances of the given Model
- associated with a given Tag or list of Tags.
- """
- tags = get_tag_list(tags)
- tag_count = len(tags)
- if tag_count == 0:
- # No existing tags were given
- return model._default_manager.none()
- elif tag_count == 1:
- # Optimisation for single tag - fall through to the simpler
- # query below.
- tag = tags[0]
- else:
- return self.get_intersection_by_model(model, tags)
-
- ctype = ContentType.objects.get_for_model(model)
- opts = self.model._meta
- tagged_item_table = qn(opts.db_table)
- return model._default_manager.extra(
- tables=[opts.db_table],
- where=[
- '%s.content_type_id = %%s' % tagged_item_table,
- '%s.tag_id = %%s' % tagged_item_table,
- '%s.%s = %s.object_id' % (qn(model._meta.db_table),
- qn(model._meta.pk.column),
- tagged_item_table)
- ],
- params=[ctype.pk, tag.pk],
- )
-
- def get_intersection_by_model(self, model, tags):
- """
- Create a queryset matching instances of the given Model
- associated with all the given list of Tags.
-
- FIXME The query currently used to grab the ids of objects
- which have all the tags should be all that we need run,
- using a non-explicit join for the QuerySet returned, as
- in get_by_model, but there's currently no way to get the
- required GROUP BY and HAVING clauses into Django's ORM.
-
- Once the ORM is capable of this, we should have a
- solution which requires only a single query and won't
- have the problem where the number of ids in the IN
- clause in the QuerySet could exceed the length allowed,
- as could currently happen.
- """
- tags = get_tag_list(tags)
- tag_count = len(tags)
- model_table = qn(model._meta.db_table)
- # This query selects the ids of all objects which have all the
- # given tags.
- query = """
- SELECT %(model_pk)s
- FROM %(model)s, %(tagged_item)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
- AND %(model_pk)s = %(tagged_item)s.object_id
- GROUP BY %(model_pk)s
- HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- 'tag_count': tag_count,
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [tag.pk for tag in tags])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- return model._default_manager.filter(pk__in=object_ids)
- else:
- return model._default_manager.none()
-
- def get_union_by_model(self, model, tags):
- """
- Create a queryset matching instances of the given Model
- associated with any of the given list of Tags.
- """
- tags = get_tag_list(tags)
- tag_count = len(tags)
- model_table = qn(model._meta.db_table)
- # This query selects the ids of all objects which have any of
- # the given tags.
- query = """
- SELECT %(model_pk)s
- FROM %(model)s, %(tagged_item)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
- AND %(model_pk)s = %(tagged_item)s.object_id
- GROUP BY %(model_pk)s""" % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [tag.pk for tag in tags])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- return model._default_manager.filter(pk__in=object_ids)
- else:
- return model._default_manager.none()
-
- def get_related(self, obj, model, num=None):
- """
- Retrieve instances of ``model`` which share tags with the
- model instance ``obj``, ordered by the number of shared tags
- in descending order.
-
- If ``num`` is given, a maximum of ``num`` instances will be
- returned.
- """
- model_table = qn(model._meta.db_table)
- content_type = ContentType.objects.get_for_model(obj)
- related_content_type = ContentType.objects.get_for_model(model)
- query = """
- SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
- FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
- WHERE %(tagged_item)s.object_id = %%s
- AND %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tag)s.id = %(tagged_item)s.tag_id
- AND related_tagged_item.content_type_id = %(related_content_type_id)s
- AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
- AND %(model_pk)s = related_tagged_item.object_id"""
- if content_type.pk == related_content_type.pk:
- # Exclude the given instance itself if determining related
- # instances for the same model.
- query += """
- AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
- query += """
- GROUP BY %(model_pk)s
- ORDER BY %(count)s DESC
- %(limit_offset)s"""
- query = query % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'count': qn('count'),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
- 'content_type_id': content_type.pk,
- 'related_content_type_id': related_content_type.pk,
- 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [obj.pk])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- # Use in_bulk here instead of an id__in lookup, because id__in would
- # clobber the ordering.
- object_dict = model._default_manager.in_bulk(object_ids)
- return [object_dict[object_id] for object_id in object_ids]
- else:
- return model._default_manager.none()
+"""
+Custom managers for Django models registered with the tagging
+application.
+"""
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+from tagging.models import Tag, TaggedItem
+
+class ModelTagManager(models.Manager):
+ """
+ A manager for retrieving tags for a particular model.
+ """
+ def get_query_set(self):
+ ctype = ContentType.objects.get_for_model(self.model)
+ return Tag.objects.filter(
+ items__content_type__pk=ctype.pk).distinct()
+
+ def cloud(self, *args, **kwargs):
+ return Tag.objects.cloud_for_model(self.model, *args, **kwargs)
+
+ def related(self, tags, *args, **kwargs):
+ return Tag.objects.related_for_model(tags, self.model, *args, **kwargs)
+
+ def usage(self, *args, **kwargs):
+ return Tag.objects.usage_for_model(self.model, *arg, **kwargs)
+
+class ModelTaggedItemManager(models.Manager):
+ """
+ A manager for retrieving model instances based on their tags.
+ """
+ def related_to(self, obj, queryset=None, num=None):
+ if queryset is None:
+ return TaggedItem.objects.get_related(obj, self.model, num=num)
+ else:
+ return TaggedItem.objects.get_related(obj, queryset, num=num)
+
+ def with_all(self, tags, queryset=None):
+ if queryset is None:
+ return TaggedItem.objects.get_by_model(self.model, tags)
+ else:
+ return TaggedItem.objects.get_by_model(queryset, tags)
+
+ def with_any(self, tags, queryset=None):
+ if queryset is None:
+ return TaggedItem.objects.get_union_by_model(self.model, tags)
+ else:
+ return TaggedItem.objects.get_union_by_model(queryset, tags)
+
+class TagDescriptor(object):
+ """
+ A descriptor which provides access to a ``ModelTagManager`` for
+ model classes and simple retrieval, updating and deletion of tags
+ for model instances.
+ """
+ def __get__(self, instance, owner):
+ if not instance:
+ tag_manager = ModelTagManager()
+ tag_manager.model = owner
+ return tag_manager
+ else:
+ return Tag.objects.get_for_object(instance)
+
+ def __set__(self, instance, value):
+ Tag.objects.update_tags(instance, value)
+
+ def __delete__(self, instance):
+ Tag.objects.update_tags(instance, None)
diff --git a/tagging/models.py b/tagging/models.py
index f81c896..7463ddb 100644
--- a/tagging/models.py
+++ b/tagging/models.py
@@ -1,52 +1,502 @@
-"""
-Models for generic tagging.
-"""
-from django.db import models
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import ugettext_lazy as _
-
-from tagging.managers import TagManager, TaggedItemManager
-from tagging.validators import isTag
-
-class Tag(models.Model):
- """
- A tag.
- """
- name = models.CharField(_('name'), max_length=50, unique=True, db_index=True, validator_list=[isTag])
-
- objects = TagManager()
-
- class Meta:
- ordering = ('name',)
- verbose_name = _('tag')
- verbose_name_plural = _('tags')
-
- class Admin:
- pass
-
- def __unicode__(self):
- return self.name
-
-class TaggedItem(models.Model):
- """
- Holds the relationship between a tag and the item being tagged.
- """
- tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
- content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
- object_id = models.PositiveIntegerField(_('object id'), db_index=True)
- object = generic.GenericForeignKey('content_type', 'object_id')
-
- objects = TaggedItemManager()
-
- class Meta:
- # Enforce unique tag association per object
- unique_together = (('tag', 'content_type', 'object_id'),)
- verbose_name = _('tagged item')
- verbose_name_plural = _('tagged items')
-
- class Admin:
- pass
-
- def __unicode__(self):
- return u'%s [%s]' % (self.object, self.tag)
+"""
+Models and managers for generic tagging.
+"""
+# Python 2.3 compatibility
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
+from django.db.models.query import QuerySet
+from django.utils.translation import ugettext_lazy as _
+
+from tagging import settings
+from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input
+from tagging.utils import LOGARITHMIC
+from tagging.validators import isTag
+
+qn = connection.ops.quote_name
+
+try:
+ from django.db.models.query import parse_lookup
+except ImportError:
+ parse_lookup = None
+
+############
+# Managers #
+############
+
+class TagManager(models.Manager):
+ def update_tags(self, obj, tag_names):
+ """
+ Update tags associated with an object.
+ """
+ ctype = ContentType.objects.get_for_model(obj)
+ current_tags = list(self.filter(items__content_type__pk=ctype.pk,
+ items__object_id=obj.pk))
+ updated_tag_names = parse_tag_input(tag_names)
+ if settings.FORCE_LOWERCASE_TAGS:
+ updated_tag_names = [t.lower() for t in updated_tag_names]
+
+ # Remove tags which no longer apply
+ tags_for_removal = [tag for tag in current_tags \
+ if tag.name not in updated_tag_names]
+ if len(tags_for_removal):
+ TaggedItem._default_manager.filter(content_type__pk=ctype.pk,
+ object_id=obj.pk,
+ tag__in=tags_for_removal).delete()
+ # Add new tags
+ current_tag_names = [tag.name for tag in current_tags]
+ for tag_name in updated_tag_names:
+ if tag_name not in current_tag_names:
+ tag, created = self.get_or_create(name=tag_name)
+ TaggedItem._default_manager.create(tag=tag, object=obj)
+
+ def add_tag(self, obj, tag_name):
+ """
+ Associates the given object with a tag.
+ """
+ tag_names = parse_tag_input(tag_name)
+ if not len(tag_names):
+ raise AttributeError(_('No tags were given: "%s".') % tag_name)
+ if len(tag_names) > 1:
+ raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
+ tag_name = tag_names[0]
+ if settings.FORCE_LOWERCASE_TAGS:
+ tag_name = tag_name.lower()
+ tag, created = self.get_or_create(name=tag_name)
+ ctype = ContentType.objects.get_for_model(obj)
+ TaggedItem._default_manager.get_or_create(
+ tag=tag, content_type=ctype, object_id=obj.pk)
+
+ def get_for_object(self, obj):
+ """
+ Create a queryset matching all tags associated with the given
+ object.
+ """
+ ctype = ContentType.objects.get_for_model(obj)
+ return self.filter(items__content_type__pk=ctype.pk,
+ items__object_id=obj.pk)
+
+ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
+ """
+ Perform the custom SQL query for ``usage_for_model`` and
+ ``usage_for_queryset``.
+ """
+ if min_count is not None: counts = True
+
+ model_table = qn(model._meta.db_table)
+ model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
+ query = """
+ SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
+ FROM
+ %(tag)s
+ INNER JOIN %(tagged_item)s
+ ON %(tag)s.id = %(tagged_item)s.tag_id
+ INNER JOIN %(model)s
+ ON %(tagged_item)s.object_id = %(model_pk)s
+ %%s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ %%s
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %%s
+ ORDER BY %(tag)s.name ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
+ 'tagged_item': qn(TaggedItem._meta.db_table),
+ 'model': model_table,
+ 'model_pk': model_pk,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ }
+
+ min_count_sql = ''
+ if min_count is not None:
+ min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
+ tags = []
+ for row in cursor.fetchall():
+ t = self.model(*row[:2])
+ if counts:
+ t.count = row[2]
+ tags.append(t)
+ return tags
+
+ def usage_for_model(self, model, counts=False, min_count=None, filters=None):
+ """
+ Obtain a list of tags associated with instances of the given
+ Model class.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+
+ To limit the tags (and counts, if specified) returned to those
+ used by a subset of the Model's instances, pass a dictionary
+ of field lookups to be applied to the given Model as the
+ ``filters`` argument.
+ """
+ if filters is None: filters = {}
+
+ if not parse_lookup:
+ # post-queryset-refactor (hand off to usage_for_queryset)
+ queryset = model._default_manager.filter()
+ for f in filters.items():
+ queryset.query.add_filter(f)
+ usage = self.usage_for_queryset(queryset, counts, min_count)
+ else:
+ # pre-queryset-refactor
+ extra_joins = ''
+ extra_criteria = ''
+ params = []
+ if len(filters) > 0:
+ joins, where, params = parse_lookup(filters.items(), model._meta)
+ extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
+ for (alias, (table, join_type, condition)) in joins.items()])
+ extra_criteria = 'AND %s' % (' AND '.join(where))
+ usage = self._get_usage(model, counts, min_count, extra_joins, extra_criteria, params)
+
+ return usage
+
+ def usage_for_queryset(self, queryset, counts=False, min_count=None):
+ """
+ Obtain a list of tags associated with instances of a model
+ contained in the given queryset.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if parse_lookup:
+ raise AttributeError("'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.")
+
+ extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
+ where, params = queryset.query.where.as_sql()
+ if where:
+ extra_criteria = 'AND %s' % where
+ else:
+ extra_criteria = ''
+ return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
+
+ def related_for_model(self, tags, model, counts=False, min_count=None):
+ """
+ Obtain a list of tags related to a given list of tags - that
+ is, other tags used by items which have all the given tags.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating the number of items which have it in
+ addition to the given list of tags.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if min_count is not None: counts = True
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ tagged_item_table = qn(TaggedItem._meta.db_table)
+ query = """
+ SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
+ FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.object_id IN
+ (
+ SELECT %(tagged_item)s.object_id
+ FROM %(tagged_item)s, %(tag)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND %(tag)s.id IN (%(tag_id_placeholders)s)
+ GROUP BY %(tagged_item)s.object_id
+ HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
+ )
+ AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %(min_count_sql)s
+ ORDER BY %(tag)s.name ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
+ 'tagged_item': tagged_item_table,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'tag_count': tag_count,
+ 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
+ }
+
+ params = [tag.pk for tag in tags] * 2
+ if min_count is not None:
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query, params)
+ related = []
+ for row in cursor.fetchall():
+ tag = self.model(*row[:2])
+ if counts is True:
+ tag.count = row[2]
+ related.append(tag)
+ return related
+
+ def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
+ filters=None, min_count=None):
+ """
+ Obtain a list of tags associated with instances of the given
+ Model, giving each tag a ``count`` attribute indicating how
+ many times it has been used and a ``font_size`` attribute for
+ use in displaying a tag cloud.
+
+ ``steps`` defines the range of font sizes - ``font_size`` will
+ be an integer between 1 and ``steps`` (inclusive).
+
+ ``distribution`` defines the type of font size distribution
+ algorithm which will be used - logarithmic or linear. It must
+ be either ``tagging.utils.LOGARITHMIC`` or
+ ``tagging.utils.LINEAR``.
+
+ To limit the tags displayed in the cloud to those associated
+ with a subset of the Model's instances, pass a dictionary of
+ field lookups to be applied to the given Model as the
+ ``filters`` argument.
+
+ To limit the tags displayed in the cloud to those with a
+ ``count`` greater than or equal to ``min_count``, pass a value
+ for the ``min_count`` argument.
+ """
+ tags = list(self.usage_for_model(model, counts=True, filters=filters,
+ min_count=min_count))
+ return calculate_cloud(tags, steps, distribution)
+
+class TaggedItemManager(models.Manager):
+ """
+ FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
+ SQL clauses required by many of this manager's methods into
+ Django's ORM.
+
+ For now, we manually execute a query to retrieve the PKs of
+ objects we're interested in, then use the ORM's ``__in``
+ lookup to return a ``QuerySet``.
+
+ Once the queryset-refactor branch lands in trunk, this can be
+ tidied up significantly.
+ """
+ def get_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with a given tag or list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ if tag_count == 0:
+ # No existing tags were given
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ return model._default_manager.none()
+ elif tag_count == 1:
+ # Optimisation for single tag - fall through to the simpler
+ # query below.
+ tag = tags[0]
+ else:
+ return self.get_intersection_by_model(queryset_or_model, tags)
+
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ content_type = ContentType.objects.get_for_model(model)
+ opts = self.model._meta
+ tagged_item_table = qn(opts.db_table)
+ return queryset.extra(
+ tables=[opts.db_table],
+ where=[
+ '%s.content_type_id = %%s' % tagged_item_table,
+ '%s.tag_id = %%s' % tagged_item_table,
+ '%s.%s = %s.object_id' % (qn(model._meta.db_table),
+ qn(model._meta.pk.column),
+ tagged_item_table)
+ ],
+ params=[content_type.pk, tag.pk],
+ )
+
+ def get_intersection_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *all* of the given list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have all the
+ # given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s
+ HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'tag_count': tag_count,
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_union_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *any* of the given list of tags.
+ """
+ tags = get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have any of
+ # the given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_related(self, obj, queryset_or_model, num=None):
+ """
+ Retrieve a list of instances of the specified model which share
+ tags with the model instance ``obj``, ordered by the number of
+ shared tags in descending order.
+
+ If ``num`` is given, a maximum of ``num`` instances will be
+ returned.
+ """
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ model_table = qn(model._meta.db_table)
+ content_type = ContentType.objects.get_for_model(obj)
+ related_content_type = ContentType.objects.get_for_model(model)
+ query = """
+ SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
+ FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
+ WHERE %(tagged_item)s.object_id = %%s
+ AND %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND related_tagged_item.content_type_id = %(related_content_type_id)s
+ AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
+ AND %(model_pk)s = related_tagged_item.object_id"""
+ if content_type.pk == related_content_type.pk:
+ # Exclude the given instance itself if determining related
+ # instances for the same model.
+ query += """
+ AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
+ query += """
+ GROUP BY %(model_pk)s
+ ORDER BY %(count)s DESC
+ %(limit_offset)s"""
+ query = query % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'count': qn('count'),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
+ 'content_type_id': content_type.pk,
+ 'related_content_type_id': related_content_type.pk,
+ 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [obj.pk])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ # Use in_bulk here instead of an id__in lookup, because id__in would
+ # clobber the ordering.
+ object_dict = queryset.in_bulk(object_ids)
+ return [object_dict[object_id] for object_id in object_ids \
+ if object_id in object_dict]
+ else:
+ return []
+
+##########
+# Models #
+##########
+
+class Tag(models.Model):
+ """
+ A tag.
+ """
+ name = models.CharField(_('name'), max_length=50, unique=True, db_index=True, validator_list=[isTag])
+
+ objects = TagManager()
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('tag')
+ verbose_name_plural = _('tags')
+
+ class Admin:
+ pass
+
+ def __unicode__(self):
+ return self.name
+
+class TaggedItem(models.Model):
+ """
+ Holds the relationship between a tag and the item being tagged.
+ """
+ tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
+ content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
+ object_id = models.PositiveIntegerField(_('object id'), db_index=True)
+ object = generic.GenericForeignKey('content_type', 'object_id')
+
+ objects = TaggedItemManager()
+
+ class Meta:
+ # Enforce unique tag association per object
+ unique_together = (('tag', 'content_type', 'object_id'),)
+ verbose_name = _('tagged item')
+ verbose_name_plural = _('tagged items')
+
+ class Admin:
+ pass
+
+ def __unicode__(self):
+ return u'%s [%s]' % (self.object, self.tag)
diff --git a/tagging/settings.py b/tagging/settings.py
index 78da3af..1d6224c 100644
--- a/tagging/settings.py
+++ b/tagging/settings.py
@@ -1,13 +1,13 @@
-"""
-Convenience module for access of custom tagging application settings,
-which enforces default settings when the main settings module does not
-contain the appropriate settings.
-"""
-from django.conf import settings
-
-# The maximum length of a tag's name.
-MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50)
-
-# Whether to force all tags to lowercase before they are saved to the
-# database.
-FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False)
+"""
+Convenience module for access of custom tagging application settings,
+which enforces default settings when the main settings module does not
+contain the appropriate settings.
+"""
+from django.conf import settings
+
+# The maximum length of a tag's name.
+MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50)
+
+# Whether to force all tags to lowercase before they are saved to the
+# database.
+FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False)
diff --git a/tagging/templatetags/.svn/all-wcprops b/tagging/templatetags/.svn/all-wcprops
new file mode 100644
index 0000000..c3f4205
--- /dev/null
+++ b/tagging/templatetags/.svn/all-wcprops
@@ -0,0 +1,17 @@
+K 25
+svn:wc:ra_dav:version-url
+V 44
+/svn/!svn/ver/114/trunk/tagging/templatetags
+END
+tagging_tags.py
+K 25
+svn:wc:ra_dav:version-url
+V 60
+/svn/!svn/ver/114/trunk/tagging/templatetags/tagging_tags.py
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 55
+/svn/!svn/ver/93/trunk/tagging/templatetags/__init__.py
+END
diff --git a/tagging/templatetags/.svn/dir-prop-base b/tagging/templatetags/.svn/dir-prop-base
new file mode 100644
index 0000000..4cc643b
--- /dev/null
+++ b/tagging/templatetags/.svn/dir-prop-base
@@ -0,0 +1,6 @@
+K 10
+svn:ignore
+V 6
+*.pyc
+
+END
diff --git a/tagging/templatetags/.svn/entries b/tagging/templatetags/.svn/entries
new file mode 100644
index 0000000..3c6a555
--- /dev/null
+++ b/tagging/templatetags/.svn/entries
@@ -0,0 +1,96 @@
+9
+
+dir
+147
+http://django-tagging.googlecode.com/svn/trunk/tagging/templatetags
+http://django-tagging.googlecode.com/svn
+
+
+
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+svn:special svn:externals svn:needs-lock
+
+
+
+
+
+
+
+
+
+
+
+83e7428b-ec2a-0410-86f2-bf466d0e5e72
+
+tagging_tags.py
+file
+
+
+
+
+2008-08-21T18:05:40.000000Z
+1deb2386b13645b6b26bc2a74e6bd6dc
+2008-01-12T02:02:01.926198Z
+114
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+8602
+
+__init__.py
+file
+
+
+
+
+2008-08-21T18:05:40.000000Z
+d41d8cd98f00b204e9800998ecf8427e
+2007-08-20T09:19:55.193228Z
+93
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+0
+
diff --git a/tagging/templatetags/.svn/format b/tagging/templatetags/.svn/format
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/tagging/templatetags/.svn/format
@@ -0,0 +1 @@
+9
diff --git a/tagging/templatetags/.svn/prop-base/__init__.py.svn-base b/tagging/templatetags/.svn/prop-base/__init__.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/templatetags/.svn/prop-base/__init__.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base b/tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/templatetags/.svn/text-base/__init__.py.svn-base b/tagging/templatetags/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tagging/templatetags/.svn/text-base/__init__.py.svn-base
diff --git a/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base b/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base
new file mode 100644
index 0000000..11d31cc
--- /dev/null
+++ b/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base
@@ -0,0 +1,231 @@
+from django.db.models import get_model
+from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable
+from django.utils.translation import ugettext as _
+
+from tagging.models import Tag, TaggedItem
+from tagging.utils import LINEAR, LOGARITHMIC
+
+register = Library()
+
+class TagsForModelNode(Node):
+ def __init__(self, model, context_var, counts):
+ self.model = model
+ self.context_var = context_var
+ self.counts = counts
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts)
+ return ''
+
+class TagCloudForModelNode(Node):
+ def __init__(self, model, context_var, **kwargs):
+ self.model = model
+ self.context_var = context_var
+ self.kwargs = kwargs
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = \
+ Tag.objects.cloud_for_model(model, **self.kwargs)
+ return ''
+
+class TagsForObjectNode(Node):
+ def __init__(self, obj, context_var):
+ self.obj = Variable(obj)
+ self.context_var = context_var
+
+ def render(self, context):
+ context[self.context_var] = \
+ Tag.objects.get_for_object(self.obj.resolve(context))
+ return ''
+
+class TaggedObjectsNode(Node):
+ def __init__(self, tag, model, context_var):
+ self.tag = Variable(tag)
+ self.context_var = context_var
+ self.model = model
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = \
+ TaggedItem.objects.get_by_model(model, self.tag.resolve(context))
+ return ''
+
+def do_tags_for_model(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects associated with a given model
+ and stores them in a context variable.
+
+ Usage::
+
+ {% tags_for_model [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ Extended usage::
+
+ {% tags_for_model [model] as [varname] with counts %}
+
+ If specified - by providing extra ``with counts`` arguments - adds
+ a ``count`` attribute to each tag containing the number of
+ instances of the given model which have been tagged with it.
+
+ Examples::
+
+ {% tags_for_model products.Widget as widget_tags %}
+ {% tags_for_model products.Widget as widget_tags with counts %}
+
+ """
+ bits = token.contents.split()
+ len_bits = len(bits)
+ if len_bits not in (4, 6):
+ raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ if len_bits == 6:
+ if bits[4] != 'with':
+ raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
+ if bits[5] != 'counts':
+ raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0])
+ if len_bits == 4:
+ return TagsForModelNode(bits[1], bits[3], counts=False)
+ else:
+ return TagsForModelNode(bits[1], bits[3], counts=True)
+
+def do_tag_cloud_for_model(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects for a given model, with tag
+ cloud attributes set, and stores them in a context variable.
+
+ Usage::
+
+ {% tag_cloud_for_model [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ Extended usage::
+
+ {% tag_cloud_for_model [model] as [varname] with [options] %}
+
+ Extra options can be provided after an optional ``with`` argument,
+ with each option being specified in ``[name]=[value]`` format. Valid
+ extra options are:
+
+ ``steps``
+ Integer. Defines the range of font sizes.
+
+ ``min_count``
+ Integer. Defines the minimum number of times a tag must have
+ been used to appear in the cloud.
+
+ ``distribution``
+ One of ``linear`` or ``log``. Defines the font-size
+ distribution algorithm to use when generating the tag cloud.
+
+ Examples::
+
+ {% tag_cloud_for_model products.Widget as widget_tags %}
+ {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
+
+ """
+ bits = token.contents.split()
+ len_bits = len(bits)
+ if len_bits != 4 and len_bits not in range(6, 9):
+ raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ kwargs = {}
+ if len_bits > 5:
+ if bits[4] != 'with':
+ raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
+ for i in range(5, len_bits):
+ try:
+ name, value = bits[i].split('=')
+ if name == 'steps' or name == 'min_count':
+ try:
+ kwargs[str(name)] = int(value)
+ except ValueError:
+ raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ 'value': value,
+ })
+ elif name == 'distribution':
+ if value in ['linear', 'log']:
+ kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value]
+ else:
+ raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ 'value': value,
+ })
+ else:
+ raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ })
+ except ValueError:
+ raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % {
+ 'tag': bits[0],
+ 'option': bits[i],
+ })
+ return TagCloudForModelNode(bits[1], bits[3], **kwargs)
+
+def do_tags_for_object(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects associated with an object and
+ stores them in a context variable.
+
+ Usage::
+
+ {% tags_for_object [object] as [varname] %}
+
+ Example::
+
+ {% tags_for_object foo_object as tag_list %}
+ """
+ bits = token.contents.split()
+ if len(bits) != 4:
+ raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ return TagsForObjectNode(bits[1], bits[3])
+
+def do_tagged_objects(parser, token):
+ """
+ Retrieves a list of instances of a given model which are tagged with
+ a given ``Tag`` and stores them in a context variable.
+
+ Usage::
+
+ {% tagged_objects [tag] in [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ The tag must be an instance of a ``Tag``, not the name of a tag.
+
+ Example::
+
+ {% tagged_objects comedy_tag in tv.Show as comedies %}
+
+ """
+ bits = token.contents.split()
+ if len(bits) != 6:
+ raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0])
+ if bits[2] != 'in':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0])
+ if bits[4] != 'as':
+ raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0])
+ return TaggedObjectsNode(bits[1], bits[3], bits[5])
+
+register.tag('tags_for_model', do_tags_for_model)
+register.tag('tag_cloud_for_model', do_tag_cloud_for_model)
+register.tag('tags_for_object', do_tags_for_object)
+register.tag('tagged_objects', do_tagged_objects)
diff --git a/tagging/templatetags/tagging_tags.py b/tagging/templatetags/tagging_tags.py
index 789517a..11d31cc 100644
--- a/tagging/templatetags/tagging_tags.py
+++ b/tagging/templatetags/tagging_tags.py
@@ -1,231 +1,231 @@
-from django.db.models import get_model
-from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable
-from django.utils.translation import ugettext as _
-
-from tagging.models import Tag, TaggedItem
-from tagging.utils import LINEAR, LOGARITHMIC
-
-register = Library()
-
-class TagsForModelNode(Node):
- def __init__(self, model, context_var, counts):
- self.model = model
- self.context_var = context_var
- self.counts = counts
-
- def render(self, context):
- model = get_model(*self.model.split('.'))
- if model is None:
- raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model)
- context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts)
- return ''
-
-class TagCloudForModelNode(Node):
- def __init__(self, model, context_var, **kwargs):
- self.model = model
- self.context_var = context_var
- self.kwargs = kwargs
-
- def render(self, context):
- model = get_model(*self.model.split('.'))
- if model is None:
- raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model)
- context[self.context_var] = \
- Tag.objects.cloud_for_model(model, **self.kwargs)
- return ''
-
-class TagsForObjectNode(Node):
- def __init__(self, obj, context_var):
- self.obj = Variable(obj)
- self.context_var = context_var
-
- def render(self, context):
- context[self.context_var] = \
- Tag.objects.get_for_object(self.obj.resolve(context))
- return ''
-
-class TaggedObjectsNode(Node):
- def __init__(self, tag, model, context_var):
- self.tag = Variable(tag)
- self.context_var = context_var
- self.model = model
-
- def render(self, context):
- model = get_model(*self.model.split('.'))
- if model is None:
- raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model)
- context[self.context_var] = \
- TaggedItem.objects.get_by_model(model, self.tag.resolve(context))
- return ''
-
-def do_tags_for_model(parser, token):
- """
- Retrieves a list of ``Tag`` objects associated with a given model
- and stores them in a context variable.
-
- Usage::
-
- {% tags_for_model [model] as [varname] %}
-
- The model is specified in ``[appname].[modelname]`` format.
-
- Extended usage::
-
- {% tags_for_model [model] as [varname] with counts %}
-
- If specified - by providing extra ``with counts`` arguments - adds
- a ``count`` attribute to each tag containing the number of
- instances of the given model which have been tagged with it.
-
- Examples::
-
- {% tags_for_model products.Widget as widget_tags %}
- {% tags_for_model products.Widget as widget_tags with counts %}
-
- """
- bits = token.contents.split()
- len_bits = len(bits)
- if len_bits not in (4, 6):
- raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0])
- if bits[2] != 'as':
- raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
- if len_bits == 6:
- if bits[4] != 'with':
- raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
- if bits[5] != 'counts':
- raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0])
- if len_bits == 4:
- return TagsForModelNode(bits[1], bits[3], counts=False)
- else:
- return TagsForModelNode(bits[1], bits[3], counts=True)
-
-def do_tag_cloud_for_model(parser, token):
- """
- Retrieves a list of ``Tag`` objects for a given model, with tag
- cloud attributes set, and stores them in a context variable.
-
- Usage::
-
- {% tag_cloud_for_model [model] as [varname] %}
-
- The model is specified in ``[appname].[modelname]`` format.
-
- Extended usage::
-
- {% tag_cloud_for_model [model] as [varname] with [options] %}
-
- Extra options can be provided after an optional ``with`` argument,
- with each option being specified in ``[name]=[value]`` format. Valid
- extra options are:
-
- ``steps``
- Integer. Defines the range of font sizes.
-
- ``min_count``
- Integer. Defines the minimum number of times a tag must have
- been used to appear in the cloud.
-
- ``distribution``
- One of ``linear`` or ``log``. Defines the font-size
- distribution algorithm to use when generating the tag cloud.
-
- Examples::
-
- {% tag_cloud_for_model products.Widget as widget_tags %}
- {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
-
- """
- bits = token.contents.split()
- len_bits = len(bits)
- if len_bits != 4 and len_bits not in range(6, 9):
- raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0])
- if bits[2] != 'as':
- raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
- kwargs = {}
- if len_bits > 5:
- if bits[4] != 'with':
- raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
- for i in range(5, len_bits):
- try:
- name, value = bits[i].split('=')
- if name == 'steps' or name == 'min_count':
- try:
- kwargs[str(name)] = int(value)
- except ValueError:
- raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % {
- 'tag': bits[0],
- 'option': name,
- 'value': value,
- })
- elif name == 'distribution':
- if value in ['linear', 'log']:
- kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value]
- else:
- raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % {
- 'tag': bits[0],
- 'option': name,
- 'value': value,
- })
- else:
- raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % {
- 'tag': bits[0],
- 'option': name,
- })
- except ValueError:
- raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % {
- 'tag': bits[0],
- 'option': bits[i],
- })
- return TagCloudForModelNode(bits[1], bits[3], **kwargs)
-
-def do_tags_for_object(parser, token):
- """
- Retrieves a list of ``Tag`` objects associated with an object and
- stores them in a context variable.
-
- Usage::
-
- {% tags_for_object [object] as [varname] %}
-
- Example::
-
- {% tags_for_object foo_object as tag_list %}
- """
- bits = token.contents.split()
- if len(bits) != 4:
- raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0])
- if bits[2] != 'as':
- raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
- return TagsForObjectNode(bits[1], bits[3])
-
-def do_tagged_objects(parser, token):
- """
- Retrieves a list of instances of a given model which are tagged with
- a given ``Tag`` and stores them in a context variable.
-
- Usage::
-
- {% tagged_objects [tag] in [model] as [varname] %}
-
- The model is specified in ``[appname].[modelname]`` format.
-
- The tag must be an instance of a ``Tag``, not the name of a tag.
-
- Example::
-
- {% tagged_objects comedy_tag in tv.Show as comedies %}
-
- """
- bits = token.contents.split()
- if len(bits) != 6:
- raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0])
- if bits[2] != 'in':
- raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0])
- if bits[4] != 'as':
- raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0])
- return TaggedObjectsNode(bits[1], bits[3], bits[5])
-
-register.tag('tags_for_model', do_tags_for_model)
-register.tag('tag_cloud_for_model', do_tag_cloud_for_model)
-register.tag('tags_for_object', do_tags_for_object)
-register.tag('tagged_objects', do_tagged_objects)
+from django.db.models import get_model
+from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable
+from django.utils.translation import ugettext as _
+
+from tagging.models import Tag, TaggedItem
+from tagging.utils import LINEAR, LOGARITHMIC
+
+register = Library()
+
+class TagsForModelNode(Node):
+ def __init__(self, model, context_var, counts):
+ self.model = model
+ self.context_var = context_var
+ self.counts = counts
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts)
+ return ''
+
+class TagCloudForModelNode(Node):
+ def __init__(self, model, context_var, **kwargs):
+ self.model = model
+ self.context_var = context_var
+ self.kwargs = kwargs
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = \
+ Tag.objects.cloud_for_model(model, **self.kwargs)
+ return ''
+
+class TagsForObjectNode(Node):
+ def __init__(self, obj, context_var):
+ self.obj = Variable(obj)
+ self.context_var = context_var
+
+ def render(self, context):
+ context[self.context_var] = \
+ Tag.objects.get_for_object(self.obj.resolve(context))
+ return ''
+
+class TaggedObjectsNode(Node):
+ def __init__(self, tag, model, context_var):
+ self.tag = Variable(tag)
+ self.context_var = context_var
+ self.model = model
+
+ def render(self, context):
+ model = get_model(*self.model.split('.'))
+ if model is None:
+ raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model)
+ context[self.context_var] = \
+ TaggedItem.objects.get_by_model(model, self.tag.resolve(context))
+ return ''
+
+def do_tags_for_model(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects associated with a given model
+ and stores them in a context variable.
+
+ Usage::
+
+ {% tags_for_model [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ Extended usage::
+
+ {% tags_for_model [model] as [varname] with counts %}
+
+ If specified - by providing extra ``with counts`` arguments - adds
+ a ``count`` attribute to each tag containing the number of
+ instances of the given model which have been tagged with it.
+
+ Examples::
+
+ {% tags_for_model products.Widget as widget_tags %}
+ {% tags_for_model products.Widget as widget_tags with counts %}
+
+ """
+ bits = token.contents.split()
+ len_bits = len(bits)
+ if len_bits not in (4, 6):
+ raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ if len_bits == 6:
+ if bits[4] != 'with':
+ raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
+ if bits[5] != 'counts':
+ raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0])
+ if len_bits == 4:
+ return TagsForModelNode(bits[1], bits[3], counts=False)
+ else:
+ return TagsForModelNode(bits[1], bits[3], counts=True)
+
+def do_tag_cloud_for_model(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects for a given model, with tag
+ cloud attributes set, and stores them in a context variable.
+
+ Usage::
+
+ {% tag_cloud_for_model [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ Extended usage::
+
+ {% tag_cloud_for_model [model] as [varname] with [options] %}
+
+ Extra options can be provided after an optional ``with`` argument,
+ with each option being specified in ``[name]=[value]`` format. Valid
+ extra options are:
+
+ ``steps``
+ Integer. Defines the range of font sizes.
+
+ ``min_count``
+ Integer. Defines the minimum number of times a tag must have
+ been used to appear in the cloud.
+
+ ``distribution``
+ One of ``linear`` or ``log``. Defines the font-size
+ distribution algorithm to use when generating the tag cloud.
+
+ Examples::
+
+ {% tag_cloud_for_model products.Widget as widget_tags %}
+ {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
+
+ """
+ bits = token.contents.split()
+ len_bits = len(bits)
+ if len_bits != 4 and len_bits not in range(6, 9):
+ raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ kwargs = {}
+ if len_bits > 5:
+ if bits[4] != 'with':
+ raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
+ for i in range(5, len_bits):
+ try:
+ name, value = bits[i].split('=')
+ if name == 'steps' or name == 'min_count':
+ try:
+ kwargs[str(name)] = int(value)
+ except ValueError:
+ raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ 'value': value,
+ })
+ elif name == 'distribution':
+ if value in ['linear', 'log']:
+ kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value]
+ else:
+ raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ 'value': value,
+ })
+ else:
+ raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % {
+ 'tag': bits[0],
+ 'option': name,
+ })
+ except ValueError:
+ raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % {
+ 'tag': bits[0],
+ 'option': bits[i],
+ })
+ return TagCloudForModelNode(bits[1], bits[3], **kwargs)
+
+def do_tags_for_object(parser, token):
+ """
+ Retrieves a list of ``Tag`` objects associated with an object and
+ stores them in a context variable.
+
+ Usage::
+
+ {% tags_for_object [object] as [varname] %}
+
+ Example::
+
+ {% tags_for_object foo_object as tag_list %}
+ """
+ bits = token.contents.split()
+ if len(bits) != 4:
+ raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0])
+ if bits[2] != 'as':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
+ return TagsForObjectNode(bits[1], bits[3])
+
+def do_tagged_objects(parser, token):
+ """
+ Retrieves a list of instances of a given model which are tagged with
+ a given ``Tag`` and stores them in a context variable.
+
+ Usage::
+
+ {% tagged_objects [tag] in [model] as [varname] %}
+
+ The model is specified in ``[appname].[modelname]`` format.
+
+ The tag must be an instance of a ``Tag``, not the name of a tag.
+
+ Example::
+
+ {% tagged_objects comedy_tag in tv.Show as comedies %}
+
+ """
+ bits = token.contents.split()
+ if len(bits) != 6:
+ raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0])
+ if bits[2] != 'in':
+ raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0])
+ if bits[4] != 'as':
+ raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0])
+ return TaggedObjectsNode(bits[1], bits[3], bits[5])
+
+register.tag('tags_for_model', do_tags_for_model)
+register.tag('tag_cloud_for_model', do_tag_cloud_for_model)
+register.tag('tags_for_object', do_tags_for_object)
+register.tag('tagged_objects', do_tagged_objects)
diff --git a/tagging/tests/.svn/all-wcprops b/tagging/tests/.svn/all-wcprops
new file mode 100644
index 0000000..1baeebb
--- /dev/null
+++ b/tagging/tests/.svn/all-wcprops
@@ -0,0 +1,35 @@
+K 25
+svn:wc:ra_dav:version-url
+V 37
+/svn/!svn/ver/137/trunk/tagging/tests
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 48
+/svn/!svn/ver/93/trunk/tagging/tests/__init__.py
+END
+settings.py
+K 25
+svn:wc:ra_dav:version-url
+V 49
+/svn/!svn/ver/137/trunk/tagging/tests/settings.py
+END
+tests.py
+K 25
+svn:wc:ra_dav:version-url
+V 46
+/svn/!svn/ver/136/trunk/tagging/tests/tests.py
+END
+models.py
+K 25
+svn:wc:ra_dav:version-url
+V 47
+/svn/!svn/ver/134/trunk/tagging/tests/models.py
+END
+tags.txt
+K 25
+svn:wc:ra_dav:version-url
+V 45
+/svn/!svn/ver/93/trunk/tagging/tests/tags.txt
+END
diff --git a/tagging/tests/.svn/dir-prop-base b/tagging/tests/.svn/dir-prop-base
new file mode 100644
index 0000000..4cc643b
--- /dev/null
+++ b/tagging/tests/.svn/dir-prop-base
@@ -0,0 +1,6 @@
+K 10
+svn:ignore
+V 6
+*.pyc
+
+END
diff --git a/tagging/tests/.svn/entries b/tagging/tests/.svn/entries
new file mode 100644
index 0000000..c5dbe87
--- /dev/null
+++ b/tagging/tests/.svn/entries
@@ -0,0 +1,198 @@
+9
+
+dir
+147
+http://django-tagging.googlecode.com/svn/trunk/tagging/tests
+http://django-tagging.googlecode.com/svn
+
+
+
+2008-05-04T20:49:27.458486Z
+137
+jonathan.buchanan
+has-props
+
+svn:special svn:externals svn:needs-lock
+
+
+
+
+
+
+
+
+
+
+
+83e7428b-ec2a-0410-86f2-bf466d0e5e72
+
+__init__.py
+file
+
+
+
+
+2008-08-21T18:05:39.000000Z
+d41d8cd98f00b204e9800998ecf8427e
+2007-08-20T09:19:55.193228Z
+93
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+0
+
+settings.py
+file
+
+
+
+
+2008-08-21T18:05:39.000000Z
+26ca4cbc5221da0059baefad5fffee9a
+2008-05-04T20:49:27.458486Z
+137
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+592
+
+tests.py
+file
+
+
+
+
+2008-08-21T18:05:39.000000Z
+20371aa8a5c90be3db9309a777756a52
+2008-05-04T20:48:43.707358Z
+136
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+18675
+
+models.py
+file
+
+
+
+
+2008-08-21T18:05:39.000000Z
+25cb3f5db9701c5688534721a2d5e9ab
+2008-04-30T22:40:28.286321Z
+134
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+811
+
+tags.txt
+file
+
+
+
+
+2008-08-21T18:05:39.000000Z
+d4923e968e0692eac4758b364dba5c44
+2007-08-20T09:19:55.193228Z
+93
+jonathan.buchanan
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1382
+
diff --git a/tagging/tests/.svn/format b/tagging/tests/.svn/format
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/tagging/tests/.svn/format
@@ -0,0 +1 @@
+9
diff --git a/tagging/tests/.svn/prop-base/__init__.py.svn-base b/tagging/tests/.svn/prop-base/__init__.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/tests/.svn/prop-base/__init__.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/tests/.svn/prop-base/models.py.svn-base b/tagging/tests/.svn/prop-base/models.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/tests/.svn/prop-base/models.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/tests/.svn/prop-base/settings.py.svn-base b/tagging/tests/.svn/prop-base/settings.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/tests/.svn/prop-base/settings.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/tests/.svn/prop-base/tags.txt.svn-base b/tagging/tests/.svn/prop-base/tags.txt.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/tests/.svn/prop-base/tags.txt.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/tests/.svn/prop-base/tests.py.svn-base b/tagging/tests/.svn/prop-base/tests.py.svn-base
new file mode 100644
index 0000000..bdbd305
--- /dev/null
+++ b/tagging/tests/.svn/prop-base/tests.py.svn-base
@@ -0,0 +1,5 @@
+K 13
+svn:eol-style
+V 6
+native
+END
diff --git a/tagging/tests/.svn/text-base/__init__.py.svn-base b/tagging/tests/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tagging/tests/.svn/text-base/__init__.py.svn-base
diff --git a/tagging/tests/.svn/text-base/models.py.svn-base b/tagging/tests/.svn/text-base/models.py.svn-base
new file mode 100644
index 0000000..0708686
--- /dev/null
+++ b/tagging/tests/.svn/text-base/models.py.svn-base
@@ -0,0 +1,38 @@
+from django.db import models
+
+from tagging.fields import TagField
+
+class Perch(models.Model):
+ size = models.IntegerField()
+ smelly = models.BooleanField(default=True)
+
+class Parrot(models.Model):
+ state = models.CharField(max_length=50)
+ perch = models.ForeignKey(Perch, null=True)
+
+ def __unicode__(self):
+ return self.state
+
+ class Meta:
+ ordering = ['state']
+
+class Link(models.Model):
+ name = models.CharField(max_length=50)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class Article(models.Model):
+ name = models.CharField(max_length=50)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class FormTest(models.Model):
+ tags = TagField('Test', help_text='Test')
diff --git a/tagging/tests/.svn/text-base/settings.py.svn-base b/tagging/tests/.svn/text-base/settings.py.svn-base
new file mode 100644
index 0000000..26e659b
--- /dev/null
+++ b/tagging/tests/.svn/text-base/settings.py.svn-base
@@ -0,0 +1,27 @@
+import os
+DIRNAME = os.path.dirname(__file__)
+
+DEFAULT_CHARSET = 'utf-8'
+
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db')
+
+#DATABASE_ENGINE = 'mysql'
+#DATABASE_NAME = 'tagging_test'
+#DATABASE_USER = 'root'
+#DATABASE_PASSWORD = ''
+#DATABASE_HOST = 'localhost'
+#DATABASE_PORT = '3306'
+
+#DATABASE_ENGINE = 'postgresql_psycopg2'
+#DATABASE_NAME = 'tagging_test'
+#DATABASE_USER = 'postgres'
+#DATABASE_PASSWORD = ''
+#DATABASE_HOST = 'localhost'
+#DATABASE_PORT = '5432'
+
+INSTALLED_APPS = (
+ 'django.contrib.contenttypes',
+ 'tagging',
+ 'tagging.tests',
+)
diff --git a/tagging/tests/.svn/text-base/tags.txt.svn-base b/tagging/tests/.svn/text-base/tags.txt.svn-base
new file mode 100644
index 0000000..8543411
--- /dev/null
+++ b/tagging/tests/.svn/text-base/tags.txt.svn-base
@@ -0,0 +1,122 @@
+NewMedia 53
+Website 45
+PR 44
+Status 44
+Collaboration 41
+Drupal 34
+Journalism 31
+Transparency 30
+Theory 29
+Decentralization 25
+EchoChamberProject 24
+OpenSource 23
+Film 22
+Blog 21
+Interview 21
+Political 21
+Worldview 21
+Communications 19
+Conference 19
+Folksonomy 15
+MediaCriticism 15
+Volunteer 15
+Dialogue 13
+InternationalLaw 13
+Rosen 12
+Evolution 11
+KentBye 11
+Objectivity 11
+Plante 11
+ToDo 11
+Advisor 10
+Civics 10
+Roadmap 10
+Wilber 9
+About 8
+CivicSpace 8
+Ecosystem 8
+Choice 7
+Murphy 7
+Sociology 7
+ACH 6
+del.icio.us 6
+IntelligenceAnalysis 6
+Science 6
+Credibility 5
+Distribution 5
+Diversity 5
+Errors 5
+FinalCutPro 5
+Fundraising 5
+Law 5
+PhilosophyofScience 5
+Podcast 5
+PoliticalBias 5
+Activism 4
+Analysis 4
+CBS 4
+DeceptionDetection 4
+Editing 4
+History 4
+RSS 4
+Social 4
+Subjectivity 4
+Vlog 4
+ABC 3
+ALTubes 3
+Economics 3
+FCC 3
+NYT 3
+Sirota 3
+Sundance 3
+Training 3
+Wiki 3
+XML 3
+Borger 2
+Brody 2
+Deliberation 2
+EcoVillage 2
+Identity 2
+LAMP 2
+Lobe 2
+Maine 2
+May 2
+MediaLogic 2
+Metaphor 2
+Mitchell 2
+NBC 2
+OHanlon 2
+Psychology 2
+Queen 2
+Software 2
+SpiralDynamics 2
+Strobel 2
+Sustainability 2
+Transcripts 2
+Brown 1
+Buddhism 1
+Community 1
+DigitalDivide 1
+Donnelly 1
+Education 1
+FairUse 1
+FireANT 1
+Google 1
+HumanRights 1
+KM 1
+Kwiatkowski 1
+Landay 1
+Loiseau 1
+Math 1
+Music 1
+Nature 1
+Schechter 1
+Screencast 1
+Sivaraksa 1
+Skype 1
+SocialCapital 1
+TagCloud 1
+Thielmann 1
+Thomas 1
+Tiger 1
+Wedgwood 1 \ No newline at end of file
diff --git a/tagging/tests/.svn/text-base/tests.py.svn-base b/tagging/tests/.svn/text-base/tests.py.svn-base
new file mode 100644
index 0000000..574fa92
--- /dev/null
+++ b/tagging/tests/.svn/text-base/tests.py.svn-base
@@ -0,0 +1,495 @@
+# -*- coding: utf-8 -*-
+tests = r"""
+>>> import os
+>>> from django import newforms as forms
+>>> from tagging.forms import TagField
+>>> from tagging import settings
+>>> from tagging.models import Tag, TaggedItem
+>>> from tagging.tests.models import Article, Link, Perch, Parrot, FormTest
+>>> from tagging.utils import calculate_cloud, get_tag_list, get_tag, parse_tag_input
+>>> from tagging.utils import LINEAR
+>>> from tagging.validators import isTagList, isTag
+
+#############
+# Utilities #
+#############
+
+# Tag input ###################################################################
+
+# Simple space-delimited tags
+>>> parse_tag_input('one')
+[u'one']
+>>> parse_tag_input('one two')
+[u'one', u'two']
+>>> parse_tag_input('one two three')
+[u'one', u'three', u'two']
+>>> parse_tag_input('one one two two')
+[u'one', u'two']
+
+# Comma-delimited multiple words - an unquoted comma in the input will trigger
+# this.
+>>> parse_tag_input(',one')
+[u'one']
+>>> parse_tag_input(',one two')
+[u'one two']
+>>> parse_tag_input(',one two three')
+[u'one two three']
+>>> parse_tag_input('a-one, a-two and a-three')
+[u'a-one', u'a-two and a-three']
+
+# Double-quoted multiple words - a completed quote will trigger this.
+# Unclosed quotes are ignored.
+>>> parse_tag_input('"one')
+[u'one']
+>>> parse_tag_input('"one two')
+[u'one', u'two']
+>>> parse_tag_input('"one two three')
+[u'one', u'three', u'two']
+>>> parse_tag_input('"one two"')
+[u'one two']
+>>> parse_tag_input('a-one "a-two and a-three"')
+[u'a-one', u'a-two and a-three']
+
+# No loose commas - split on spaces
+>>> parse_tag_input('one two "thr,ee"')
+[u'one', u'thr,ee', u'two']
+
+# Loose commas - split on commas
+>>> parse_tag_input('"one", two three')
+[u'one', u'two three']
+
+# Double quotes can contain commas
+>>> parse_tag_input('a-one "a-two, and a-three"')
+[u'a-one', u'a-two, and a-three']
+>>> parse_tag_input('"two", one, one, two, "one"')
+[u'one', u'two']
+
+# Bad users! Naughty users!
+>>> parse_tag_input(None)
+[]
+>>> parse_tag_input('')
+[]
+>>> parse_tag_input('"')
+[]
+>>> parse_tag_input('""')
+[]
+>>> parse_tag_input('"' * 7)
+[]
+>>> parse_tag_input(',,,,,,')
+[]
+>>> parse_tag_input('",",",",",",","')
+[u',']
+>>> parse_tag_input('a-one "a-two" and "a-three')
+[u'a-one', u'a-three', u'a-two', u'and']
+
+# Normalised Tag list input ###################################################
+>>> cheese = Tag.objects.create(name='cheese')
+>>> toast = Tag.objects.create(name='toast')
+>>> get_tag_list(cheese)
+[<Tag: cheese>]
+>>> get_tag_list('cheese toast')
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list('cheese,toast')
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([])
+[]
+>>> get_tag_list(['cheese', 'toast'])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([cheese.id, toast.id])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ'])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([cheese, toast])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list((cheese, toast))
+(<Tag: cheese>, <Tag: toast>)
+>>> get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast']))
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list(['cheese', toast])
+Traceback (most recent call last):
+ ...
+ValueError: If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.
+>>> get_tag_list(29)
+Traceback (most recent call last):
+ ...
+ValueError: The tag input given was invalid.
+
+# Normalised Tag input
+>>> get_tag(cheese)
+<Tag: cheese>
+>>> get_tag('cheese')
+<Tag: cheese>
+>>> get_tag(cheese.id)
+<Tag: cheese>
+>>> get_tag('mouse')
+
+# Tag clouds ##################################################################
+>>> tags = []
+>>> for line in open(os.path.join(os.path.dirname(__file__), 'tags.txt')).readlines():
+... name, count = line.rstrip().split()
+... tag = Tag(name=name)
+... tag.count = int(count)
+... tags.append(tag)
+
+>>> sizes = {}
+>>> for tag in calculate_cloud(tags, steps=5):
+... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
+
+# This isn't a pre-calculated test, just making sure it's consistent
+>>> sizes
+{1: 48, 2: 30, 3: 19, 4: 15, 5: 10}
+
+>>> sizes = {}
+>>> for tag in calculate_cloud(tags, steps=5, distribution=LINEAR):
+... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
+
+# This isn't a pre-calculated test, just making sure it's consistent
+>>> sizes
+{1: 97, 2: 12, 3: 7, 4: 2, 5: 4}
+
+>>> calculate_cloud(tags, steps=5, distribution='cheese')
+Traceback (most recent call last):
+ ...
+ValueError: Invalid distribution algorithm specified: cheese.
+
+# Validators ##################################################################
+
+>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+
+>>> isTag('"test"', {})
+>>> isTag(',test', {})
+>>> isTag('f o o', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Multiple tags were given.']
+>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+
+###########
+# Tagging #
+###########
+
+# Basic tagging ###############################################################
+
+>>> dead = Parrot.objects.create(state='dead')
+>>> Tag.objects.update_tags(dead, 'foo,bar,"ter"')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: foo>, <Tag: ter>]
+>>> Tag.objects.update_tags(dead, '"foo" bar "baz"')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'foo')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'zip')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
+>>> Tag.objects.add_tag(dead, ' ')
+Traceback (most recent call last):
+ ...
+AttributeError: No tags were given: " ".
+>>> Tag.objects.add_tag(dead, 'one two')
+Traceback (most recent call last):
+ ...
+AttributeError: Multiple tags were given: "one two".
+
+# Note that doctest in Python 2.4 (and maybe 2.5?) doesn't support non-ascii
+# characters in output, so we're displaying the repr() here.
+>>> Tag.objects.update_tags(dead, 'ŠĐĆŽćžšđ')
+>>> repr(Tag.objects.get_for_object(dead))
+'[<Tag: \xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91>]'
+
+>>> Tag.objects.update_tags(dead, None)
+>>> Tag.objects.get_for_object(dead)
+[]
+
+# Using a model's TagField
+>>> f1 = FormTest.objects.create(tags=u'test3 test2 test1')
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test1>, <Tag: test2>, <Tag: test3>]
+>>> f1.tags = u'test4'
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test4>]
+>>> f1.tags = ''
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[]
+
+# Forcing tags to lowercase
+>>> settings.FORCE_LOWERCASE_TAGS = True
+>>> Tag.objects.update_tags(dead, 'foO bAr Ter')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: foo>, <Tag: ter>]
+>>> Tag.objects.update_tags(dead, 'foO bAr baZ')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'FOO')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'Zip')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
+>>> Tag.objects.update_tags(dead, None)
+>>> f1.tags = u'TEST5'
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test5>]
+>>> f1.tags
+u'test5'
+
+# Retrieving tags by Model ####################################################
+
+>>> Tag.objects.usage_for_model(Parrot)
+[]
+>>> parrot_details = (
+... ('pining for the fjords', 9, True, 'foo bar'),
+... ('passed on', 6, False, 'bar baz ter'),
+... ('no more', 4, True, 'foo ter'),
+... ('late', 2, False, 'bar ter'),
+... )
+
+>>> for state, perch_size, perch_smelly, tags in parrot_details:
+... perch = Perch.objects.create(size=perch_size, smelly=perch_smelly)
+... parrot = Parrot.objects.create(state=state, perch=perch)
+... Tag.objects.update_tags(parrot, tags)
+
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True)]
+[(u'bar', 3), (u'baz', 1), (u'foo', 2), (u'ter', 3)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2)]
+[(u'bar', 3), (u'foo', 2), (u'ter', 3)]
+
+# Limiting results to a subset of the model
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more'))]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p'))]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4))]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True))]
+[(u'bar', 1), (u'foo', 2), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True))]
+[(u'foo', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4))]
+[(u'bar', False), (u'baz', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99))]
+[]
+
+# Related tags
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True)]
+[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2)]
+[(u'ter', 2)]
+>>> [tag.name for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False)]
+[u'baz', u'foo', u'ter']
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True)]
+[(u'baz', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True)]
+[]
+
+# Once again, with feeling (strings)
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, counts=True)]
+[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, min_count=2)]
+[(u'ter', 2)]
+>>> [tag.name for tag in Tag.objects.related_for_model('bar', Parrot, counts=False)]
+[u'baz', u'foo', u'ter']
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True)]
+[(u'baz', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True)]
+[]
+
+# Retrieving tagged objects by Model ##########################################
+
+>>> foo = Tag.objects.get(name='foo')
+>>> bar = Tag.objects.get(name='bar')
+>>> baz = Tag.objects.get(name='baz')
+>>> ter = Tag.objects.get(name='ter')
+>>> TaggedItem.objects.get_by_model(Parrot, foo)
+[<Parrot: no more>, <Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, bar)
+[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+
+# Intersections are supported
+>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz])
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar])
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter])
+[<Parrot: late>, <Parrot: passed on>]
+
+# Issue 114 - Intersection with non-existant tags
+>>> TaggedItem.objects.get_intersection_by_model(Parrot, [])
+[]
+
+# You can also pass Tag QuerySets
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz']))
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar']))
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter']))
+[<Parrot: late>, <Parrot: passed on>]
+
+# You can also pass strings and lists of strings
+>>> TaggedItem.objects.get_by_model(Parrot, 'foo baz')
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, 'foo bar')
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter')
+[<Parrot: late>, <Parrot: passed on>]
+>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz'])
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar'])
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter'])
+[<Parrot: late>, <Parrot: passed on>]
+
+# Issue 50 - Get by non-existent tag
+>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons')
+[]
+
+# Unions
+>>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter'])
+[<Parrot: late>, <Parrot: no more>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz'])
+[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+
+# Issue 114 - Union with non-existant tags
+>>> TaggedItem.objects.get_union_by_model(Parrot, [])
+[]
+
+# Retrieving related objects by Model #########################################
+
+# Related instances of the same Model
+>>> l1 = Link.objects.create(name='link 1')
+>>> Tag.objects.update_tags(l1, 'tag1 tag2 tag3 tag4 tag5')
+>>> l2 = Link.objects.create(name='link 2')
+>>> Tag.objects.update_tags(l2, 'tag1 tag2 tag3')
+>>> l3 = Link.objects.create(name='link 3')
+>>> Tag.objects.update_tags(l3, 'tag1')
+>>> l4 = Link.objects.create(name='link 4')
+>>> TaggedItem.objects.get_related(l1, Link)
+[<Link: link 2>, <Link: link 3>]
+>>> TaggedItem.objects.get_related(l1, Link, num=1)
+[<Link: link 2>]
+>>> TaggedItem.objects.get_related(l4, Link)
+[]
+
+# Limit related items
+>>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3'))
+[<Link: link 2>]
+
+# Related instance of a different Model
+>>> a1 = Article.objects.create(name='article 1')
+>>> Tag.objects.update_tags(a1, 'tag1 tag2 tag3 tag4')
+>>> TaggedItem.objects.get_related(a1, Link)
+[<Link: link 1>, <Link: link 2>, <Link: link 3>]
+>>> Tag.objects.update_tags(a1, 'tag6')
+>>> TaggedItem.objects.get_related(a1, Link)
+[]
+
+################
+# Model Fields #
+################
+
+# TagField ####################################################################
+
+# Ensure that automatically created forms use TagField
+>>> class TestForm(forms.ModelForm):
+... class Meta:
+... model = FormTest
+>>> form = TestForm()
+>>> form.fields['tags'].__class__.__name__
+'TagField'
+
+# Recreating string representaions of tag lists ###############################
+>>> plain = Tag.objects.create(name='plain')
+>>> spaces = Tag.objects.create(name='spa ces')
+>>> comma = Tag.objects.create(name='com,ma')
+
+>>> from tagging.utils import edit_string_for_tags
+>>> edit_string_for_tags([plain])
+u'plain'
+>>> edit_string_for_tags([plain, spaces])
+u'plain, spa ces'
+>>> edit_string_for_tags([plain, spaces, comma])
+u'plain, spa ces, "com,ma"'
+>>> edit_string_for_tags([plain, comma])
+u'plain "com,ma"'
+>>> edit_string_for_tags([comma, spaces])
+u'"com,ma", spa ces'
+
+###############
+# Form Fields #
+###############
+
+>>> t = TagField()
+>>> t.clean('foo')
+u'foo'
+>>> t.clean('foo bar baz')
+u'foo bar baz'
+>>> t.clean('foo,bar,baz')
+u'foo,bar,baz'
+>>> t.clean('foo, bar, baz')
+u'foo, bar, baz'
+>>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar')
+u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'
+>>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar')
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+"""
+
+tests_pre_qsrf = tests + r"""
+# Limiting results to a queryset
+>>> Tag.objects.usage_for_queryset(Parrot.objects.filter())
+Traceback (most recent call last):
+ ...
+AttributeError: 'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.
+"""
+
+tests_post_qsrf = tests + r"""
+>>> from django.db.models import Q
+
+# Limiting results to a queryset
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(state='no more'), counts=True)]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(state__startswith='p'), counts=True)]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4), counts=True)]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), counts=True)]
+[(u'bar', 1), (u'foo', 2), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), min_count=2)]
+[(u'foo', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4))]
+[(u'bar', False), (u'baz', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=99))]
+[]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), counts=True)]
+[(u'bar', 2), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), min_count=2)]
+[(u'bar', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')))]
+[(u'bar', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(state='passed on'), counts=True)]
+[(u'bar', 2), (u'foo', 2), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(state__startswith='p'), min_count=2)]
+[(u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(Q(perch__size__gt=6) | Q(perch__smelly=False)), counts=True)]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(perch__smelly=True).filter(state__startswith='l'), counts=True)]
+[(u'bar', 1), (u'ter', 1)]
+"""
+
+try:
+ from django.db.models.query import parse_lookup
+except ImportError:
+ __test__ = {'post-qsrf': tests_post_qsrf}
+else:
+ __test__ = {'pre-qsrf': tests_pre_qsrf}
diff --git a/tagging/tests/models.py b/tagging/tests/models.py
index 69a5c7b..0708686 100644
--- a/tagging/tests/models.py
+++ b/tagging/tests/models.py
@@ -1,38 +1,38 @@
-from django.db import models
-
-from tagging.fields import TagField
-
-class Perch(models.Model):
- size = models.IntegerField()
- smelly = models.BooleanField(default=True)
-
-class Parrot(models.Model):
- state = models.CharField(max_length=50)
- perch = models.ForeignKey(Perch, null=True)
-
- def __unicode__(self):
- return self.state
-
- class Meta:
- ordering = ['state']
-
-class Link(models.Model):
- name = models.CharField(max_length=50)
-
- def __unicode__(self):
- return self.name
-
- class Meta:
- ordering = ['name']
-
-class Article(models.Model):
- name = models.CharField(max_length=50)
-
- def __unicode__(self):
- return self.name
-
- class Meta:
- ordering = ['name']
-
-class FormTest(models.Model):
- tags = TagField()
+from django.db import models
+
+from tagging.fields import TagField
+
+class Perch(models.Model):
+ size = models.IntegerField()
+ smelly = models.BooleanField(default=True)
+
+class Parrot(models.Model):
+ state = models.CharField(max_length=50)
+ perch = models.ForeignKey(Perch, null=True)
+
+ def __unicode__(self):
+ return self.state
+
+ class Meta:
+ ordering = ['state']
+
+class Link(models.Model):
+ name = models.CharField(max_length=50)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class Article(models.Model):
+ name = models.CharField(max_length=50)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ['name']
+
+class FormTest(models.Model):
+ tags = TagField('Test', help_text='Test')
diff --git a/tagging/tests/runtests.py b/tagging/tests/runtests.py
deleted file mode 100644
index e66ee86..0000000
--- a/tagging/tests/runtests.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import os, sys
-os.environ['DJANGO_SETTINGS_MODULE'] = 'tagging.tests.settings'
-
-from django.test.simple import run_tests
-
-failures = run_tests(None, verbosity=9)
-if failures:
- sys.exit(failures)
diff --git a/tagging/tests/settings.py b/tagging/tests/settings.py
index 93ba4b0..26e659b 100644
--- a/tagging/tests/settings.py
+++ b/tagging/tests/settings.py
@@ -1,27 +1,27 @@
-import os
-DIRNAME = os.path.dirname(__file__)
-
-DEFAULT_CHARSET = 'utf-8'
-
-DATABASE_ENGINE = 'sqlite3'
-DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db')
-
-#DATABASE_ENGINE = 'mysql'
-#DATABASE_NAME = 'tagging_test'
-#DATABASE_USER = 'root'
-#DATABASE_PASSWORD = ''
-#DATABASE_HOST = 'localhost'
-#DATABASE_PORT = '3306'
-
-#DATABASE_ENGINE = 'postgresql_psycopg2'
-#DATABASE_NAME = 'tagging_test'
-#DATABASE_USER = 'postgres'
-#DATABASE_PASSWORD = ''
-#DATABASE_HOST = 'localhost'
-#DATABASE_PORT = '5432'
-
-INSTALLED_APPS = (
- 'django.contrib.contenttypes',
- 'tagging',
- 'tagging.tests',
-)
+import os
+DIRNAME = os.path.dirname(__file__)
+
+DEFAULT_CHARSET = 'utf-8'
+
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db')
+
+#DATABASE_ENGINE = 'mysql'
+#DATABASE_NAME = 'tagging_test'
+#DATABASE_USER = 'root'
+#DATABASE_PASSWORD = ''
+#DATABASE_HOST = 'localhost'
+#DATABASE_PORT = '3306'
+
+#DATABASE_ENGINE = 'postgresql_psycopg2'
+#DATABASE_NAME = 'tagging_test'
+#DATABASE_USER = 'postgres'
+#DATABASE_PASSWORD = ''
+#DATABASE_HOST = 'localhost'
+#DATABASE_PORT = '5432'
+
+INSTALLED_APPS = (
+ 'django.contrib.contenttypes',
+ 'tagging',
+ 'tagging.tests',
+)
diff --git a/tagging/tests/tags.txt b/tagging/tests/tags.txt
index 61bfd9e..8543411 100644
--- a/tagging/tests/tags.txt
+++ b/tagging/tests/tags.txt
@@ -1,122 +1,122 @@
-NewMedia 53
-Website 45
-PR 44
-Status 44
-Collaboration 41
-Drupal 34
-Journalism 31
-Transparency 30
-Theory 29
-Decentralization 25
-EchoChamberProject 24
-OpenSource 23
-Film 22
-Blog 21
-Interview 21
-Political 21
-Worldview 21
-Communications 19
-Conference 19
-Folksonomy 15
-MediaCriticism 15
-Volunteer 15
-Dialogue 13
-InternationalLaw 13
-Rosen 12
-Evolution 11
-KentBye 11
-Objectivity 11
-Plante 11
-ToDo 11
-Advisor 10
-Civics 10
-Roadmap 10
-Wilber 9
-About 8
-CivicSpace 8
-Ecosystem 8
-Choice 7
-Murphy 7
-Sociology 7
-ACH 6
-del.icio.us 6
-IntelligenceAnalysis 6
-Science 6
-Credibility 5
-Distribution 5
-Diversity 5
-Errors 5
-FinalCutPro 5
-Fundraising 5
-Law 5
-PhilosophyofScience 5
-Podcast 5
-PoliticalBias 5
-Activism 4
-Analysis 4
-CBS 4
-DeceptionDetection 4
-Editing 4
-History 4
-RSS 4
-Social 4
-Subjectivity 4
-Vlog 4
-ABC 3
-ALTubes 3
-Economics 3
-FCC 3
-NYT 3
-Sirota 3
-Sundance 3
-Training 3
-Wiki 3
-XML 3
-Borger 2
-Brody 2
-Deliberation 2
-EcoVillage 2
-Identity 2
-LAMP 2
-Lobe 2
-Maine 2
-May 2
-MediaLogic 2
-Metaphor 2
-Mitchell 2
-NBC 2
-OHanlon 2
-Psychology 2
-Queen 2
-Software 2
-SpiralDynamics 2
-Strobel 2
-Sustainability 2
-Transcripts 2
-Brown 1
-Buddhism 1
-Community 1
-DigitalDivide 1
-Donnelly 1
-Education 1
-FairUse 1
-FireANT 1
-Google 1
-HumanRights 1
-KM 1
-Kwiatkowski 1
-Landay 1
-Loiseau 1
-Math 1
-Music 1
-Nature 1
-Schechter 1
-Screencast 1
-Sivaraksa 1
-Skype 1
-SocialCapital 1
-TagCloud 1
-Thielmann 1
-Thomas 1
-Tiger 1
+NewMedia 53
+Website 45
+PR 44
+Status 44
+Collaboration 41
+Drupal 34
+Journalism 31
+Transparency 30
+Theory 29
+Decentralization 25
+EchoChamberProject 24
+OpenSource 23
+Film 22
+Blog 21
+Interview 21
+Political 21
+Worldview 21
+Communications 19
+Conference 19
+Folksonomy 15
+MediaCriticism 15
+Volunteer 15
+Dialogue 13
+InternationalLaw 13
+Rosen 12
+Evolution 11
+KentBye 11
+Objectivity 11
+Plante 11
+ToDo 11
+Advisor 10
+Civics 10
+Roadmap 10
+Wilber 9
+About 8
+CivicSpace 8
+Ecosystem 8
+Choice 7
+Murphy 7
+Sociology 7
+ACH 6
+del.icio.us 6
+IntelligenceAnalysis 6
+Science 6
+Credibility 5
+Distribution 5
+Diversity 5
+Errors 5
+FinalCutPro 5
+Fundraising 5
+Law 5
+PhilosophyofScience 5
+Podcast 5
+PoliticalBias 5
+Activism 4
+Analysis 4
+CBS 4
+DeceptionDetection 4
+Editing 4
+History 4
+RSS 4
+Social 4
+Subjectivity 4
+Vlog 4
+ABC 3
+ALTubes 3
+Economics 3
+FCC 3
+NYT 3
+Sirota 3
+Sundance 3
+Training 3
+Wiki 3
+XML 3
+Borger 2
+Brody 2
+Deliberation 2
+EcoVillage 2
+Identity 2
+LAMP 2
+Lobe 2
+Maine 2
+May 2
+MediaLogic 2
+Metaphor 2
+Mitchell 2
+NBC 2
+OHanlon 2
+Psychology 2
+Queen 2
+Software 2
+SpiralDynamics 2
+Strobel 2
+Sustainability 2
+Transcripts 2
+Brown 1
+Buddhism 1
+Community 1
+DigitalDivide 1
+Donnelly 1
+Education 1
+FairUse 1
+FireANT 1
+Google 1
+HumanRights 1
+KM 1
+Kwiatkowski 1
+Landay 1
+Loiseau 1
+Math 1
+Music 1
+Nature 1
+Schechter 1
+Screencast 1
+Sivaraksa 1
+Skype 1
+SocialCapital 1
+TagCloud 1
+Thielmann 1
+Thomas 1
+Tiger 1
Wedgwood 1 \ No newline at end of file
diff --git a/tagging/tests/tests.py b/tagging/tests/tests.py
index b1c8032..574fa92 100644
--- a/tagging/tests/tests.py
+++ b/tagging/tests/tests.py
@@ -1,434 +1,495 @@
-# -*- coding: utf-8 -*-
-r"""
->>> import os
->>> from django import newforms as forms
->>> from tagging.forms import TagField
->>> from tagging import settings
->>> from tagging.models import Tag, TaggedItem
->>> from tagging.tests.models import Article, Link, Perch, Parrot, FormTest
->>> from tagging.utils import calculate_cloud, get_tag_list, get_tag, parse_tag_input
->>> from tagging.utils import LINEAR
->>> from tagging.validators import isTagList, isTag
-
-#############
-# Utilities #
-#############
-
-# Tag input ###################################################################
-
-# Simple space-delimited tags
->>> parse_tag_input('one')
-[u'one']
->>> parse_tag_input('one two')
-[u'one', u'two']
->>> parse_tag_input('one two three')
-[u'one', u'three', u'two']
->>> parse_tag_input('one one two two')
-[u'one', u'two']
-
-# Comma-delimited multiple words - an unquoted comma in the input will trigger
-# this.
->>> parse_tag_input(',one')
-[u'one']
->>> parse_tag_input(',one two')
-[u'one two']
->>> parse_tag_input(',one two three')
-[u'one two three']
->>> parse_tag_input('a-one, a-two and a-three')
-[u'a-one', u'a-two and a-three']
-
-# Double-quoted multiple words - a completed quote will trigger this.
-# Unclosed quotes are ignored.
->>> parse_tag_input('"one')
-[u'one']
->>> parse_tag_input('"one two')
-[u'one', u'two']
->>> parse_tag_input('"one two three')
-[u'one', u'three', u'two']
->>> parse_tag_input('"one two"')
-[u'one two']
->>> parse_tag_input('a-one "a-two and a-three"')
-[u'a-one', u'a-two and a-three']
-
-# No loose commas - split on spaces
->>> parse_tag_input('one two "thr,ee"')
-[u'one', u'thr,ee', u'two']
-
-# Loose commas - split on commas
->>> parse_tag_input('"one", two three')
-[u'one', u'two three']
-
-# Double quotes can contain commas
->>> parse_tag_input('a-one "a-two, and a-three"')
-[u'a-one', u'a-two, and a-three']
->>> parse_tag_input('"two", one, one, two, "one"')
-[u'one', u'two']
-
-# Bad users! Naughty users!
->>> parse_tag_input(None)
-[]
->>> parse_tag_input('')
-[]
->>> parse_tag_input('"')
-[]
->>> parse_tag_input('""')
-[]
->>> parse_tag_input('"' * 7)
-[]
->>> parse_tag_input(',,,,,,')
-[]
->>> parse_tag_input('",",",",",",","')
-[u',']
->>> parse_tag_input('a-one "a-two" and "a-three')
-[u'a-one', u'a-three', u'a-two', u'and']
-
-# Normalised Tag list input ###################################################
->>> cheese = Tag.objects.create(name='cheese')
->>> toast = Tag.objects.create(name='toast')
->>> get_tag_list(cheese)
-[<Tag: cheese>]
->>> get_tag_list('cheese toast')
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list('cheese,toast')
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list([])
-[]
->>> get_tag_list(['cheese', 'toast'])
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list([cheese.id, toast.id])
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ'])
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list([cheese, toast])
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list((cheese, toast))
-(<Tag: cheese>, <Tag: toast>)
->>> get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast']))
-[<Tag: cheese>, <Tag: toast>]
->>> get_tag_list(['cheese', toast])
-Traceback (most recent call last):
- ...
-ValueError: If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.
->>> get_tag_list(29)
-Traceback (most recent call last):
- ...
-ValueError: The tag input given was invalid.
-
-# Normalised Tag input
->>> get_tag(cheese)
-<Tag: cheese>
->>> get_tag('cheese')
-<Tag: cheese>
->>> get_tag(cheese.id)
-<Tag: cheese>
->>> get_tag('mouse')
-
-# Tag clouds ##################################################################
->>> tags = []
->>> for line in open(os.path.join(os.path.dirname(__file__), 'tags.txt')).readlines():
-... name, count = line.rstrip().split()
-... tag = Tag(name=name)
-... tag.count = int(count)
-... tags.append(tag)
-
->>> sizes = {}
->>> for tag in calculate_cloud(tags, steps=5):
-... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
-
-# This isn't a pre-calculated test, just making sure it's consistent
->>> sizes
-{1: 48, 2: 30, 3: 19, 4: 15, 5: 10}
-
->>> sizes = {}
->>> for tag in calculate_cloud(tags, steps=5, distribution=LINEAR):
-... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
-
-# This isn't a pre-calculated test, just making sure it's consistent
->>> sizes
-{1: 97, 2: 12, 3: 7, 4: 2, 5: 4}
-
->>> calculate_cloud(tags, steps=5, distribution='cheese')
-Traceback (most recent call last):
- ...
-ValueError: Invalid distribution algorithm specified: cheese.
-
-# Validators ##################################################################
-
->>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
-Traceback (most recent call last):
- ...
-ValidationError: [u'Each tag may be no more than 50 characters long.']
-
->>> isTag('"test"', {})
->>> isTag(',test', {})
->>> isTag('f o o', {})
-Traceback (most recent call last):
- ...
-ValidationError: [u'Multiple tags were given.']
->>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
-Traceback (most recent call last):
- ...
-ValidationError: [u'Each tag may be no more than 50 characters long.']
-
-###########
-# Tagging #
-###########
-
-# Basic tagging ###############################################################
-
->>> dead = Parrot.objects.create(state='dead')
->>> Tag.objects.update_tags(dead, 'foo,bar,"ter"')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: foo>, <Tag: ter>]
->>> Tag.objects.update_tags(dead, '"foo" bar "baz"')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>]
->>> Tag.objects.add_tag(dead, 'foo')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>]
->>> Tag.objects.add_tag(dead, 'zip')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
->>> Tag.objects.add_tag(dead, ' ')
-Traceback (most recent call last):
- ...
-AttributeError: No tags were given: " ".
->>> Tag.objects.add_tag(dead, 'one two')
-Traceback (most recent call last):
- ...
-AttributeError: Multiple tags were given: "one two".
-
-# Note that doctest in Python 2.4 (and maybe 2.5?) doesn't support non-ascii
-# characters in output, so we're displaying the repr() here.
->>> Tag.objects.update_tags(dead, 'ŠĐĆŽćžšđ')
->>> repr(Tag.objects.get_for_object(dead))
-'[<Tag: \xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91>]'
-
->>> Tag.objects.update_tags(dead, None)
->>> Tag.objects.get_for_object(dead)
-[]
-
-# Using a model's TagField
->>> f1 = FormTest.objects.create(tags=u'test3 test2 test1')
->>> Tag.objects.get_for_object(f1)
-[<Tag: test1>, <Tag: test2>, <Tag: test3>]
->>> f1.tags = u'test4'
->>> f1.save()
->>> Tag.objects.get_for_object(f1)
-[<Tag: test4>]
->>> f1.tags = ''
->>> f1.save()
->>> Tag.objects.get_for_object(f1)
-[]
-
-# Forcing tags to lowercase
->>> settings.FORCE_LOWERCASE_TAGS = True
->>> Tag.objects.update_tags(dead, 'foO bAr Ter')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: foo>, <Tag: ter>]
->>> Tag.objects.update_tags(dead, 'foO bAr baZ')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>]
->>> Tag.objects.add_tag(dead, 'FOO')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>]
->>> Tag.objects.add_tag(dead, 'Zip')
->>> Tag.objects.get_for_object(dead)
-[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
->>> Tag.objects.update_tags(dead, None)
->>> f1.tags = u'TEST5'
->>> f1.save()
->>> Tag.objects.get_for_object(f1)
-[<Tag: test5>]
->>> f1.tags
-u'test5'
-
-# Retrieving tags by Model ####################################################
-
->>> Tag.objects.usage_for_model(Parrot)
-[]
->>> parrot_details = (
-... ('pining for the fjords', 9, True, 'foo bar'),
-... ('passed on', 6, False, 'bar baz ter'),
-... ('no more', 4, True, 'foo ter'),
-... ('late', 2, False, 'bar ter'),
-... )
-
->>> for state, perch_size, perch_smelly, tags in parrot_details:
-... perch = Perch.objects.create(size=perch_size, smelly=perch_smelly)
-... parrot = Parrot.objects.create(state=state, perch=perch)
-... Tag.objects.update_tags(parrot, tags)
-
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True)]
-[(u'bar', 3), (u'baz', 1), (u'foo', 2), (u'ter', 3)]
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2)]
-[(u'bar', 3), (u'foo', 2), (u'ter', 3)]
-
-# Limiting results to a subset of the model
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more'))]
-[(u'foo', 1), (u'ter', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p'))]
-[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4))]
-[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True))]
-[(u'bar', 1), (u'foo', 2), (u'ter', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True))]
-[(u'foo', 2)]
->>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4))]
-[(u'bar', False), (u'baz', False), (u'foo', False), (u'ter', False)]
->>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99))]
-[]
-
-# Related tags
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True)]
-[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2)]
-[(u'ter', 2)]
->>> [tag.name for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False)]
-[u'baz', u'foo', u'ter']
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True)]
-[(u'baz', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True)]
-[]
-
-# Once again, with feeling (strings)
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, counts=True)]
-[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, min_count=2)]
-[(u'ter', 2)]
->>> [tag.name for tag in Tag.objects.related_for_model('bar', Parrot, counts=False)]
-[u'baz', u'foo', u'ter']
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True)]
-[(u'baz', 1)]
->>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True)]
-[]
-
-# Retrieving tagged objects by Model ##########################################
-
->>> foo = Tag.objects.get(name='foo')
->>> bar = Tag.objects.get(name='bar')
->>> baz = Tag.objects.get(name='baz')
->>> ter = Tag.objects.get(name='ter')
->>> TaggedItem.objects.get_by_model(Parrot, foo)
-[<Parrot: no more>, <Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_by_model(Parrot, bar)
-[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
-
-# Intersections are supported
->>> TaggedItem.objects.get_by_model(Parrot, [foo, baz])
-[]
->>> TaggedItem.objects.get_by_model(Parrot, [foo, bar])
-[<Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_by_model(Parrot, [bar, ter])
-[<Parrot: late>, <Parrot: passed on>]
-
-# You can also pass Tag QuerySets
->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz']))
-[]
->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar']))
-[<Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter']))
-[<Parrot: late>, <Parrot: passed on>]
-
-# You can also pass strings and lists of strings
->>> TaggedItem.objects.get_by_model(Parrot, 'foo baz')
-[]
->>> TaggedItem.objects.get_by_model(Parrot, 'foo bar')
-[<Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_by_model(Parrot, 'bar ter')
-[<Parrot: late>, <Parrot: passed on>]
->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz'])
-[]
->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar'])
-[<Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter'])
-[<Parrot: late>, <Parrot: passed on>]
-
-# Issue 50 - Get by non-existent tag
->>> TaggedItem.objects.get_by_model(Parrot, 'argatrons')
-[]
-
-# Unions
->>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter'])
-[<Parrot: late>, <Parrot: no more>, <Parrot: passed on>, <Parrot: pining for the fjords>]
->>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz'])
-[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
-
-# Retrieving related objects by Model #########################################
-
-# Related instances of the same Model
->>> l1 = Link.objects.create(name='link 1')
->>> Tag.objects.update_tags(l1, 'tag1 tag2 tag3 tag4 tag5')
->>> l2 = Link.objects.create(name='link 2')
->>> Tag.objects.update_tags(l2, 'tag1 tag2 tag3')
->>> l3 = Link.objects.create(name='link 3')
->>> Tag.objects.update_tags(l3, 'tag1')
->>> l4 = Link.objects.create(name='link 4')
->>> TaggedItem.objects.get_related(l1, Link)
-[<Link: link 2>, <Link: link 3>]
->>> TaggedItem.objects.get_related(l1, Link, num=1)
-[<Link: link 2>]
->>> TaggedItem.objects.get_related(l4, Link)
-[]
-
-# Related instance of a different Model
->>> a1 = Article.objects.create(name='article 1')
->>> Tag.objects.update_tags(a1, 'tag1 tag2 tag3 tag4')
->>> TaggedItem.objects.get_related(a1, Link)
-[<Link: link 1>, <Link: link 2>, <Link: link 3>]
->>> Tag.objects.update_tags(a1, 'tag6')
->>> TaggedItem.objects.get_related(a1, Link)
-[]
-
-################
-# Model Fields #
-################
-
-# TagField ####################################################################
-
-# Ensure that automatically created forms use TagField
->>> class TestForm(forms.ModelForm):
-... class Meta:
-... model = FormTest
->>> form = TestForm()
->>> form.fields['tags'].__class__.__name__
-'TagField'
-
-# Recreating string representaions of tag lists ###############################
->>> plain = Tag.objects.create(name='plain')
->>> spaces = Tag.objects.create(name='spa ces')
->>> comma = Tag.objects.create(name='com,ma')
-
->>> from tagging.utils import edit_string_for_tags
->>> edit_string_for_tags([plain])
-u'plain'
->>> edit_string_for_tags([plain, spaces])
-u'plain, spa ces'
->>> edit_string_for_tags([plain, spaces, comma])
-u'plain, spa ces, "com,ma"'
->>> edit_string_for_tags([plain, comma])
-u'plain "com,ma"'
->>> edit_string_for_tags([comma, spaces])
-u'"com,ma", spa ces'
-
-###############
-# Form Fields #
-###############
-
->>> t = TagField()
->>> t.clean('foo')
-u'foo'
->>> t.clean('foo bar baz')
-u'foo bar baz'
->>> t.clean('foo,bar,baz')
-u'foo,bar,baz'
->>> t.clean('foo, bar, baz')
-u'foo, bar, baz'
->>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar')
-u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'
->>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar')
-Traceback (most recent call last):
- ...
-ValidationError: [u'Each tag may be no more than 50 characters long.']
-"""
+# -*- coding: utf-8 -*-
+tests = r"""
+>>> import os
+>>> from django import newforms as forms
+>>> from tagging.forms import TagField
+>>> from tagging import settings
+>>> from tagging.models import Tag, TaggedItem
+>>> from tagging.tests.models import Article, Link, Perch, Parrot, FormTest
+>>> from tagging.utils import calculate_cloud, get_tag_list, get_tag, parse_tag_input
+>>> from tagging.utils import LINEAR
+>>> from tagging.validators import isTagList, isTag
+
+#############
+# Utilities #
+#############
+
+# Tag input ###################################################################
+
+# Simple space-delimited tags
+>>> parse_tag_input('one')
+[u'one']
+>>> parse_tag_input('one two')
+[u'one', u'two']
+>>> parse_tag_input('one two three')
+[u'one', u'three', u'two']
+>>> parse_tag_input('one one two two')
+[u'one', u'two']
+
+# Comma-delimited multiple words - an unquoted comma in the input will trigger
+# this.
+>>> parse_tag_input(',one')
+[u'one']
+>>> parse_tag_input(',one two')
+[u'one two']
+>>> parse_tag_input(',one two three')
+[u'one two three']
+>>> parse_tag_input('a-one, a-two and a-three')
+[u'a-one', u'a-two and a-three']
+
+# Double-quoted multiple words - a completed quote will trigger this.
+# Unclosed quotes are ignored.
+>>> parse_tag_input('"one')
+[u'one']
+>>> parse_tag_input('"one two')
+[u'one', u'two']
+>>> parse_tag_input('"one two three')
+[u'one', u'three', u'two']
+>>> parse_tag_input('"one two"')
+[u'one two']
+>>> parse_tag_input('a-one "a-two and a-three"')
+[u'a-one', u'a-two and a-three']
+
+# No loose commas - split on spaces
+>>> parse_tag_input('one two "thr,ee"')
+[u'one', u'thr,ee', u'two']
+
+# Loose commas - split on commas
+>>> parse_tag_input('"one", two three')
+[u'one', u'two three']
+
+# Double quotes can contain commas
+>>> parse_tag_input('a-one "a-two, and a-three"')
+[u'a-one', u'a-two, and a-three']
+>>> parse_tag_input('"two", one, one, two, "one"')
+[u'one', u'two']
+
+# Bad users! Naughty users!
+>>> parse_tag_input(None)
+[]
+>>> parse_tag_input('')
+[]
+>>> parse_tag_input('"')
+[]
+>>> parse_tag_input('""')
+[]
+>>> parse_tag_input('"' * 7)
+[]
+>>> parse_tag_input(',,,,,,')
+[]
+>>> parse_tag_input('",",",",",",","')
+[u',']
+>>> parse_tag_input('a-one "a-two" and "a-three')
+[u'a-one', u'a-three', u'a-two', u'and']
+
+# Normalised Tag list input ###################################################
+>>> cheese = Tag.objects.create(name='cheese')
+>>> toast = Tag.objects.create(name='toast')
+>>> get_tag_list(cheese)
+[<Tag: cheese>]
+>>> get_tag_list('cheese toast')
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list('cheese,toast')
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([])
+[]
+>>> get_tag_list(['cheese', 'toast'])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([cheese.id, toast.id])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ'])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list([cheese, toast])
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list((cheese, toast))
+(<Tag: cheese>, <Tag: toast>)
+>>> get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast']))
+[<Tag: cheese>, <Tag: toast>]
+>>> get_tag_list(['cheese', toast])
+Traceback (most recent call last):
+ ...
+ValueError: If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.
+>>> get_tag_list(29)
+Traceback (most recent call last):
+ ...
+ValueError: The tag input given was invalid.
+
+# Normalised Tag input
+>>> get_tag(cheese)
+<Tag: cheese>
+>>> get_tag('cheese')
+<Tag: cheese>
+>>> get_tag(cheese.id)
+<Tag: cheese>
+>>> get_tag('mouse')
+
+# Tag clouds ##################################################################
+>>> tags = []
+>>> for line in open(os.path.join(os.path.dirname(__file__), 'tags.txt')).readlines():
+... name, count = line.rstrip().split()
+... tag = Tag(name=name)
+... tag.count = int(count)
+... tags.append(tag)
+
+>>> sizes = {}
+>>> for tag in calculate_cloud(tags, steps=5):
+... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
+
+# This isn't a pre-calculated test, just making sure it's consistent
+>>> sizes
+{1: 48, 2: 30, 3: 19, 4: 15, 5: 10}
+
+>>> sizes = {}
+>>> for tag in calculate_cloud(tags, steps=5, distribution=LINEAR):
+... sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1
+
+# This isn't a pre-calculated test, just making sure it's consistent
+>>> sizes
+{1: 97, 2: 12, 3: 7, 4: 2, 5: 4}
+
+>>> calculate_cloud(tags, steps=5, distribution='cheese')
+Traceback (most recent call last):
+ ...
+ValueError: Invalid distribution algorithm specified: cheese.
+
+# Validators ##################################################################
+
+>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+
+>>> isTag('"test"', {})
+>>> isTag(',test', {})
+>>> isTag('f o o', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Multiple tags were given.']
+>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {})
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+
+###########
+# Tagging #
+###########
+
+# Basic tagging ###############################################################
+
+>>> dead = Parrot.objects.create(state='dead')
+>>> Tag.objects.update_tags(dead, 'foo,bar,"ter"')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: foo>, <Tag: ter>]
+>>> Tag.objects.update_tags(dead, '"foo" bar "baz"')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'foo')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'zip')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
+>>> Tag.objects.add_tag(dead, ' ')
+Traceback (most recent call last):
+ ...
+AttributeError: No tags were given: " ".
+>>> Tag.objects.add_tag(dead, 'one two')
+Traceback (most recent call last):
+ ...
+AttributeError: Multiple tags were given: "one two".
+
+# Note that doctest in Python 2.4 (and maybe 2.5?) doesn't support non-ascii
+# characters in output, so we're displaying the repr() here.
+>>> Tag.objects.update_tags(dead, 'ŠĐĆŽćžšđ')
+>>> repr(Tag.objects.get_for_object(dead))
+'[<Tag: \xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91>]'
+
+>>> Tag.objects.update_tags(dead, None)
+>>> Tag.objects.get_for_object(dead)
+[]
+
+# Using a model's TagField
+>>> f1 = FormTest.objects.create(tags=u'test3 test2 test1')
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test1>, <Tag: test2>, <Tag: test3>]
+>>> f1.tags = u'test4'
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test4>]
+>>> f1.tags = ''
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[]
+
+# Forcing tags to lowercase
+>>> settings.FORCE_LOWERCASE_TAGS = True
+>>> Tag.objects.update_tags(dead, 'foO bAr Ter')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: foo>, <Tag: ter>]
+>>> Tag.objects.update_tags(dead, 'foO bAr baZ')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'FOO')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>]
+>>> Tag.objects.add_tag(dead, 'Zip')
+>>> Tag.objects.get_for_object(dead)
+[<Tag: bar>, <Tag: baz>, <Tag: foo>, <Tag: zip>]
+>>> Tag.objects.update_tags(dead, None)
+>>> f1.tags = u'TEST5'
+>>> f1.save()
+>>> Tag.objects.get_for_object(f1)
+[<Tag: test5>]
+>>> f1.tags
+u'test5'
+
+# Retrieving tags by Model ####################################################
+
+>>> Tag.objects.usage_for_model(Parrot)
+[]
+>>> parrot_details = (
+... ('pining for the fjords', 9, True, 'foo bar'),
+... ('passed on', 6, False, 'bar baz ter'),
+... ('no more', 4, True, 'foo ter'),
+... ('late', 2, False, 'bar ter'),
+... )
+
+>>> for state, perch_size, perch_smelly, tags in parrot_details:
+... perch = Perch.objects.create(size=perch_size, smelly=perch_smelly)
+... parrot = Parrot.objects.create(state=state, perch=perch)
+... Tag.objects.update_tags(parrot, tags)
+
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True)]
+[(u'bar', 3), (u'baz', 1), (u'foo', 2), (u'ter', 3)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2)]
+[(u'bar', 3), (u'foo', 2), (u'ter', 3)]
+
+# Limiting results to a subset of the model
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more'))]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p'))]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4))]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True))]
+[(u'bar', 1), (u'foo', 2), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True))]
+[(u'foo', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4))]
+[(u'bar', False), (u'baz', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99))]
+[]
+
+# Related tags
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True)]
+[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2)]
+[(u'ter', 2)]
+>>> [tag.name for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False)]
+[u'baz', u'foo', u'ter']
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True)]
+[(u'baz', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True)]
+[]
+
+# Once again, with feeling (strings)
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, counts=True)]
+[(u'baz', 1), (u'foo', 1), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model('bar', Parrot, min_count=2)]
+[(u'ter', 2)]
+>>> [tag.name for tag in Tag.objects.related_for_model('bar', Parrot, counts=False)]
+[u'baz', u'foo', u'ter']
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True)]
+[(u'baz', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True)]
+[]
+
+# Retrieving tagged objects by Model ##########################################
+
+>>> foo = Tag.objects.get(name='foo')
+>>> bar = Tag.objects.get(name='bar')
+>>> baz = Tag.objects.get(name='baz')
+>>> ter = Tag.objects.get(name='ter')
+>>> TaggedItem.objects.get_by_model(Parrot, foo)
+[<Parrot: no more>, <Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, bar)
+[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+
+# Intersections are supported
+>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz])
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar])
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter])
+[<Parrot: late>, <Parrot: passed on>]
+
+# Issue 114 - Intersection with non-existant tags
+>>> TaggedItem.objects.get_intersection_by_model(Parrot, [])
+[]
+
+# You can also pass Tag QuerySets
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz']))
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar']))
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter']))
+[<Parrot: late>, <Parrot: passed on>]
+
+# You can also pass strings and lists of strings
+>>> TaggedItem.objects.get_by_model(Parrot, 'foo baz')
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, 'foo bar')
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter')
+[<Parrot: late>, <Parrot: passed on>]
+>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz'])
+[]
+>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar'])
+[<Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter'])
+[<Parrot: late>, <Parrot: passed on>]
+
+# Issue 50 - Get by non-existent tag
+>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons')
+[]
+
+# Unions
+>>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter'])
+[<Parrot: late>, <Parrot: no more>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+>>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz'])
+[<Parrot: late>, <Parrot: passed on>, <Parrot: pining for the fjords>]
+
+# Issue 114 - Union with non-existant tags
+>>> TaggedItem.objects.get_union_by_model(Parrot, [])
+[]
+
+# Retrieving related objects by Model #########################################
+
+# Related instances of the same Model
+>>> l1 = Link.objects.create(name='link 1')
+>>> Tag.objects.update_tags(l1, 'tag1 tag2 tag3 tag4 tag5')
+>>> l2 = Link.objects.create(name='link 2')
+>>> Tag.objects.update_tags(l2, 'tag1 tag2 tag3')
+>>> l3 = Link.objects.create(name='link 3')
+>>> Tag.objects.update_tags(l3, 'tag1')
+>>> l4 = Link.objects.create(name='link 4')
+>>> TaggedItem.objects.get_related(l1, Link)
+[<Link: link 2>, <Link: link 3>]
+>>> TaggedItem.objects.get_related(l1, Link, num=1)
+[<Link: link 2>]
+>>> TaggedItem.objects.get_related(l4, Link)
+[]
+
+# Limit related items
+>>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3'))
+[<Link: link 2>]
+
+# Related instance of a different Model
+>>> a1 = Article.objects.create(name='article 1')
+>>> Tag.objects.update_tags(a1, 'tag1 tag2 tag3 tag4')
+>>> TaggedItem.objects.get_related(a1, Link)
+[<Link: link 1>, <Link: link 2>, <Link: link 3>]
+>>> Tag.objects.update_tags(a1, 'tag6')
+>>> TaggedItem.objects.get_related(a1, Link)
+[]
+
+################
+# Model Fields #
+################
+
+# TagField ####################################################################
+
+# Ensure that automatically created forms use TagField
+>>> class TestForm(forms.ModelForm):
+... class Meta:
+... model = FormTest
+>>> form = TestForm()
+>>> form.fields['tags'].__class__.__name__
+'TagField'
+
+# Recreating string representaions of tag lists ###############################
+>>> plain = Tag.objects.create(name='plain')
+>>> spaces = Tag.objects.create(name='spa ces')
+>>> comma = Tag.objects.create(name='com,ma')
+
+>>> from tagging.utils import edit_string_for_tags
+>>> edit_string_for_tags([plain])
+u'plain'
+>>> edit_string_for_tags([plain, spaces])
+u'plain, spa ces'
+>>> edit_string_for_tags([plain, spaces, comma])
+u'plain, spa ces, "com,ma"'
+>>> edit_string_for_tags([plain, comma])
+u'plain "com,ma"'
+>>> edit_string_for_tags([comma, spaces])
+u'"com,ma", spa ces'
+
+###############
+# Form Fields #
+###############
+
+>>> t = TagField()
+>>> t.clean('foo')
+u'foo'
+>>> t.clean('foo bar baz')
+u'foo bar baz'
+>>> t.clean('foo,bar,baz')
+u'foo,bar,baz'
+>>> t.clean('foo, bar, baz')
+u'foo, bar, baz'
+>>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar')
+u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'
+>>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar')
+Traceback (most recent call last):
+ ...
+ValidationError: [u'Each tag may be no more than 50 characters long.']
+"""
+
+tests_pre_qsrf = tests + r"""
+# Limiting results to a queryset
+>>> Tag.objects.usage_for_queryset(Parrot.objects.filter())
+Traceback (most recent call last):
+ ...
+AttributeError: 'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.
+"""
+
+tests_post_qsrf = tests + r"""
+>>> from django.db.models import Q
+
+# Limiting results to a queryset
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(state='no more'), counts=True)]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(state__startswith='p'), counts=True)]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4), counts=True)]
+[(u'bar', 2), (u'baz', 1), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), counts=True)]
+[(u'bar', 1), (u'foo', 2), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), min_count=2)]
+[(u'foo', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4))]
+[(u'bar', False), (u'baz', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=99))]
+[]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), counts=True)]
+[(u'bar', 2), (u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), min_count=2)]
+[(u'bar', 2)]
+>>> [(tag.name, hasattr(tag, 'counts')) for tag in Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')))]
+[(u'bar', False), (u'foo', False), (u'ter', False)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(state='passed on'), counts=True)]
+[(u'bar', 2), (u'foo', 2), (u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(state__startswith='p'), min_count=2)]
+[(u'ter', 2)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(Q(perch__size__gt=6) | Q(perch__smelly=False)), counts=True)]
+[(u'foo', 1), (u'ter', 1)]
+>>> [(tag.name, tag.count) for tag in Tag.objects.usage_for_queryset(Parrot.objects.exclude(perch__smelly=True).filter(state__startswith='l'), counts=True)]
+[(u'bar', 1), (u'ter', 1)]
+"""
+
+try:
+ from django.db.models.query import parse_lookup
+except ImportError:
+ __test__ = {'post-qsrf': tests_post_qsrf}
+else:
+ __test__ = {'pre-qsrf': tests_pre_qsrf}
diff --git a/tagging/utils.py b/tagging/utils.py
index 9bfd07f..e89bab0 100644
--- a/tagging/utils.py
+++ b/tagging/utils.py
@@ -1,248 +1,263 @@
-"""
-Tagging utilities - from user tag input parsing to tag cloud
-calculation.
-"""
-import math
-import types
-
-from django.db.models.query import QuerySet
-from django.utils.encoding import force_unicode
-from django.utils.translation import ugettext as _
-
-# Python 2.3 compatibility
-if not hasattr(__builtins__, 'set'):
- from sets import Set as set
-
-def parse_tag_input(input):
- """
- 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.
- """
- if not input:
- return []
-
- input = force_unicode(input)
-
- # Special case - if there are no commas or double quotes in the
- # input, we don't *do* a recall... I mean, we know we only need to
- # split on spaces.
- if u',' not in input and u'"' not in input:
- words = list(set(split_strip(input, u' ')))
- 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(input)
- try:
- while 1:
- 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(input, delimiter=u','):
- """
- Splits ``input`` on ``delimiter``, stripping each resulting string
- and returning a list of non-empty strings.
- """
- if not input:
- return []
-
- words = [w.strip() for w in input.split(delimiter)]
- return [w for w in words if w]
-
-def edit_string_for_tags(tags):
- """
- Given list of ``Tag`` instances, creates a string representation of
- 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.
- """
- names = []
- use_commas = False
- for tag in tags:
- name = tag.name
- if u',' in name:
- names.append('"%s"' % name)
- continue
- elif u' ' in name:
- if not use_commas:
- use_commas = True
- names.append(name)
- if use_commas:
- glue = u', '
- else:
- glue = u' '
- return glue.join(names)
-
-def get_tag_list(tags):
- """
- Utility function for accepting tag input in a flexible manner.
-
- If a ``Tag`` object is given, it will be returned in a list as
- its single occupant.
-
- If given, the tag names in the following will be used to create a
- ``Tag`` ``QuerySet``:
-
- * A string, which may contain multiple tag names.
- * A list or tuple of strings corresponding to tag names.
- * A list or tuple of integers corresponding to tag ids.
-
- If given, the following will be returned as-is:
-
- * A list or tuple of ``Tag`` objects.
- * A ``Tag`` ``QuerySet``.
-
- """
- from tagging.models import Tag
- if isinstance(tags, Tag):
- return [tags]
- elif isinstance(tags, QuerySet) and tags.model is Tag:
- return tags
- elif isinstance(tags, types.StringTypes):
- return Tag.objects.filter(name__in=parse_tag_input(tags))
- elif isinstance(tags, (types.ListType, types.TupleType)):
- if len(tags) == 0:
- return tags
- contents = set()
- for item in tags:
- if isinstance(item, types.StringTypes):
- contents.add('string')
- elif isinstance(item, Tag):
- contents.add('tag')
- elif isinstance(item, (types.IntType, types.LongType)):
- contents.add('int')
- if len(contents) == 1:
- if 'string' in contents:
- return Tag.objects.filter(name__in=[force_unicode(tag) \
- for tag in tags])
- elif 'tag' in contents:
- return tags
- elif 'int' in contents:
- return Tag.objects.filter(id__in=tags)
- else:
- raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.'))
- else:
- raise ValueError(_('The tag input given was invalid.'))
-
-def get_tag(tag):
- """
- Utility function for accepting single tag input in a flexible
- manner.
-
- If a ``Tag`` object is given it will be returned as-is; if a
- string or integer are given, they will be used to lookup the
- appropriate ``Tag``.
-
- If no matching tag can be found, ``None`` will be returned.
- """
- from tagging.models import Tag
- if isinstance(tag, Tag):
- return tag
-
- try:
- if isinstance(tag, types.StringTypes):
- return Tag.objects.get(name=tag)
- elif isinstance(tag, (types.IntType, types.LongType)):
- return Tag.objects.get(id=tag)
- except Tag.DoesNotExist:
- pass
-
- return None
-
-# Font size distribution algorithms
-LOGARITHMIC, LINEAR = 1, 2
-
-def _calculate_thresholds(min_weight, max_weight, steps):
- delta = (max_weight - min_weight) / float(steps)
- return [min_weight + i * delta for i in range(1, steps + 1)]
-
-def _calculate_tag_weight(weight, max_weight, distribution):
- """
- Logarithmic tag weight calculation is based on code from the
- `Tag Cloud`_ plugin for Mephisto, by Sven Fuchs.
-
- .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud
- """
- if distribution == LINEAR or max_weight == 1:
- return weight
- elif distribution == LOGARITHMIC:
- return math.log(weight) * max_weight / math.log(max_weight)
- raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution)
-
-def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC):
- """
- Add a ``font_size`` attribute to each tag according to the
- frequency of its use, as indicated by its ``count``
- attribute.
-
- ``steps`` defines the range of font sizes - ``font_size`` will
- be an integer between 1 and ``steps`` (inclusive).
-
- ``distribution`` defines the type of font size distribution
- algorithm which will be used - logarithmic or linear. It must be
- one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
- """
- if len(tags) > 0:
- counts = [tag.count for tag in tags]
- min_weight = float(min(counts))
- max_weight = float(max(counts))
- thresholds = _calculate_thresholds(min_weight, max_weight, steps)
- for tag in tags:
- font_set = False
- tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution)
- for i in range(steps):
- if not font_set and tag_weight <= thresholds[i]:
- tag.font_size = i + 1
- font_set = True
- return tags
+"""
+Tagging utilities - from user tag input parsing to tag cloud
+calculation.
+"""
+import math
+import types
+
+from django.db.models.query import QuerySet
+from django.utils.encoding import force_unicode
+from django.utils.translation import ugettext as _
+
+# Python 2.3 compatibility
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+def parse_tag_input(input):
+ """
+ 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.
+ """
+ if not input:
+ return []
+
+ input = force_unicode(input)
+
+ # Special case - if there are no commas or double quotes in the
+ # input, we don't *do* a recall... I mean, we know we only need to
+ # split on spaces.
+ if u',' not in input and u'"' not in input:
+ words = list(set(split_strip(input, u' ')))
+ 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(input)
+ try:
+ while 1:
+ 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(input, delimiter=u','):
+ """
+ Splits ``input`` on ``delimiter``, stripping each resulting string
+ and returning a list of non-empty strings.
+ """
+ if not input:
+ return []
+
+ words = [w.strip() for w in input.split(delimiter)]
+ return [w for w in words if w]
+
+def edit_string_for_tags(tags):
+ """
+ Given list of ``Tag`` instances, creates a string representation of
+ 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.
+ """
+ names = []
+ use_commas = False
+ for tag in tags:
+ name = tag.name
+ if u',' in name:
+ names.append('"%s"' % name)
+ continue
+ elif u' ' in name:
+ if not use_commas:
+ use_commas = True
+ names.append(name)
+ if use_commas:
+ glue = u', '
+ else:
+ glue = u' '
+ return glue.join(names)
+
+def get_queryset_and_model(queryset_or_model):
+ """
+ Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
+ (queryset, model).
+
+ If a ``Model`` is given, the ``QuerySet`` returned will be created
+ using its default manager.
+ """
+ try:
+ return queryset_or_model, queryset_or_model.model
+ except AttributeError:
+ return queryset_or_model._default_manager.all(), queryset_or_model
+
+def get_tag_list(tags):
+ """
+ Utility function for accepting tag input in a flexible manner.
+
+ If a ``Tag`` object is given, it will be returned in a list as
+ its single occupant.
+
+ If given, the tag names in the following will be used to create a
+ ``Tag`` ``QuerySet``:
+
+ * A string, which may contain multiple tag names.
+ * A list or tuple of strings corresponding to tag names.
+ * A list or tuple of integers corresponding to tag ids.
+
+ If given, the following will be returned as-is:
+
+ * A list or tuple of ``Tag`` objects.
+ * A ``Tag`` ``QuerySet``.
+
+ """
+ from tagging.models import Tag
+ if isinstance(tags, Tag):
+ return [tags]
+ elif isinstance(tags, QuerySet) and tags.model is Tag:
+ return tags
+ elif isinstance(tags, types.StringTypes):
+ return Tag.objects.filter(name__in=parse_tag_input(tags))
+ elif isinstance(tags, (types.ListType, types.TupleType)):
+ if len(tags) == 0:
+ return tags
+ contents = set()
+ for item in tags:
+ if isinstance(item, types.StringTypes):
+ contents.add('string')
+ elif isinstance(item, Tag):
+ contents.add('tag')
+ elif isinstance(item, (types.IntType, types.LongType)):
+ contents.add('int')
+ if len(contents) == 1:
+ if 'string' in contents:
+ return Tag.objects.filter(name__in=[force_unicode(tag) \
+ for tag in tags])
+ elif 'tag' in contents:
+ return tags
+ elif 'int' in contents:
+ return Tag.objects.filter(id__in=tags)
+ else:
+ raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.'))
+ else:
+ raise ValueError(_('The tag input given was invalid.'))
+
+def get_tag(tag):
+ """
+ Utility function for accepting single tag input in a flexible
+ manner.
+
+ If a ``Tag`` object is given it will be returned as-is; if a
+ string or integer are given, they will be used to lookup the
+ appropriate ``Tag``.
+
+ If no matching tag can be found, ``None`` will be returned.
+ """
+ from tagging.models import Tag
+ if isinstance(tag, Tag):
+ return tag
+
+ try:
+ if isinstance(tag, types.StringTypes):
+ return Tag.objects.get(name=tag)
+ elif isinstance(tag, (types.IntType, types.LongType)):
+ return Tag.objects.get(id=tag)
+ except Tag.DoesNotExist:
+ pass
+
+ return None
+
+# Font size distribution algorithms
+LOGARITHMIC, LINEAR = 1, 2
+
+def _calculate_thresholds(min_weight, max_weight, steps):
+ delta = (max_weight - min_weight) / float(steps)
+ return [min_weight + i * delta for i in range(1, steps + 1)]
+
+def _calculate_tag_weight(weight, max_weight, distribution):
+ """
+ Logarithmic tag weight calculation is based on code from the
+ `Tag Cloud`_ plugin for Mephisto, by Sven Fuchs.
+
+ .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud
+ """
+ if distribution == LINEAR or max_weight == 1:
+ return weight
+ elif distribution == LOGARITHMIC:
+ return math.log(weight) * max_weight / math.log(max_weight)
+ raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution)
+
+def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC):
+ """
+ Add a ``font_size`` attribute to each tag according to the
+ frequency of its use, as indicated by its ``count``
+ attribute.
+
+ ``steps`` defines the range of font sizes - ``font_size`` will
+ be an integer between 1 and ``steps`` (inclusive).
+
+ ``distribution`` defines the type of font size distribution
+ algorithm which will be used - logarithmic or linear. It must be
+ one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
+ """
+ if len(tags) > 0:
+ counts = [tag.count for tag in tags]
+ min_weight = float(min(counts))
+ max_weight = float(max(counts))
+ thresholds = _calculate_thresholds(min_weight, max_weight, steps)
+ for tag in tags:
+ font_set = False
+ tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution)
+ for i in range(steps):
+ if not font_set and tag_weight <= thresholds[i]:
+ tag.font_size = i + 1
+ font_set = True
+ return tags
diff --git a/tagging/validators.py b/tagging/validators.py
index 0daeb17..e902237 100644
--- a/tagging/validators.py
+++ b/tagging/validators.py
@@ -1,30 +1,30 @@
-"""
-Oldforms validators for tagging related fields - these are still
-required for basic ``django.contrib.admin`` application field validation
-until the ``newforms-admin`` branch lands in trunk.
-"""
-from django.core.validators import ValidationError
-from django.utils.translation import ugettext as _
-
-from tagging import settings
-from tagging.utils import parse_tag_input
-
-def isTagList(field_data, all_data):
- """
- Validates that ``field_data`` is a valid list of tags.
- """
- for tag_name in parse_tag_input(field_data):
- if len(tag_name) > settings.MAX_TAG_LENGTH:
- raise ValidationError(
- _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
-
-def isTag(field_data, all_data):
- """
- Validates that ``field_data`` is a valid tag.
- """
- tag_names = parse_tag_input(field_data)
- if len(tag_names) > 1:
- raise ValidationError(_('Multiple tags were given.'))
- elif len(tag_names[0]) > settings.MAX_TAG_LENGTH:
- raise ValidationError(
- _('A tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
+"""
+Oldforms validators for tagging related fields - these are still
+required for basic ``django.contrib.admin`` application field validation
+until the ``newforms-admin`` branch lands in trunk.
+"""
+from django.core.validators import ValidationError
+from django.utils.translation import ugettext as _
+
+from tagging import settings
+from tagging.utils import parse_tag_input
+
+def isTagList(field_data, all_data):
+ """
+ Validates that ``field_data`` is a valid list of tags.
+ """
+ for tag_name in parse_tag_input(field_data):
+ if len(tag_name) > settings.MAX_TAG_LENGTH:
+ raise ValidationError(
+ _('Each tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
+
+def isTag(field_data, all_data):
+ """
+ Validates that ``field_data`` is a valid tag.
+ """
+ tag_names = parse_tag_input(field_data)
+ if len(tag_names) > 1:
+ raise ValidationError(_('Multiple tags were given.'))
+ elif len(tag_names[0]) > settings.MAX_TAG_LENGTH:
+ raise ValidationError(
+ _('A tag may be no more than %s characters long.') % settings.MAX_TAG_LENGTH)
diff --git a/tagging/views.py b/tagging/views.py
index 26a391e..9e7e2f5 100644
--- a/tagging/views.py
+++ b/tagging/views.py
@@ -1,52 +1,52 @@
-"""
-Tagging related views.
-"""
-from django.http import Http404
-from django.utils.translation import ugettext as _
-from django.views.generic.list_detail import object_list
-
-from tagging.models import Tag, TaggedItem
-from tagging.utils import get_tag
-
-def tagged_object_list(request, model=None, tag=None, related_tags=False,
- related_tag_counts=True, **kwargs):
- """
- A thin wrapper around
- ``django.views.generic.list_detail.object_list`` which creates a
- ``QuerySet`` containing instances of the given model tagged with
- the given tag.
-
- In addition to the context variables set up by ``object_list``, a
- ``tag`` context variable will contain the ``Tag`` instance for the
- tag.
-
- If ``related_tags`` is ``True``, a ``related_tags`` context variable
- will contain tags related to the given tag for the given model.
- Additionally, if ``related_tag_counts`` is ``True``, each related
- tag will have a ``count`` attribute indicating the number of items
- which have it in addition to the given tag.
- """
- if model is None:
- try:
- model = kwargs['model']
- except KeyError:
- raise AttributeError(_('tagged_object_list must be called with a model.'))
-
- if tag is None:
- try:
- tag = kwargs['tag']
- except KeyError:
- raise AttributeError(_('tagged_object_list must be called with a tag.'))
-
- tag_instance = get_tag(tag)
- if tag_instance is None:
- raise Http404(_('No Tag found matching "%s".') % tag)
- queryset = TaggedItem.objects.get_by_model(model, tag_instance)
- if not kwargs.has_key('extra_context'):
- kwargs['extra_context'] = {}
- kwargs['extra_context']['tag'] = tag_instance
- if related_tags:
- kwargs['extra_context']['related_tags'] = \
- Tag.objects.related_for_model(tag_instance, model,
- counts=related_tag_counts)
- return object_list(request, queryset, **kwargs)
+"""
+Tagging related views.
+"""
+from django.http import Http404
+from django.utils.translation import ugettext as _
+from django.views.generic.list_detail import object_list
+
+from tagging.models import Tag, TaggedItem
+from tagging.utils import get_tag, get_queryset_and_model
+
+def tagged_object_list(request, queryset_or_model=None, tag=None,
+ related_tags=False, related_tag_counts=True, **kwargs):
+ """
+ A thin wrapper around
+ ``django.views.generic.list_detail.object_list`` which creates a
+ ``QuerySet`` containing instances of the given queryset or model
+ tagged with the given tag.
+
+ In addition to the context variables set up by ``object_list``, a
+ ``tag`` context variable will contain the ``Tag`` instance for the
+ tag.
+
+ If ``related_tags`` is ``True``, a ``related_tags`` context variable
+ will contain tags related to the given tag for the given model.
+ Additionally, if ``related_tag_counts`` is ``True``, each related
+ tag will have a ``count`` attribute indicating the number of items
+ which have it in addition to the given tag.
+ """
+ if queryset_or_model is None:
+ try:
+ queryset_or_model = kwargs.pop('queryset_or_model')
+ except KeyError:
+ raise AttributeError(_('tagged_object_list must be called with a queryset or a model.'))
+
+ if tag is None:
+ try:
+ tag = kwargs.pop('tag')
+ except KeyError:
+ raise AttributeError(_('tagged_object_list must be called with a tag.'))
+
+ tag_instance = get_tag(tag)
+ if tag_instance is None:
+ raise Http404(_('No Tag found matching "%s".') % tag)
+ queryset = TaggedItem.objects.get_by_model(queryset_or_model, tag_instance)
+ if not kwargs.has_key('extra_context'):
+ kwargs['extra_context'] = {}
+ kwargs['extra_context']['tag'] = tag_instance
+ if related_tags:
+ kwargs['extra_context']['related_tags'] = \
+ Tag.objects.related_for_model(tag_instance, queryset_or_model,
+ counts=related_tag_counts)
+ return object_list(request, queryset, **kwargs)