diff options
Diffstat (limited to 'tagging')
62 files changed, 4976 insertions, 1796 deletions
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) |