From 2228968f3d51a3d686adb2839bf43e018432f941 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:45 -0700 Subject: Imported Upstream version 0.1+svn102 --- tagging/__init__.py | 1 + tagging/fields.py | 109 ++++++++++ tagging/forms.py | 22 ++ tagging/managers.py | 366 +++++++++++++++++++++++++++++++++ tagging/models.py | 53 +++++ tagging/settings.py | 13 ++ tagging/templatetags/__init__.py | 0 tagging/templatetags/tagging_tags.py | 114 +++++++++++ tagging/tests/__init__.py | 0 tagging/tests/models.py | 38 ++++ tagging/tests/runtests.py | 8 + tagging/tests/settings.py | 27 +++ tagging/tests/tags.txt | 122 +++++++++++ tagging/tests/tests.py | 384 +++++++++++++++++++++++++++++++++++ tagging/utils.py | 140 +++++++++++++ tagging/validators.py | 33 +++ tagging/views.py | 48 +++++ 17 files changed, 1478 insertions(+) create mode 100644 tagging/__init__.py create mode 100644 tagging/fields.py create mode 100644 tagging/forms.py create mode 100644 tagging/managers.py create mode 100644 tagging/models.py create mode 100644 tagging/settings.py create mode 100644 tagging/templatetags/__init__.py create mode 100644 tagging/templatetags/tagging_tags.py create mode 100644 tagging/tests/__init__.py create mode 100644 tagging/tests/models.py create mode 100644 tagging/tests/runtests.py create mode 100644 tagging/tests/settings.py create mode 100644 tagging/tests/tags.txt create mode 100644 tagging/tests/tests.py create mode 100644 tagging/utils.py create mode 100644 tagging/validators.py create mode 100644 tagging/views.py (limited to 'tagging') diff --git a/tagging/__init__.py b/tagging/__init__.py new file mode 100644 index 0000000..171b9e0 --- /dev/null +++ b/tagging/__init__.py @@ -0,0 +1 @@ +VERSION = (0, 2, 'pre') diff --git a/tagging/fields.py b/tagging/fields.py new file mode 100644 index 0000000..1e66458 --- /dev/null +++ b/tagging/fields.py @@ -0,0 +1,109 @@ +from django.db.models import signals +from django.db.models.fields import CharField +from django.dispatch import dispatcher + +from tagging import settings +from tagging.models import Tag +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 tags2str(Tag.objects.usage_for_model(owner)) + + tags = self._get_instance_tag_cache(instance) + if tags is None: + if instance._get_pk_val() is None: + self._set_instance_tag_cache(instance, '') + else: + self._set_instance_tag_cache(instance, tags2str(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: + self._set_instance_tag_cache(instance, value.lower()) + else: + 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) + +# Helper +def tags2str(tagset): + return u' '.join([t.name for t in tagset]) diff --git a/tagging/forms.py b/tagging/forms.py new file mode 100644 index 0000000..875c598 --- /dev/null +++ b/tagging/forms.py @@ -0,0 +1,22 @@ +from django import newforms as forms + +from tagging.utils import get_tag_name_list +from tagging.validators import tag_list_re + +class TagField(forms.CharField): + def clean(self, value): + """ + Validates that the input is a valid list of tag names, + separated by a single comma, a single space or a comma + followed by a space. + """ + value = super(TagField, self).clean(value) + if value == u'': + return value + if not tag_list_re.search(value): + raise forms.ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.') + tag_names = get_tag_name_list(value) + for tag_name in tag_names: + if len(tag_name) > 50: + raise forms.ValidationError(u'Tag names must be no longer than 50 characters.') + return value diff --git a/tagging/managers.py b/tagging/managers.py new file mode 100644 index 0000000..5e47605 --- /dev/null +++ b/tagging/managers.py @@ -0,0 +1,366 @@ +""" +Custom Managers for generic tagging Models. +""" +import types + +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 tagging import settings +from tagging.utils import calculate_cloud, get_tag_name_list, get_tag_list, LOGARITHMIC +from tagging.validators import tag_re + +# 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.id, + items__object_id=obj._get_pk_val())) + updated_tag_names = set(get_tag_name_list(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) > 0: + TaggedItemModel._default_manager.filter(content_type__pk=ctype.id, + object_id=obj._get_pk_val(), + 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. + """ + if not tag_re.match(tag_name): + raise AttributeError(u'An invalid tag name was given: %s. Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.' % tag_name) + 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._get_pk_val()) + + 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.id, + items__object_id=obj._get_pk_val()) + + 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. + + If ``counts`` is True, a ``count`` attribute will be added to + each tag, indicating how many times it has been used against + the Model 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).id, + } + + 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).id, + '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.id 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) + + 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: + return [] # No existing tags were given + elif tag_count == 1: + tag = tags[0] # Optimisation for single tag + else: + return self.get_intersection_by_model(Model, tags) + ctype = ContentType.objects.get_for_model(Model) + rel_table = qn(self.model._meta.db_table) + return Model._default_manager.extra( + tables=[self.model._meta.db_table], # Use a non-explicit join + where=[ + '%s.content_type_id = %%s' % rel_table, + '%s.tag_id = %%s' % rel_table, + '%s.%s = %s.object_id' % (qn(Model._meta.db_table), + qn(Model._meta.pk.column), + rel_table) + ], + params=[ctype.id, tag.id], + ) + + 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).id, + 'tag_id_placeholders': ','.join(['%s'] * tag_count), + 'tag_count': tag_count, + } + + cursor = connection.cursor() + cursor.execute(query, [tag.id 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.id == related_content_type.id: + # 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.id, + 'related_content_type_id': related_content_type.id, + 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '', + } + + cursor = connection.cursor() + cursor.execute(query, [obj._get_pk_val()]) + 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() diff --git a/tagging/models.py b/tagging/models.py new file mode 100644 index 0000000..199e5eb --- /dev/null +++ b/tagging/models.py @@ -0,0 +1,53 @@ +""" +Models for generic tagging. +""" +from django.db import models +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType + +from tagging.managers import TagManager, TaggedItemManager +from tagging.validators import isTag + +class Tag(models.Model): + """ + A basic tag. + """ + name = models.CharField(max_length=50, unique=True, db_index=True, validator_list=[isTag]) + + objects = TagManager() + + class Meta: + db_table = 'tag' + verbose_name = 'Tag' + verbose_name_plural = 'Tags' + ordering = ('name',) + + 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, related_name='items') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + object = generic.GenericForeignKey('content_type', 'object_id') + + objects = TaggedItemManager() + + class Meta: + db_table = 'tagged_item' + verbose_name = 'Tagged Item' + verbose_name_plural = 'Tagged Items' + # Enforce unique tag association per object + unique_together = (('tag', 'content_type', 'object_id'),) + + class Admin: + pass + + def __unicode__(self): + return u'%s [%s]' % (self.object, self.tag) diff --git a/tagging/settings.py b/tagging/settings.py new file mode 100644 index 0000000..18321bd --- /dev/null +++ b/tagging/settings.py @@ -0,0 +1,13 @@ +""" +Convenience module for access of custom tagging application settings, +which enforces default settings when the main settings module which has +been configured does not contain the appropriate settings. +""" +from django.conf import settings + +# Whether to force all tags to lowercase before they are saved to the +# database. Default is False. +try: + FORCE_LOWERCASE_TAGS = settings.FORCE_LOWERCASE_TAGS +except AttributeError: + FORCE_LOWERCASE_TAGS = False diff --git a/tagging/templatetags/__init__.py b/tagging/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tagging/templatetags/tagging_tags.py b/tagging/templatetags/tagging_tags.py new file mode 100644 index 0000000..fb4347f --- /dev/null +++ b/tagging/templatetags/tagging_tags.py @@ -0,0 +1,114 @@ +from django.db.models import get_model +from django.template import Library, Node, TemplateSyntaxError, resolve_variable +from tagging.models import Tag, TaggedItem + +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('.')) + context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts) + return '' + +class TagsForObjectNode(Node): + def __init__(self, obj, context_var): + self.obj = obj + self.context_var = context_var + + def render(self, context): + obj = resolve_variable(self.obj, context) + context[self.context_var] = Tag.objects.get_for_object(obj) + return '' + +class TaggedObjectsNode(Node): + def __init__(self, tag, model, context_var): + self.tag = tag + self.context_var = context_var + self.model = model + + def render(self, context): + tag = resolve_variable(self.tag, context) + model = get_model(*self.model.split('.')) + context[self.context_var] = TaggedItem.objects.get_by_model(model, + tag) + 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. + + The model is specified in ``[appname].[modelname]`` format. + + 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. + + Example usage:: + + {% 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_tags_for_object(parser, token): + """ + Retrieves a list of ``Tag`` objects associated with an object and + stores them in a context variable. + + Example usage:: + + {% 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 objects for a given Model which are tagged with + a given Tag and stores them in a context variable. + + The tag must be an instance of a ``Tag``, not the name of a tag. + + The model is specified in ``[appname].[modelname]`` format. + + Example usage:: + + {% tagged_objects foo_tag in tv.Model as object_list %} + """ + 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('tags_for_object', do_tags_for_object) +register.tag('tagged_objects', do_tagged_objects) \ No newline at end of file diff --git a/tagging/tests/__init__.py b/tagging/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tagging/tests/models.py b/tagging/tests/models.py new file mode 100644 index 0000000..86754cd --- /dev/null +++ b/tagging/tests/models.py @@ -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() diff --git a/tagging/tests/runtests.py b/tagging/tests/runtests.py new file mode 100644 index 0000000..ab1d49c --- /dev/null +++ b/tagging/tests/runtests.py @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..26e659b --- /dev/null +++ b/tagging/tests/settings.py @@ -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/tags.txt b/tagging/tests/tags.txt new file mode 100644 index 0000000..8543411 --- /dev/null +++ b/tagging/tests/tags.txt @@ -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/tests.py b/tagging/tests/tests.py new file mode 100644 index 0000000..41a7895 --- /dev/null +++ b/tagging/tests/tests.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +r""" +>>> import os +>>> from django import newforms as forms +>>> 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_name_list, get_tag_list, get_tag, LINEAR +>>> from tagging.validators import isTagList, isTag +>>> from tagging.forms import TagField + +############# +# Utilities # +############# + +# Tag input ################################################################### + +# Tag names +>>> get_tag_name_list(None) +[] +>>> get_tag_name_list('') +[] +>>> get_tag_name_list('foo') +[u'foo'] +>>> get_tag_name_list('foo bar') +[u'foo', u'bar'] +>>> get_tag_name_list('foo,bar') +[u'foo', u'bar'] +>>> get_tag_name_list(', , foo , bar , ,baz, , ,') +[u'foo', u'bar', u'baz'] +>>> get_tag_name_list('foo,ŠĐĆŽćžšđ') +[u'foo', u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'] + +# Normalised Tag list input +>>> cheese = Tag.objects.create(name='cheese') +>>> toast = Tag.objects.create(name='toast') +>>> get_tag_list(cheese) +[] +>>> get_tag_list('cheese toast') +[, ] +>>> get_tag_list(u'cheese toast') +[, ] +>>> get_tag_list([]) +[] +>>> get_tag_list(['cheese', 'toast']) +[, ] +>>> get_tag_list([cheese.id, toast.id]) +[, ] +>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) +[, ] +>>> get_tag_list([cheese, toast]) +[, ] +>>> get_tag_list((cheese, toast)) +(, ) +>>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) + +>>> get_tag('cheese') + +>>> get_tag(cheese.id) + +>>> 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: 20, 3: 24, 4: 19, 5: 11} + +>>> 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 font size distribution algorithm specified: cheese. + +############## +# Validators # +############## + +>>> isTagList('foo', {}) +>>> isTagList('foo bar baz', {}) +>>> isTagList('foo,bar,baz', {}) +>>> isTagList('foo, bar, baz', {}) +>>> isTagList('foo, ŠĐĆŽćžšđ, baz', {}) +>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar', {}) +>>> isTagList('', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> isTagList(' foo', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> isTagList('foo ', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> isTagList('foo bar', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must be no longer than 50 characters.'] +>>> isTag('f-o_1o', {}) +>>> isTag('ŠĐĆŽćžšđ', {}) +>>> isTag('f o o', {}) +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.'] + +############### +# 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') +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> t.clean('foo ') +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> t.clean('foo bar') +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] +>>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') +Traceback (most recent call last): + ... +ValidationError: [u'Tag names must be no longer than 50 characters.'] + +# Ensure that automatically created forms use TagField +>>> TestForm = forms.form_for_model(FormTest) +>>> form = TestForm() +>>> form.fields['tags'].__class__.__name__ +'TagField' +>>> instance = FormTest(tags='one two three') +>>> TestInstanceForm = forms.form_for_instance(instance) +>>> form = TestInstanceForm() +>>> form.fields['tags'].__class__.__name__ +'TagField' + +########### +# Tagging # +########### + +# Basic tagging ############################################################### + +>>> dead = Parrot.objects.create(state='dead') +>>> Tag.objects.update_tags(dead, 'foo bar ter') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.update_tags(dead, 'foo bar baz') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'foo') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> Tag.objects.add_tag(dead, 'f o o') +Traceback (most recent call last): + ... +AttributeError: An invalid tag name was given: f o o. Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens. + +# 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.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) +[, , ] +>>> f1.tags = u'test4' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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.objects.update_tags(dead, 'foO bAr baZ') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'FOO') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'Zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> Tag.objects.update_tags(dead, None) +>>> f1.tags = u'TEST5' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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) +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, bar) +[, , ] + +# Intersections are supported +>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) +[, ] + +# 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'])) +[] +>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) +[, ] + +# 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') +[] +>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) +[, ] + +# Issue 50 - Get by non-existent tag +>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') +[] + +# 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) +[, ] +>>> TaggedItem.objects.get_related(l1, Link, num=1) +[] +>>> 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) +[, , ] +>>> Tag.objects.update_tags(a1, 'tag6') +>>> TaggedItem.objects.get_related(a1, Link) +[] +""" diff --git a/tagging/utils.py b/tagging/utils.py new file mode 100644 index 0000000..af50bac --- /dev/null +++ b/tagging/utils.py @@ -0,0 +1,140 @@ +import math +import re +import types + +from django.db.models.query import QuerySet +from django.utils.encoding import force_unicode, smart_unicode + +# Python 2.3 compatibility +if not hasattr(__builtins__, 'set'): + from sets import Set as set + +find_tag_re = re.compile(r'[-\w]+', re.U) + +def get_tag_name_list(tag_names): + """ + Finds tag names in the given string and return them as a list. + """ + if tag_names is not None: + tag_names = force_unicode(tag_names) + results = find_tag_re.findall(tag_names or '') + return results + +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=get_tag_name_list(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=[smart_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(u'If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.') + else: + raise ValueError(u'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_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 + either ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. + + The algorithm to scale the tags logarithmically is from a + blog post by Anders Pearson, 'Scaling tag clouds': + http://thraxil.com/users/anders/posts/2005/12/13/scaling-tag-clouds/ + """ + if len(tags) > 0: + thresholds = [] + counts = [tag.count for tag in tags] + max_weight = float(max(counts)) + min_weight = float(min(counts)) + + # Set up the appropriate thresholds + if distribution == LOGARITHMIC: + thresholds = [math.pow(max_weight - min_weight + 1, float(i) / float(steps)) \ + for i in range(1, steps + 1)] + elif distribution == LINEAR: + delta = (max_weight - min_weight) / float(steps) + thresholds = [min_weight + i * delta for i in range(1, steps + 1)] + else: + raise ValueError(u'Invalid font size distribution algorithm specified: %s.' % distribution) + + for tag in tags: + font_set = False + for i in range(steps): + if not font_set and tag.count <= thresholds[i]: + tag.font_size = i + 1 + font_set = True + return tags diff --git a/tagging/validators.py b/tagging/validators.py new file mode 100644 index 0000000..363fa6d --- /dev/null +++ b/tagging/validators.py @@ -0,0 +1,33 @@ +import re + +from django.core.validators import ValidationError +from django.utils.encoding import smart_unicode + +from tagging.utils import get_tag_name_list + +tag_re = re.compile(r'^[-\w]+$', re.U) +tag_list_re = re.compile(r'^[-\w]+(?:(?:,\s|[,\s])[-\w]+)*$', re.U) + +def isTagList(field_data, all_data): + """ + Validates that ``field_data`` is a valid list of tag names, + separated by a single comma, a single space or a comma followed + by a space. + """ + if field_data is not None: + field_data = smart_unicode(field_data) + if not tag_list_re.search(field_data): + raise ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.') + tag_names = get_tag_name_list(field_data) + for tag_name in tag_names: + if len(tag_name) > 50: + raise ValidationError(u'Tag names must be no longer than 50 characters.') + +def isTag(field_data, all_data): + """ + Validates that ``field_data`` is a valid tag name. + """ + if field_data is not None: + field_data = smart_unicode(field_data) + if not tag_re.match(field_data): + raise ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.') diff --git a/tagging/views.py b/tagging/views.py new file mode 100644 index 0000000..4e424f1 --- /dev/null +++ b/tagging/views.py @@ -0,0 +1,48 @@ +from django.http import Http404 +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 given 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(u'tagged_object_list must be called with a model.') + + if tag is None: + try: + tag = kwargs['tag'] + except KeyError: + raise AttributeError(u'tagged_object_list must be called with a tag.') + + tag_instance = get_tag(tag) + if tag_instance is None: + raise Http404(u'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) -- cgit v1.2.3 From 3b9f21a55fed735652716e63fedabad87899be81 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:45 -0700 Subject: Imported Upstream version 0.2.1 --- tagging/__init__.py | 2 +- tagging/fields.py | 219 +++++----- tagging/forms.py | 45 +- tagging/managers.py | 774 +++++++++++++++++---------------- tagging/models.py | 105 +++-- tagging/settings.py | 26 +- tagging/templatetags/tagging_tags.py | 345 ++++++++++----- tagging/tests/models.py | 76 ++-- tagging/tests/runtests.py | 16 +- tagging/tests/settings.py | 54 +-- tagging/tests/tags.txt | 242 +++++------ tagging/tests/tests.py | 818 +++++++++++++++++++---------------- tagging/utils.py | 388 +++++++++++------ tagging/validators.py | 63 ++- tagging/views.py | 100 +++-- 15 files changed, 1796 insertions(+), 1477 deletions(-) (limited to 'tagging') diff --git a/tagging/__init__.py b/tagging/__init__.py index 171b9e0..28ed501 100644 --- a/tagging/__init__.py +++ b/tagging/__init__.py @@ -1 +1 @@ -VERSION = (0, 2, 'pre') +VERSION = (0, 2.1, None) diff --git a/tagging/fields.py b/tagging/fields.py index 1e66458..c9475f6 100644 --- a/tagging/fields.py +++ b/tagging/fields.py @@ -1,109 +1,110 @@ -from django.db.models import signals -from django.db.models.fields import CharField -from django.dispatch import dispatcher - -from tagging import settings -from tagging.models import Tag -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 tags2str(Tag.objects.usage_for_model(owner)) - - tags = self._get_instance_tag_cache(instance) - if tags is None: - if instance._get_pk_val() is None: - self._set_instance_tag_cache(instance, '') - else: - self._set_instance_tag_cache(instance, tags2str(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: - self._set_instance_tag_cache(instance, value.lower()) - else: - 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) - -# Helper -def tags2str(tagset): - return u' '.join([t.name for t in tagset]) +""" +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) diff --git a/tagging/forms.py b/tagging/forms.py index 875c598..6844039 100644 --- a/tagging/forms.py +++ b/tagging/forms.py @@ -1,22 +1,23 @@ -from django import newforms as forms - -from tagging.utils import get_tag_name_list -from tagging.validators import tag_list_re - -class TagField(forms.CharField): - def clean(self, value): - """ - Validates that the input is a valid list of tag names, - separated by a single comma, a single space or a comma - followed by a space. - """ - value = super(TagField, self).clean(value) - if value == u'': - return value - if not tag_list_re.search(value): - raise forms.ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.') - tag_names = get_tag_name_list(value) - for tag_name in tag_names: - if len(tag_name) > 50: - raise forms.ValidationError(u'Tag names must be no longer than 50 characters.') - return value +""" +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 diff --git a/tagging/managers.py b/tagging/managers.py index 5e47605..612c127 100644 --- a/tagging/managers.py +++ b/tagging/managers.py @@ -1,366 +1,408 @@ -""" -Custom Managers for generic tagging Models. -""" -import types - -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 tagging import settings -from tagging.utils import calculate_cloud, get_tag_name_list, get_tag_list, LOGARITHMIC -from tagging.validators import tag_re - -# 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.id, - items__object_id=obj._get_pk_val())) - updated_tag_names = set(get_tag_name_list(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) > 0: - TaggedItemModel._default_manager.filter(content_type__pk=ctype.id, - object_id=obj._get_pk_val(), - 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. - """ - if not tag_re.match(tag_name): - raise AttributeError(u'An invalid tag name was given: %s. Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.' % tag_name) - 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._get_pk_val()) - - 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.id, - items__object_id=obj._get_pk_val()) - - 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. - - If ``counts`` is True, a ``count`` attribute will be added to - each tag, indicating how many times it has been used against - the Model 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).id, - } - - 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).id, - '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.id 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) - - 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: - return [] # No existing tags were given - elif tag_count == 1: - tag = tags[0] # Optimisation for single tag - else: - return self.get_intersection_by_model(Model, tags) - ctype = ContentType.objects.get_for_model(Model) - rel_table = qn(self.model._meta.db_table) - return Model._default_manager.extra( - tables=[self.model._meta.db_table], # Use a non-explicit join - where=[ - '%s.content_type_id = %%s' % rel_table, - '%s.tag_id = %%s' % rel_table, - '%s.%s = %s.object_id' % (qn(Model._meta.db_table), - qn(Model._meta.pk.column), - rel_table) - ], - params=[ctype.id, tag.id], - ) - - 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).id, - 'tag_id_placeholders': ','.join(['%s'] * tag_count), - 'tag_count': tag_count, - } - - cursor = connection.cursor() - cursor.execute(query, [tag.id 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.id == related_content_type.id: - # 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.id, - 'related_content_type_id': related_content_type.id, - 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '', - } - - cursor = connection.cursor() - cursor.execute(query, [obj._get_pk_val()]) - 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 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() diff --git a/tagging/models.py b/tagging/models.py index 199e5eb..f81c896 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -1,53 +1,52 @@ -""" -Models for generic tagging. -""" -from django.db import models -from django.contrib.contenttypes import generic -from django.contrib.contenttypes.models import ContentType - -from tagging.managers import TagManager, TaggedItemManager -from tagging.validators import isTag - -class Tag(models.Model): - """ - A basic tag. - """ - name = models.CharField(max_length=50, unique=True, db_index=True, validator_list=[isTag]) - - objects = TagManager() - - class Meta: - db_table = 'tag' - verbose_name = 'Tag' - verbose_name_plural = 'Tags' - ordering = ('name',) - - 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, related_name='items') - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - object = generic.GenericForeignKey('content_type', 'object_id') - - objects = TaggedItemManager() - - class Meta: - db_table = 'tagged_item' - verbose_name = 'Tagged Item' - verbose_name_plural = 'Tagged Items' - # Enforce unique tag association per object - unique_together = (('tag', 'content_type', 'object_id'),) - - class Admin: - pass - - def __unicode__(self): - return u'%s [%s]' % (self.object, self.tag) +""" +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) diff --git a/tagging/settings.py b/tagging/settings.py index 18321bd..78da3af 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 which has -been configured does not contain the appropriate settings. -""" -from django.conf import settings - -# Whether to force all tags to lowercase before they are saved to the -# database. Default is False. -try: - FORCE_LOWERCASE_TAGS = settings.FORCE_LOWERCASE_TAGS -except AttributeError: - 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/tagging_tags.py b/tagging/templatetags/tagging_tags.py index fb4347f..789517a 100644 --- a/tagging/templatetags/tagging_tags.py +++ b/tagging/templatetags/tagging_tags.py @@ -1,114 +1,231 @@ -from django.db.models import get_model -from django.template import Library, Node, TemplateSyntaxError, resolve_variable -from tagging.models import Tag, TaggedItem - -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('.')) - context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts) - return '' - -class TagsForObjectNode(Node): - def __init__(self, obj, context_var): - self.obj = obj - self.context_var = context_var - - def render(self, context): - obj = resolve_variable(self.obj, context) - context[self.context_var] = Tag.objects.get_for_object(obj) - return '' - -class TaggedObjectsNode(Node): - def __init__(self, tag, model, context_var): - self.tag = tag - self.context_var = context_var - self.model = model - - def render(self, context): - tag = resolve_variable(self.tag, context) - model = get_model(*self.model.split('.')) - context[self.context_var] = TaggedItem.objects.get_by_model(model, - tag) - 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. - - The model is specified in ``[appname].[modelname]`` format. - - 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. - - Example usage:: - - {% 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_tags_for_object(parser, token): - """ - Retrieves a list of ``Tag`` objects associated with an object and - stores them in a context variable. - - Example usage:: - - {% 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 objects for a given Model which are tagged with - a given Tag and stores them in a context variable. - - The tag must be an instance of a ``Tag``, not the name of a tag. - - The model is specified in ``[appname].[modelname]`` format. - - Example usage:: - - {% tagged_objects foo_tag in tv.Model as object_list %} - """ - 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('tags_for_object', do_tags_for_object) -register.tag('tagged_objects', do_tagged_objects) \ No newline at end of file +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/models.py b/tagging/tests/models.py index 86754cd..69a5c7b 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() diff --git a/tagging/tests/runtests.py b/tagging/tests/runtests.py index ab1d49c..e66ee86 100644 --- a/tagging/tests/runtests.py +++ b/tagging/tests/runtests.py @@ -1,8 +1,8 @@ -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) +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 26e659b..93ba4b0 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 8543411..61bfd9e 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 41a7895..b1c8032 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -1,384 +1,434 @@ -# -*- coding: utf-8 -*- -r""" ->>> import os ->>> from django import newforms as forms ->>> 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_name_list, get_tag_list, get_tag, LINEAR ->>> from tagging.validators import isTagList, isTag ->>> from tagging.forms import TagField - -############# -# Utilities # -############# - -# Tag input ################################################################### - -# Tag names ->>> get_tag_name_list(None) -[] ->>> get_tag_name_list('') -[] ->>> get_tag_name_list('foo') -[u'foo'] ->>> get_tag_name_list('foo bar') -[u'foo', u'bar'] ->>> get_tag_name_list('foo,bar') -[u'foo', u'bar'] ->>> get_tag_name_list(', , foo , bar , ,baz, , ,') -[u'foo', u'bar', u'baz'] ->>> get_tag_name_list('foo,ŠĐĆŽćžšđ') -[u'foo', u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'] - -# Normalised Tag list input ->>> cheese = Tag.objects.create(name='cheese') ->>> toast = Tag.objects.create(name='toast') ->>> get_tag_list(cheese) -[] ->>> get_tag_list('cheese toast') -[, ] ->>> get_tag_list(u'cheese toast') -[, ] ->>> get_tag_list([]) -[] ->>> get_tag_list(['cheese', 'toast']) -[, ] ->>> get_tag_list([cheese.id, toast.id]) -[, ] ->>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) -[, ] ->>> get_tag_list([cheese, toast]) -[, ] ->>> get_tag_list((cheese, toast)) -(, ) ->>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) - ->>> get_tag('cheese') - ->>> get_tag(cheese.id) - ->>> 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: 20, 3: 24, 4: 19, 5: 11} - ->>> 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 font size distribution algorithm specified: cheese. - -############## -# Validators # -############## - ->>> isTagList('foo', {}) ->>> isTagList('foo bar baz', {}) ->>> isTagList('foo,bar,baz', {}) ->>> isTagList('foo, bar, baz', {}) ->>> isTagList('foo, ŠĐĆŽćžšđ, baz', {}) ->>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar', {}) ->>> isTagList('', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> isTagList(' foo', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> isTagList('foo ', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> isTagList('foo bar', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> isTagList('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must be no longer than 50 characters.'] ->>> isTag('f-o_1o', {}) ->>> isTag('ŠĐĆŽćžšđ', {}) ->>> isTag('f o o', {}) -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.'] - -############### -# 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') -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> t.clean('foo ') -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> t.clean('foo bar') -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.'] ->>> t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') -Traceback (most recent call last): - ... -ValidationError: [u'Tag names must be no longer than 50 characters.'] - -# Ensure that automatically created forms use TagField ->>> TestForm = forms.form_for_model(FormTest) ->>> form = TestForm() ->>> form.fields['tags'].__class__.__name__ -'TagField' ->>> instance = FormTest(tags='one two three') ->>> TestInstanceForm = forms.form_for_instance(instance) ->>> form = TestInstanceForm() ->>> form.fields['tags'].__class__.__name__ -'TagField' - -########### -# Tagging # -########### - -# Basic tagging ############################################################### - ->>> dead = Parrot.objects.create(state='dead') ->>> Tag.objects.update_tags(dead, 'foo bar ter') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.update_tags(dead, 'foo bar baz') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'foo') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> Tag.objects.add_tag(dead, 'f o o') -Traceback (most recent call last): - ... -AttributeError: An invalid tag name was given: f o o. Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens. - -# 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.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) -[, , ] ->>> f1.tags = u'test4' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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.objects.update_tags(dead, 'foO bAr baZ') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'FOO') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'Zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> Tag.objects.update_tags(dead, None) ->>> f1.tags = u'TEST5' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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) -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, bar) -[, , ] - -# Intersections are supported ->>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) -[, ] - -# 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'])) -[] ->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) -[, ] - -# 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') -[] ->>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) -[, ] - -# Issue 50 - Get by non-existent tag ->>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') -[] - -# 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) -[, ] ->>> TaggedItem.objects.get_related(l1, Link, num=1) -[] ->>> 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) -[, , ] ->>> Tag.objects.update_tags(a1, 'tag6') ->>> TaggedItem.objects.get_related(a1, Link) -[] -""" +# -*- 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) +[] +>>> get_tag_list('cheese toast') +[, ] +>>> get_tag_list('cheese,toast') +[, ] +>>> get_tag_list([]) +[] +>>> get_tag_list(['cheese', 'toast']) +[, ] +>>> get_tag_list([cheese.id, toast.id]) +[, ] +>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) +[, ] +>>> get_tag_list([cheese, toast]) +[, ] +>>> get_tag_list((cheese, toast)) +(, ) +>>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) + +>>> get_tag('cheese') + +>>> get_tag(cheese.id) + +>>> 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.objects.update_tags(dead, '"foo" bar "baz"') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'foo') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> 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.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) +[, , ] +>>> f1.tags = u'test4' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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.objects.update_tags(dead, 'foO bAr baZ') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'FOO') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'Zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> Tag.objects.update_tags(dead, None) +>>> f1.tags = u'TEST5' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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) +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, bar) +[, , ] + +# Intersections are supported +>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) +[, ] + +# 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'])) +[] +>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) +[, ] + +# 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') +[] +>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) +[, ] + +# Issue 50 - Get by non-existent tag +>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') +[] + +# Unions +>>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) +[, , , ] +>>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) +[, , ] + +# 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) +[, ] +>>> TaggedItem.objects.get_related(l1, Link, num=1) +[] +>>> 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) +[, , ] +>>> 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.'] +""" diff --git a/tagging/utils.py b/tagging/utils.py index af50bac..9bfd07f 100644 --- a/tagging/utils.py +++ b/tagging/utils.py @@ -1,140 +1,248 @@ -import math -import re -import types - -from django.db.models.query import QuerySet -from django.utils.encoding import force_unicode, smart_unicode - -# Python 2.3 compatibility -if not hasattr(__builtins__, 'set'): - from sets import Set as set - -find_tag_re = re.compile(r'[-\w]+', re.U) - -def get_tag_name_list(tag_names): - """ - Finds tag names in the given string and return them as a list. - """ - if tag_names is not None: - tag_names = force_unicode(tag_names) - results = find_tag_re.findall(tag_names or '') - return results - -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=get_tag_name_list(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=[smart_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(u'If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.') - else: - raise ValueError(u'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_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 - either ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. - - The algorithm to scale the tags logarithmically is from a - blog post by Anders Pearson, 'Scaling tag clouds': - http://thraxil.com/users/anders/posts/2005/12/13/scaling-tag-clouds/ - """ - if len(tags) > 0: - thresholds = [] - counts = [tag.count for tag in tags] - max_weight = float(max(counts)) - min_weight = float(min(counts)) - - # Set up the appropriate thresholds - if distribution == LOGARITHMIC: - thresholds = [math.pow(max_weight - min_weight + 1, float(i) / float(steps)) \ - for i in range(1, steps + 1)] - elif distribution == LINEAR: - delta = (max_weight - min_weight) / float(steps) - thresholds = [min_weight + i * delta for i in range(1, steps + 1)] - else: - raise ValueError(u'Invalid font size distribution algorithm specified: %s.' % distribution) - - for tag in tags: - font_set = False - for i in range(steps): - if not font_set and tag.count <= 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 +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 diff --git a/tagging/validators.py b/tagging/validators.py index 363fa6d..0daeb17 100644 --- a/tagging/validators.py +++ b/tagging/validators.py @@ -1,33 +1,30 @@ -import re - -from django.core.validators import ValidationError -from django.utils.encoding import smart_unicode - -from tagging.utils import get_tag_name_list - -tag_re = re.compile(r'^[-\w]+$', re.U) -tag_list_re = re.compile(r'^[-\w]+(?:(?:,\s|[,\s])[-\w]+)*$', re.U) - -def isTagList(field_data, all_data): - """ - Validates that ``field_data`` is a valid list of tag names, - separated by a single comma, a single space or a comma followed - by a space. - """ - if field_data is not None: - field_data = smart_unicode(field_data) - if not tag_list_re.search(field_data): - raise ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens, with a comma, space or comma followed by space used to separate each tag name.') - tag_names = get_tag_name_list(field_data) - for tag_name in tag_names: - if len(tag_name) > 50: - raise ValidationError(u'Tag names must be no longer than 50 characters.') - -def isTag(field_data, all_data): - """ - Validates that ``field_data`` is a valid tag name. - """ - if field_data is not None: - field_data = smart_unicode(field_data) - if not tag_re.match(field_data): - raise ValidationError(u'Tag names must contain only unicode alphanumeric characters, numbers, underscores or hyphens.') +""" +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 4e424f1..26a391e 100644 --- a/tagging/views.py +++ b/tagging/views.py @@ -1,48 +1,52 @@ -from django.http import Http404 -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 given 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(u'tagged_object_list must be called with a model.') - - if tag is None: - try: - tag = kwargs['tag'] - except KeyError: - raise AttributeError(u'tagged_object_list must be called with a tag.') - - tag_instance = get_tag(tag) - if tag_instance is None: - raise Http404(u'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 + +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) -- cgit v1.2.3 From d03c8d2d2efaf848b01e96863c29f46ce3a0db21 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:46 -0700 Subject: Imported Upstream version 0.2.1+svn147 --- tagging/.svn/all-wcprops | 65 ++ tagging/.svn/dir-prop-base | 6 + tagging/.svn/entries | 374 +++++++++ tagging/.svn/format | 1 + tagging/.svn/prop-base/__init__.py.svn-base | 5 + tagging/.svn/prop-base/fields.py.svn-base | 5 + tagging/.svn/prop-base/forms.py.svn-base | 5 + tagging/.svn/prop-base/generic.py.svn-base | 5 + tagging/.svn/prop-base/managers.py.svn-base | 5 + tagging/.svn/prop-base/models.py.svn-base | 5 + tagging/.svn/prop-base/settings.py.svn-base | 5 + tagging/.svn/prop-base/utils.py.svn-base | 5 + tagging/.svn/prop-base/validators.py.svn-base | 5 + tagging/.svn/prop-base/views.py.svn-base | 5 + tagging/.svn/text-base/__init__.py.svn-base | 30 + tagging/.svn/text-base/fields.py.svn-base | 109 +++ tagging/.svn/text-base/forms.py.svn-base | 23 + tagging/.svn/text-base/generic.py.svn-base | 40 + tagging/.svn/text-base/managers.py.svn-base | 68 ++ tagging/.svn/text-base/models.py.svn-base | 502 +++++++++++ tagging/.svn/text-base/settings.py.svn-base | 13 + tagging/.svn/text-base/utils.py.svn-base | 263 ++++++ tagging/.svn/text-base/validators.py.svn-base | 30 + tagging/.svn/text-base/views.py.svn-base | 52 ++ tagging/__init__.py | 31 +- tagging/fields.py | 219 +++-- tagging/forms.py | 46 +- tagging/generic.py | 40 + tagging/managers.py | 476 ++--------- tagging/models.py | 554 ++++++++++-- tagging/settings.py | 26 +- tagging/templatetags/.svn/all-wcprops | 17 + tagging/templatetags/.svn/dir-prop-base | 6 + tagging/templatetags/.svn/entries | 96 +++ tagging/templatetags/.svn/format | 1 + .../.svn/prop-base/__init__.py.svn-base | 5 + .../.svn/prop-base/tagging_tags.py.svn-base | 5 + .../.svn/text-base/__init__.py.svn-base | 0 .../.svn/text-base/tagging_tags.py.svn-base | 231 +++++ tagging/templatetags/tagging_tags.py | 462 +++++----- tagging/tests/.svn/all-wcprops | 35 + tagging/tests/.svn/dir-prop-base | 6 + tagging/tests/.svn/entries | 198 +++++ tagging/tests/.svn/format | 1 + tagging/tests/.svn/prop-base/__init__.py.svn-base | 5 + tagging/tests/.svn/prop-base/models.py.svn-base | 5 + tagging/tests/.svn/prop-base/settings.py.svn-base | 5 + tagging/tests/.svn/prop-base/tags.txt.svn-base | 5 + tagging/tests/.svn/prop-base/tests.py.svn-base | 5 + tagging/tests/.svn/text-base/__init__.py.svn-base | 0 tagging/tests/.svn/text-base/models.py.svn-base | 38 + tagging/tests/.svn/text-base/settings.py.svn-base | 27 + tagging/tests/.svn/text-base/tags.txt.svn-base | 122 +++ tagging/tests/.svn/text-base/tests.py.svn-base | 495 +++++++++++ tagging/tests/models.py | 76 +- tagging/tests/runtests.py | 8 - tagging/tests/settings.py | 54 +- tagging/tests/tags.txt | 242 +++--- tagging/tests/tests.py | 929 +++++++++++---------- tagging/utils.py | 511 ++++++------ tagging/validators.py | 60 +- tagging/views.py | 104 +-- 62 files changed, 4976 insertions(+), 1796 deletions(-) create mode 100644 tagging/.svn/all-wcprops create mode 100644 tagging/.svn/dir-prop-base create mode 100644 tagging/.svn/entries create mode 100644 tagging/.svn/format create mode 100644 tagging/.svn/prop-base/__init__.py.svn-base create mode 100644 tagging/.svn/prop-base/fields.py.svn-base create mode 100644 tagging/.svn/prop-base/forms.py.svn-base create mode 100644 tagging/.svn/prop-base/generic.py.svn-base create mode 100644 tagging/.svn/prop-base/managers.py.svn-base create mode 100644 tagging/.svn/prop-base/models.py.svn-base create mode 100644 tagging/.svn/prop-base/settings.py.svn-base create mode 100644 tagging/.svn/prop-base/utils.py.svn-base create mode 100644 tagging/.svn/prop-base/validators.py.svn-base create mode 100644 tagging/.svn/prop-base/views.py.svn-base create mode 100644 tagging/.svn/text-base/__init__.py.svn-base create mode 100644 tagging/.svn/text-base/fields.py.svn-base create mode 100644 tagging/.svn/text-base/forms.py.svn-base create mode 100644 tagging/.svn/text-base/generic.py.svn-base create mode 100644 tagging/.svn/text-base/managers.py.svn-base create mode 100644 tagging/.svn/text-base/models.py.svn-base create mode 100644 tagging/.svn/text-base/settings.py.svn-base create mode 100644 tagging/.svn/text-base/utils.py.svn-base create mode 100644 tagging/.svn/text-base/validators.py.svn-base create mode 100644 tagging/.svn/text-base/views.py.svn-base create mode 100644 tagging/generic.py create mode 100644 tagging/templatetags/.svn/all-wcprops create mode 100644 tagging/templatetags/.svn/dir-prop-base create mode 100644 tagging/templatetags/.svn/entries create mode 100644 tagging/templatetags/.svn/format create mode 100644 tagging/templatetags/.svn/prop-base/__init__.py.svn-base create mode 100644 tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base create mode 100644 tagging/templatetags/.svn/text-base/__init__.py.svn-base create mode 100644 tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base create mode 100644 tagging/tests/.svn/all-wcprops create mode 100644 tagging/tests/.svn/dir-prop-base create mode 100644 tagging/tests/.svn/entries create mode 100644 tagging/tests/.svn/format create mode 100644 tagging/tests/.svn/prop-base/__init__.py.svn-base create mode 100644 tagging/tests/.svn/prop-base/models.py.svn-base create mode 100644 tagging/tests/.svn/prop-base/settings.py.svn-base create mode 100644 tagging/tests/.svn/prop-base/tags.txt.svn-base create mode 100644 tagging/tests/.svn/prop-base/tests.py.svn-base create mode 100644 tagging/tests/.svn/text-base/__init__.py.svn-base create mode 100644 tagging/tests/.svn/text-base/models.py.svn-base create mode 100644 tagging/tests/.svn/text-base/settings.py.svn-base create mode 100644 tagging/tests/.svn/text-base/tags.txt.svn-base create mode 100644 tagging/tests/.svn/text-base/tests.py.svn-base delete mode 100644 tagging/tests/runtests.py (limited to 'tagging') 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 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 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) +[] +>>> get_tag_list('cheese toast') +[, ] +>>> get_tag_list('cheese,toast') +[, ] +>>> get_tag_list([]) +[] +>>> get_tag_list(['cheese', 'toast']) +[, ] +>>> get_tag_list([cheese.id, toast.id]) +[, ] +>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) +[, ] +>>> get_tag_list([cheese, toast]) +[, ] +>>> get_tag_list((cheese, toast)) +(, ) +>>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) + +>>> get_tag('cheese') + +>>> get_tag(cheese.id) + +>>> 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.objects.update_tags(dead, '"foo" bar "baz"') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'foo') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> 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.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) +[, , ] +>>> f1.tags = u'test4' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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.objects.update_tags(dead, 'foO bAr baZ') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'FOO') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'Zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> Tag.objects.update_tags(dead, None) +>>> f1.tags = u'TEST5' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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) +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, bar) +[, , ] + +# Intersections are supported +>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) +[, ] + +# 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'])) +[] +>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) +[, ] + +# 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') +[] +>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) +[, ] + +# Issue 50 - Get by non-existent tag +>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') +[] + +# Unions +>>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) +[, , , ] +>>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) +[, , ] + +# 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) +[, ] +>>> TaggedItem.objects.get_related(l1, Link, num=1) +[] +>>> TaggedItem.objects.get_related(l4, Link) +[] + +# Limit related items +>>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3')) +[] + +# 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) +[, , ] +>>> 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) -[] ->>> get_tag_list('cheese toast') -[, ] ->>> get_tag_list('cheese,toast') -[, ] ->>> get_tag_list([]) -[] ->>> get_tag_list(['cheese', 'toast']) -[, ] ->>> get_tag_list([cheese.id, toast.id]) -[, ] ->>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) -[, ] ->>> get_tag_list([cheese, toast]) -[, ] ->>> get_tag_list((cheese, toast)) -(, ) ->>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) - ->>> get_tag('cheese') - ->>> get_tag(cheese.id) - ->>> 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.objects.update_tags(dead, '"foo" bar "baz"') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'foo') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> 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.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) -[, , ] ->>> f1.tags = u'test4' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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.objects.update_tags(dead, 'foO bAr baZ') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'FOO') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'Zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> Tag.objects.update_tags(dead, None) ->>> f1.tags = u'TEST5' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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) -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, bar) -[, , ] - -# Intersections are supported ->>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) -[, ] - -# 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'])) -[] ->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) -[, ] - -# 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') -[] ->>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) -[, ] - -# Issue 50 - Get by non-existent tag ->>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') -[] - -# Unions ->>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) -[, , , ] ->>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) -[, , ] - -# 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) -[, ] ->>> TaggedItem.objects.get_related(l1, Link, num=1) -[] ->>> 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) -[, , ] ->>> 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) +[] +>>> get_tag_list('cheese toast') +[, ] +>>> get_tag_list('cheese,toast') +[, ] +>>> get_tag_list([]) +[] +>>> get_tag_list(['cheese', 'toast']) +[, ] +>>> get_tag_list([cheese.id, toast.id]) +[, ] +>>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) +[, ] +>>> get_tag_list([cheese, toast]) +[, ] +>>> get_tag_list((cheese, toast)) +(, ) +>>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) + +>>> get_tag('cheese') + +>>> get_tag(cheese.id) + +>>> 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.objects.update_tags(dead, '"foo" bar "baz"') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'foo') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> 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.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) +[, , ] +>>> f1.tags = u'test4' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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.objects.update_tags(dead, 'foO bAr baZ') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'FOO') +>>> Tag.objects.get_for_object(dead) +[, , ] +>>> Tag.objects.add_tag(dead, 'Zip') +>>> Tag.objects.get_for_object(dead) +[, , , ] +>>> Tag.objects.update_tags(dead, None) +>>> f1.tags = u'TEST5' +>>> f1.save() +>>> Tag.objects.get_for_object(f1) +[] +>>> 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) +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, bar) +[, , ] + +# Intersections are supported +>>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) +[] +>>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) +[, ] + +# 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'])) +[] +>>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) +[, ] + +# 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') +[] +>>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') +[, ] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) +[] +>>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) +[, ] + +# Issue 50 - Get by non-existent tag +>>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') +[] + +# Unions +>>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) +[, , , ] +>>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) +[, , ] + +# 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) +[, ] +>>> TaggedItem.objects.get_related(l1, Link, num=1) +[] +>>> TaggedItem.objects.get_related(l4, Link) +[] + +# Limit related items +>>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3')) +[] + +# 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) +[, , ] +>>> 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) -- cgit v1.2.3 From 71ac4bf01bb5d6dad8eb77b783a0b06cf4fdeb1e Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:47 -0700 Subject: Imported Upstream version 0.2.1+svn154 --- tagging/.svn/all-wcprops | 65 --- tagging/.svn/dir-prop-base | 6 - tagging/.svn/entries | 374 --------------- tagging/.svn/format | 1 - tagging/.svn/prop-base/__init__.py.svn-base | 5 - tagging/.svn/prop-base/fields.py.svn-base | 5 - tagging/.svn/prop-base/forms.py.svn-base | 5 - tagging/.svn/prop-base/generic.py.svn-base | 5 - tagging/.svn/prop-base/managers.py.svn-base | 5 - tagging/.svn/prop-base/models.py.svn-base | 5 - tagging/.svn/prop-base/settings.py.svn-base | 5 - tagging/.svn/prop-base/utils.py.svn-base | 5 - tagging/.svn/prop-base/validators.py.svn-base | 5 - tagging/.svn/prop-base/views.py.svn-base | 5 - tagging/.svn/text-base/__init__.py.svn-base | 30 -- tagging/.svn/text-base/fields.py.svn-base | 109 ----- tagging/.svn/text-base/forms.py.svn-base | 23 - tagging/.svn/text-base/generic.py.svn-base | 40 -- tagging/.svn/text-base/managers.py.svn-base | 68 --- tagging/.svn/text-base/models.py.svn-base | 502 --------------------- tagging/.svn/text-base/settings.py.svn-base | 13 - tagging/.svn/text-base/utils.py.svn-base | 263 ----------- tagging/.svn/text-base/validators.py.svn-base | 30 -- tagging/.svn/text-base/views.py.svn-base | 52 --- tagging/admin.py | 5 + tagging/fields.py | 2 - tagging/forms.py | 21 +- tagging/managers.py | 2 +- tagging/models.py | 48 +- tagging/templatetags/.svn/all-wcprops | 17 - tagging/templatetags/.svn/dir-prop-base | 6 - tagging/templatetags/.svn/entries | 96 ---- tagging/templatetags/.svn/format | 1 - .../.svn/prop-base/__init__.py.svn-base | 5 - .../.svn/prop-base/tagging_tags.py.svn-base | 5 - .../.svn/text-base/__init__.py.svn-base | 0 .../.svn/text-base/tagging_tags.py.svn-base | 231 ---------- tagging/tests/.svn/all-wcprops | 35 -- tagging/tests/.svn/dir-prop-base | 6 - tagging/tests/.svn/entries | 198 -------- tagging/tests/.svn/format | 1 - tagging/tests/.svn/prop-base/__init__.py.svn-base | 5 - tagging/tests/.svn/prop-base/models.py.svn-base | 5 - tagging/tests/.svn/prop-base/settings.py.svn-base | 5 - tagging/tests/.svn/prop-base/tags.txt.svn-base | 5 - tagging/tests/.svn/prop-base/tests.py.svn-base | 5 - tagging/tests/.svn/text-base/__init__.py.svn-base | 0 tagging/tests/.svn/text-base/models.py.svn-base | 38 -- tagging/tests/.svn/text-base/settings.py.svn-base | 27 -- tagging/tests/.svn/text-base/tags.txt.svn-base | 122 ----- tagging/tests/.svn/text-base/tests.py.svn-base | 495 -------------------- tagging/tests/tests.py | 103 ++--- tagging/validators.py | 30 -- 53 files changed, 71 insertions(+), 3074 deletions(-) delete mode 100644 tagging/.svn/all-wcprops delete mode 100644 tagging/.svn/dir-prop-base delete mode 100644 tagging/.svn/entries delete mode 100644 tagging/.svn/format delete mode 100644 tagging/.svn/prop-base/__init__.py.svn-base delete mode 100644 tagging/.svn/prop-base/fields.py.svn-base delete mode 100644 tagging/.svn/prop-base/forms.py.svn-base delete mode 100644 tagging/.svn/prop-base/generic.py.svn-base delete mode 100644 tagging/.svn/prop-base/managers.py.svn-base delete mode 100644 tagging/.svn/prop-base/models.py.svn-base delete mode 100644 tagging/.svn/prop-base/settings.py.svn-base delete mode 100644 tagging/.svn/prop-base/utils.py.svn-base delete mode 100644 tagging/.svn/prop-base/validators.py.svn-base delete mode 100644 tagging/.svn/prop-base/views.py.svn-base delete mode 100644 tagging/.svn/text-base/__init__.py.svn-base delete mode 100644 tagging/.svn/text-base/fields.py.svn-base delete mode 100644 tagging/.svn/text-base/forms.py.svn-base delete mode 100644 tagging/.svn/text-base/generic.py.svn-base delete mode 100644 tagging/.svn/text-base/managers.py.svn-base delete mode 100644 tagging/.svn/text-base/models.py.svn-base delete mode 100644 tagging/.svn/text-base/settings.py.svn-base delete mode 100644 tagging/.svn/text-base/utils.py.svn-base delete mode 100644 tagging/.svn/text-base/validators.py.svn-base delete mode 100644 tagging/.svn/text-base/views.py.svn-base create mode 100644 tagging/admin.py delete mode 100644 tagging/templatetags/.svn/all-wcprops delete mode 100644 tagging/templatetags/.svn/dir-prop-base delete mode 100644 tagging/templatetags/.svn/entries delete mode 100644 tagging/templatetags/.svn/format delete mode 100644 tagging/templatetags/.svn/prop-base/__init__.py.svn-base delete mode 100644 tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base delete mode 100644 tagging/templatetags/.svn/text-base/__init__.py.svn-base delete mode 100644 tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base delete mode 100644 tagging/tests/.svn/all-wcprops delete mode 100644 tagging/tests/.svn/dir-prop-base delete mode 100644 tagging/tests/.svn/entries delete mode 100644 tagging/tests/.svn/format delete mode 100644 tagging/tests/.svn/prop-base/__init__.py.svn-base delete mode 100644 tagging/tests/.svn/prop-base/models.py.svn-base delete mode 100644 tagging/tests/.svn/prop-base/settings.py.svn-base delete mode 100644 tagging/tests/.svn/prop-base/tags.txt.svn-base delete mode 100644 tagging/tests/.svn/prop-base/tests.py.svn-base delete mode 100644 tagging/tests/.svn/text-base/__init__.py.svn-base delete mode 100644 tagging/tests/.svn/text-base/models.py.svn-base delete mode 100644 tagging/tests/.svn/text-base/settings.py.svn-base delete mode 100644 tagging/tests/.svn/text-base/tags.txt.svn-base delete mode 100644 tagging/tests/.svn/text-base/tests.py.svn-base delete mode 100644 tagging/validators.py (limited to 'tagging') diff --git a/tagging/.svn/all-wcprops b/tagging/.svn/all-wcprops deleted file mode 100644 index fbf957a..0000000 --- a/tagging/.svn/all-wcprops +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 4cc643b..0000000 --- a/tagging/.svn/dir-prop-base +++ /dev/null @@ -1,6 +0,0 @@ -K 10 -svn:ignore -V 6 -*.pyc - -END diff --git a/tagging/.svn/entries b/tagging/.svn/entries deleted file mode 100644 index e226b2d..0000000 --- a/tagging/.svn/entries +++ /dev/null @@ -1,374 +0,0 @@ -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 deleted file mode 100644 index ec63514..0000000 --- a/tagging/.svn/format +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/tagging/.svn/prop-base/__init__.py.svn-base b/tagging/.svn/prop-base/__init__.py.svn-base deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/__init__.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/fields.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/forms.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/generic.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/managers.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/models.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/settings.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/utils.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/validators.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/.svn/prop-base/views.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 9241c20..0000000 --- a/tagging/.svn/text-base/__init__.py.svn-base +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 5b39e84..0000000 --- a/tagging/.svn/text-base/fields.py.svn-base +++ /dev/null @@ -1,109 +0,0 @@ -""" -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 deleted file mode 100644 index e65aec4..0000000 --- a/tagging/.svn/text-base/forms.py.svn-base +++ /dev/null @@ -1,23 +0,0 @@ -""" -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 deleted file mode 100644 index 75d1b8e..0000000 --- a/tagging/.svn/text-base/generic.py.svn-base +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 51e2473..0000000 --- a/tagging/.svn/text-base/managers.py.svn-base +++ /dev/null @@ -1,68 +0,0 @@ -""" -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 deleted file mode 100644 index 7463ddb..0000000 --- a/tagging/.svn/text-base/models.py.svn-base +++ /dev/null @@ -1,502 +0,0 @@ -""" -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 deleted file mode 100644 index 1d6224c..0000000 --- a/tagging/.svn/text-base/settings.py.svn-base +++ /dev/null @@ -1,13 +0,0 @@ -""" -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 deleted file mode 100644 index e89bab0..0000000 --- a/tagging/.svn/text-base/utils.py.svn-base +++ /dev/null @@ -1,263 +0,0 @@ -""" -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 deleted file mode 100644 index e902237..0000000 --- a/tagging/.svn/text-base/validators.py.svn-base +++ /dev/null @@ -1,30 +0,0 @@ -""" -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 deleted file mode 100644 index 9e7e2f5..0000000 --- a/tagging/.svn/text-base/views.py.svn-base +++ /dev/null @@ -1,52 +0,0 @@ -""" -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/admin.py b/tagging/admin.py new file mode 100644 index 0000000..4c1ef38 --- /dev/null +++ b/tagging/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from tagging.models import Tag, TaggedItem + +admin.site.register(TaggedItem) +admin.site.register(Tag) diff --git a/tagging/fields.py b/tagging/fields.py index 5b39e84..f52daff 100644 --- a/tagging/fields.py +++ b/tagging/fields.py @@ -8,7 +8,6 @@ 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): """ @@ -19,7 +18,6 @@ class TagField(CharField): 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): diff --git a/tagging/forms.py b/tagging/forms.py index e65aec4..a70ac6b 100644 --- a/tagging/forms.py +++ b/tagging/forms.py @@ -1,12 +1,28 @@ """ -Tagging components for Django's ``newforms`` form library. +Tagging components for Django's form library. """ from django import forms from django.utils.translation import ugettext as _ from tagging import settings +from tagging.models import Tag from tagging.utils import parse_tag_input +class AdminTagForm(forms.ModelForm): + class Meta: + model = Tag + + def clean_name(self): + value = self.cleaned_data['name'] + tag_names = parse_tag_input(value) + if len(tag_names) > 1: + raise ValidationError(_('Multiple tags were given.')) + elif len(tag_names[0]) > settings.MAX_TAG_LENGTH: + raise forms.ValidationError( + _('A tag may be no more than %s characters long.') % + settings.MAX_TAG_LENGTH) + return value + class TagField(forms.CharField): """ A ``CharField`` which validates that its input is a valid list of @@ -19,5 +35,6 @@ class TagField(forms.CharField): 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) + _('Each tag may be no more than %s characters long.') % + settings.MAX_TAG_LENGTH) return value diff --git a/tagging/managers.py b/tagging/managers.py index 51e2473..02cd1c2 100644 --- a/tagging/managers.py +++ b/tagging/managers.py @@ -23,7 +23,7 @@ class ModelTagManager(models.Manager): 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) + return Tag.objects.usage_for_model(self.model, *args, **kwargs) class ModelTaggedItemManager(models.Manager): """ diff --git a/tagging/models.py b/tagging/models.py index 7463ddb..d43f22d 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -16,15 +16,9 @@ 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 # ############ @@ -147,23 +141,10 @@ class TagManager(models.Manager): """ 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) + queryset = model._default_manager.filter() + for f in filters.items(): + queryset.query.add_filter(f) + usage = self.usage_for_queryset(queryset, counts, min_count) return usage @@ -180,8 +161,6 @@ class TagManager(models.Manager): 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() @@ -288,7 +267,7 @@ class TaggedItemManager(models.Manager): 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 + Now that the queryset-refactor branch is in the trunk, this can be tidied up significantly. """ def get_by_model(self, queryset_or_model, tags): @@ -440,11 +419,16 @@ class TaggedItemManager(models.Manager): '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 '', + # Hardcoding this for now just to get tests working again - this + # should now be handled by the query object. + 'limit_offset': num is not None and 'LIMIT %s' or '', } cursor = connection.cursor() - cursor.execute(query, [obj.pk]) + params = [obj.pk] + if num is not None: + params.append(num) + cursor.execute(query, params) 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 @@ -463,7 +447,7 @@ class Tag(models.Model): """ A tag. """ - name = models.CharField(_('name'), max_length=50, unique=True, db_index=True, validator_list=[isTag]) + name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) objects = TagManager() @@ -472,9 +456,6 @@ class Tag(models.Model): verbose_name = _('tag') verbose_name_plural = _('tags') - class Admin: - pass - def __unicode__(self): return self.name @@ -495,8 +476,5 @@ class TaggedItem(models.Model): 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/templatetags/.svn/all-wcprops b/tagging/templatetags/.svn/all-wcprops deleted file mode 100644 index c3f4205..0000000 --- a/tagging/templatetags/.svn/all-wcprops +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 4cc643b..0000000 --- a/tagging/templatetags/.svn/dir-prop-base +++ /dev/null @@ -1,6 +0,0 @@ -K 10 -svn:ignore -V 6 -*.pyc - -END diff --git a/tagging/templatetags/.svn/entries b/tagging/templatetags/.svn/entries deleted file mode 100644 index 3c6a555..0000000 --- a/tagging/templatetags/.svn/entries +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index ec63514..0000000 --- a/tagging/templatetags/.svn/format +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/tagging/templatetags/.svn/prop-base/__init__.py.svn-base b/tagging/templatetags/.svn/prop-base/__init__.py.svn-base deleted file mode 100644 index bdbd305..0000000 --- a/tagging/templatetags/.svn/prop-base/__init__.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/templatetags/.svn/prop-base/tagging_tags.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index e69de29..0000000 diff --git a/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base b/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base deleted file mode 100644 index 11d31cc..0000000 --- a/tagging/templatetags/.svn/text-base/tagging_tags.py.svn-base +++ /dev/null @@ -1,231 +0,0 @@ -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 deleted file mode 100644 index 1baeebb..0000000 --- a/tagging/tests/.svn/all-wcprops +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 4cc643b..0000000 --- a/tagging/tests/.svn/dir-prop-base +++ /dev/null @@ -1,6 +0,0 @@ -K 10 -svn:ignore -V 6 -*.pyc - -END diff --git a/tagging/tests/.svn/entries b/tagging/tests/.svn/entries deleted file mode 100644 index c5dbe87..0000000 --- a/tagging/tests/.svn/entries +++ /dev/null @@ -1,198 +0,0 @@ -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 deleted file mode 100644 index ec63514..0000000 --- a/tagging/tests/.svn/format +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/tagging/tests/.svn/prop-base/__init__.py.svn-base b/tagging/tests/.svn/prop-base/__init__.py.svn-base deleted file mode 100644 index bdbd305..0000000 --- a/tagging/tests/.svn/prop-base/__init__.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/tests/.svn/prop-base/models.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/tests/.svn/prop-base/settings.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/tests/.svn/prop-base/tags.txt.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index bdbd305..0000000 --- a/tagging/tests/.svn/prop-base/tests.py.svn-base +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index e69de29..0000000 diff --git a/tagging/tests/.svn/text-base/models.py.svn-base b/tagging/tests/.svn/text-base/models.py.svn-base deleted file mode 100644 index 0708686..0000000 --- a/tagging/tests/.svn/text-base/models.py.svn-base +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 26e659b..0000000 --- a/tagging/tests/.svn/text-base/settings.py.svn-base +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 8543411..0000000 --- a/tagging/tests/.svn/text-base/tags.txt.svn-base +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index 574fa92..0000000 --- a/tagging/tests/.svn/text-base/tests.py.svn-base +++ /dev/null @@ -1,495 +0,0 @@ -# -*- 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) -[] ->>> get_tag_list('cheese toast') -[, ] ->>> get_tag_list('cheese,toast') -[, ] ->>> get_tag_list([]) -[] ->>> get_tag_list(['cheese', 'toast']) -[, ] ->>> get_tag_list([cheese.id, toast.id]) -[, ] ->>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) -[, ] ->>> get_tag_list([cheese, toast]) -[, ] ->>> get_tag_list((cheese, toast)) -(, ) ->>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) - ->>> get_tag('cheese') - ->>> get_tag(cheese.id) - ->>> 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.objects.update_tags(dead, '"foo" bar "baz"') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'foo') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> 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.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) -[, , ] ->>> f1.tags = u'test4' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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.objects.update_tags(dead, 'foO bAr baZ') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'FOO') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'Zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> Tag.objects.update_tags(dead, None) ->>> f1.tags = u'TEST5' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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) -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, bar) -[, , ] - -# Intersections are supported ->>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) -[, ] - -# 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'])) -[] ->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) -[, ] - -# 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') -[] ->>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) -[, ] - -# Issue 50 - Get by non-existent tag ->>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') -[] - -# Unions ->>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) -[, , , ] ->>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) -[, , ] - -# 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) -[, ] ->>> TaggedItem.objects.get_related(l1, Link, num=1) -[] ->>> TaggedItem.objects.get_related(l4, Link) -[] - -# Limit related items ->>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3')) -[] - -# 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) -[, , ] ->>> 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/tests.py b/tagging/tests/tests.py index 574fa92..5d6d8b5 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -tests = r""" +r""" >>> import os ->>> from django import newforms as forms +>>> from django import forms +>>> from django.db.models import Q >>> 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 # @@ -152,24 +152,6 @@ 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 # ########### @@ -393,6 +375,36 @@ u'test5' >>> TaggedItem.objects.get_related(a1, Link) [] +# 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)] + ################ # Model Fields # ################ @@ -444,52 +456,3 @@ 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/validators.py b/tagging/validators.py deleted file mode 100644 index e902237..0000000 --- a/tagging/validators.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -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) -- cgit v1.2.3 From 99fde585a39d7867d2c2257b74a7cc441bc9f2a5 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:48 -0700 Subject: Imported Upstream version 0.3 --- tagging/admin.py | 10 +++++++++- tagging/forms.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'tagging') diff --git a/tagging/admin.py b/tagging/admin.py index 4c1ef38..bec3922 100644 --- a/tagging/admin.py +++ b/tagging/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin from tagging.models import Tag, TaggedItem +from tagging.forms import TagAdminForm + +class TagAdmin(admin.ModelAdmin): + form = TagAdminForm admin.site.register(TaggedItem) -admin.site.register(Tag) +admin.site.register(Tag, TagAdmin) + + + + diff --git a/tagging/forms.py b/tagging/forms.py index a70ac6b..a2d9fd9 100644 --- a/tagging/forms.py +++ b/tagging/forms.py @@ -8,7 +8,7 @@ from tagging import settings from tagging.models import Tag from tagging.utils import parse_tag_input -class AdminTagForm(forms.ModelForm): +class TagAdminForm(forms.ModelForm): class Meta: model = Tag @@ -16,7 +16,7 @@ class AdminTagForm(forms.ModelForm): value = self.cleaned_data['name'] tag_names = parse_tag_input(value) if len(tag_names) > 1: - raise ValidationError(_('Multiple tags were given.')) + raise forms.ValidationError(_('Multiple tags were given.')) elif len(tag_names[0]) > settings.MAX_TAG_LENGTH: raise forms.ValidationError( _('A tag may be no more than %s characters long.') % -- cgit v1.2.3 From 29425a36c920e9b54e5860429ef3e3ce639fb155 Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:49 -0700 Subject: Imported Upstream version 0.3.1 --- tagging/._models.py | Bin 0 -> 187 bytes tagging/._settings.py | Bin 0 -> 184 bytes tagging/__init__.py | 48 +- tagging/models.py | 14 +- tagging/tests/._settings.py | Bin 0 -> 184 bytes tagging/tests/._tags.txt | Bin 0 -> 184 bytes tagging/tests/settings.py | 28 +- tagging/tests/tests.py | 1331 ++++++++++++++++++++++++++++--------------- 8 files changed, 953 insertions(+), 468 deletions(-) create mode 100644 tagging/._models.py create mode 100644 tagging/._settings.py create mode 100644 tagging/tests/._settings.py create mode 100644 tagging/tests/._tags.txt (limited to 'tagging') diff --git a/tagging/._models.py b/tagging/._models.py new file mode 100644 index 0000000..bdd7231 Binary files /dev/null and b/tagging/._models.py differ diff --git a/tagging/._settings.py b/tagging/._settings.py new file mode 100644 index 0000000..3c4ee38 Binary files /dev/null and b/tagging/._settings.py differ diff --git a/tagging/__init__.py b/tagging/__init__.py index 9241c20..03d6c03 100644 --- a/tagging/__init__.py +++ b/tagging/__init__.py @@ -1,8 +1,20 @@ -from django.utils.translation import ugettext as _ +VERSION = (0, 3, 1, "final", 0) -from tagging.managers import ModelTaggedItemManager, TagDescriptor -VERSION = (0, 3, 'pre') + +def get_version(): + if VERSION[3] == "final": + return "%s.%s.%s" % (VERSION[0], VERSION[1], VERSION[2]) + elif VERSION[3] == "dev": + if VERSION[2] == 0: + return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[3], VERSION[4]) + return "%s.%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3], VERSION[4]) + else: + return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3]) + + +__version__ = get_version() + class AlreadyRegistered(Exception): """ @@ -10,21 +22,41 @@ class AlreadyRegistered(Exception): """ pass + registry = [] + def register(model, tag_descriptor_attr='tags', tagged_item_manager_attr='tagged'): """ Sets the given model class up for working with tags. """ + + from tagging.managers import ModelTaggedItemManager, TagDescriptor + if model in registry: - raise AlreadyRegistered( - _('The model %s has already been registered.') % model.__name__) - registry.append(model) + raise AlreadyRegistered("The model '%s' has already been " + "registered." % model._meta.object_name) + if hasattr(model, tag_descriptor_attr): + raise AttributeError("'%s' already has an attribute '%s'. You must " + "provide a custom tag_descriptor_attr to register." % ( + model._meta.object_name, + tag_descriptor_attr, + ) + ) + if hasattr(model, tagged_item_manager_attr): + raise AttributeError("'%s' already has an attribute '%s'. You must " + "provide a custom tagged_item_manager_attr to register." % ( + model._meta.object_name, + tagged_item_manager_attr, + ) + ) # Add tag descriptor setattr(model, tag_descriptor_attr, TagDescriptor()) # Add custom manager - ModelTaggedItemManager().contribute_to_class(model, - tagged_item_manager_attr) + ModelTaggedItemManager().contribute_to_class(model, tagged_item_manager_attr) + + # Finally register in registry + registry.append(model) diff --git a/tagging/models.py b/tagging/models.py index d43f22d..860cf81 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -162,8 +162,18 @@ class TagManager(models.Manager): Passing a value for ``min_count`` implies ``counts=True``. """ - extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:]) - where, params = queryset.query.where.as_sql() + if getattr(queryset.query, 'get_compiler', None): + # Django 1.2+ + compiler = queryset.query.get_compiler(using='default') + extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) + where, params = queryset.query.where.as_sql( + compiler.quote_name_unless_alias, compiler.connection + ) + else: + # Django pre-1.2 + 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: diff --git a/tagging/tests/._settings.py b/tagging/tests/._settings.py new file mode 100644 index 0000000..6092bc4 Binary files /dev/null and b/tagging/tests/._settings.py differ diff --git a/tagging/tests/._tags.txt b/tagging/tests/._tags.txt new file mode 100644 index 0000000..3348eaa Binary files /dev/null and b/tagging/tests/._tags.txt differ diff --git a/tagging/tests/settings.py b/tagging/tests/settings.py index 26e659b..74eb909 100644 --- a/tagging/tests/settings.py +++ b/tagging/tests/settings.py @@ -3,22 +3,22 @@ DIRNAME = os.path.dirname(__file__) DEFAULT_CHARSET = 'utf-8' -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db') +test_engine = os.environ.get("TAGGING_TEST_ENGINE", "sqlite3") -#DATABASE_ENGINE = 'mysql' -#DATABASE_NAME = 'tagging_test' -#DATABASE_USER = 'root' -#DATABASE_PASSWORD = '' -#DATABASE_HOST = 'localhost' -#DATABASE_PORT = '3306' +DATABASE_ENGINE = test_engine +DATABASE_NAME = os.environ.get("TAGGING_DATABASE_NAME", "tagging_test") +DATABASE_USER = os.environ.get("TAGGING_DATABASE_USER", "") +DATABASE_PASSWORD = os.environ.get("TAGGING_DATABASE_PASSWORD", "") +DATABASE_HOST = os.environ.get("TAGGING_DATABASE_HOST", "localhost") + +if test_engine == "sqlite": + DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db') + DATABASE_HOST = "" +elif test_engine == "mysql": + DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 3306) +elif test_engine == "postgresql_psycopg2": + DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 5432) -#DATABASE_ENGINE = 'postgresql_psycopg2' -#DATABASE_NAME = 'tagging_test' -#DATABASE_USER = 'postgres' -#DATABASE_PASSWORD = '' -#DATABASE_HOST = 'localhost' -#DATABASE_PORT = '5432' INSTALLED_APPS = ( 'django.contrib.contenttypes', diff --git a/tagging/tests/tests.py b/tagging/tests/tests.py index 5d6d8b5..f5c9e37 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -1,458 +1,901 @@ # -*- coding: utf-8 -*- -r""" ->>> import os ->>> from django import forms ->>> from django.db.models import Q ->>> 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 + +import os +from django import forms +from django.db.models import Q +from django.test import TestCase +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, edit_string_for_tags, get_tag_list, get_tag, parse_tag_input +from tagging.utils import LINEAR ############# # 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) -[] ->>> get_tag_list('cheese toast') -[, ] ->>> get_tag_list('cheese,toast') -[, ] ->>> get_tag_list([]) -[] ->>> get_tag_list(['cheese', 'toast']) -[, ] ->>> get_tag_list([cheese.id, toast.id]) -[, ] ->>> get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) -[, ] ->>> get_tag_list([cheese, toast]) -[, ] ->>> get_tag_list((cheese, toast)) -(, ) ->>> get_tag_list(Tag.objects.filter(name__in=['cheese', '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) - ->>> get_tag('cheese') - ->>> get_tag(cheese.id) - ->>> 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. - +class TestParseTagInput(TestCase): + def test_with_simple_space_delimited_tags(self): + """ Test with simple space-delimited tags. """ + + self.assertEquals(parse_tag_input('one'), [u'one']) + self.assertEquals(parse_tag_input('one two'), [u'one', u'two']) + self.assertEquals(parse_tag_input('one two three'), [u'one', u'three', u'two']) + self.assertEquals(parse_tag_input('one one two two'), [u'one', u'two']) + + def test_with_comma_delimited_multiple_words(self): + """ Test with comma-delimited multiple words. + An unquoted comma in the input will trigger this. """ + + self.assertEquals(parse_tag_input(',one'), [u'one']) + self.assertEquals(parse_tag_input(',one two'), [u'one two']) + self.assertEquals(parse_tag_input(',one two three'), [u'one two three']) + self.assertEquals(parse_tag_input('a-one, a-two and a-three'), + [u'a-one', u'a-two and a-three']) + + def test_with_double_quoted_multiple_words(self): + """ Test with double-quoted multiple words. + A completed quote will trigger this. Unclosed quotes are ignored. """ + + self.assertEquals(parse_tag_input('"one'), [u'one']) + self.assertEquals(parse_tag_input('"one two'), [u'one', u'two']) + self.assertEquals(parse_tag_input('"one two three'), [u'one', u'three', u'two']) + self.assertEquals(parse_tag_input('"one two"'), [u'one two']) + self.assertEquals(parse_tag_input('a-one "a-two and a-three"'), + [u'a-one', u'a-two and a-three']) + + def test_with_no_loose_commas(self): + """ Test with no loose commas -- split on spaces. """ + self.assertEquals(parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) + + def test_with_loose_commas(self): + """ Loose commas - split on commas """ + self.assertEquals(parse_tag_input('"one", two three'), [u'one', u'two three']) + + def test_tags_with_double_quotes_can_contain_commas(self): + """ Double quotes can contain commas """ + self.assertEquals(parse_tag_input('a-one "a-two, and a-three"'), + [u'a-one', u'a-two, and a-three']) + self.assertEquals(parse_tag_input('"two", one, one, two, "one"'), + [u'one', u'two']) + + def test_with_naughty_input(self): + """ Test with naughty input. """ + + # Bad users! Naughty users! + self.assertEquals(parse_tag_input(None), []) + self.assertEquals(parse_tag_input(''), []) + self.assertEquals(parse_tag_input('"'), []) + self.assertEquals(parse_tag_input('""'), []) + self.assertEquals(parse_tag_input('"' * 7), []) + self.assertEquals(parse_tag_input(',,,,,,'), []) + self.assertEquals(parse_tag_input('",",",",",",","'), [u',']) + self.assertEquals(parse_tag_input('a-one "a-two" and "a-three'), + [u'a-one', u'a-three', u'a-two', u'and']) + +class TestNormalisedTagListInput(TestCase): + def setUp(self): + self.cheese = Tag.objects.create(name='cheese') + self.toast = Tag.objects.create(name='toast') + + def test_single_tag_object_as_input(self): + self.assertEquals(get_tag_list(self.cheese), [self.cheese]) + + def test_space_delimeted_string_as_input(self): + ret = get_tag_list('cheese toast') + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_comma_delimeted_string_as_input(self): + ret = get_tag_list('cheese,toast') + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_empty_list(self): + self.assertEquals(get_tag_list([]), []) + + def test_list_of_two_strings(self): + ret = get_tag_list(['cheese', 'toast']) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_tag_primary_keys(self): + ret = get_tag_list([self.cheese.id, self.toast.id]) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_strings_with_strange_nontag_string(self): + ret = get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_tag_instances(self): + ret = get_tag_list([self.cheese, self.toast]) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_tuple_of_instances(self): + ret = get_tag_list((self.cheese, self.toast)) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_tag_filter(self): + ret = get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast'])) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_invalid_input_mix_of_string_and_instance(self): + try: + get_tag_list(['cheese', self.toast]) + except ValueError, ve: + self.assertEquals(str(ve), + 'If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + + def test_with_invalid_input(self): + try: + get_tag_list(29) + except ValueError, ve: + self.assertEquals(str(ve), 'The tag input given was invalid.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + + def test_with_tag_instance(self): + self.assertEquals(get_tag(self.cheese), self.cheese) + + def test_with_string(self): + self.assertEquals(get_tag('cheese'), self.cheese) + + def test_with_primary_key(self): + self.assertEquals(get_tag(self.cheese.id), self.cheese) + + def test_nonexistent_tag(self): + self.assertEquals(get_tag('mouse'), None) + +class TestCalculateCloud(TestCase): + def setUp(self): + self.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) + self.tags.append(tag) + + def test_default_distribution(self): + sizes = {} + for tag in calculate_cloud(self.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 + self.assertEquals(sizes[1], 48) + self.assertEquals(sizes[2], 30) + self.assertEquals(sizes[3], 19) + self.assertEquals(sizes[4], 15) + self.assertEquals(sizes[5], 10) + + def test_linear_distribution(self): + sizes = {} + for tag in calculate_cloud(self.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 + self.assertEquals(sizes[1], 97) + self.assertEquals(sizes[2], 12) + self.assertEquals(sizes[3], 7) + self.assertEquals(sizes[4], 2) + self.assertEquals(sizes[5], 4) + + def test_invalid_distribution(self): + try: + calculate_cloud(self.tags, steps=5, distribution='cheese') + except ValueError, ve: + self.assertEquals(str(ve), 'Invalid distribution algorithm specified: cheese.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + ########### # Tagging # ########### -# Basic tagging ############################################################### - ->>> dead = Parrot.objects.create(state='dead') ->>> Tag.objects.update_tags(dead, 'foo,bar,"ter"') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.update_tags(dead, '"foo" bar "baz"') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'foo') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> 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.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) -[, , ] ->>> f1.tags = u'test4' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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.objects.update_tags(dead, 'foO bAr baZ') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'FOO') ->>> Tag.objects.get_for_object(dead) -[, , ] ->>> Tag.objects.add_tag(dead, 'Zip') ->>> Tag.objects.get_for_object(dead) -[, , , ] ->>> Tag.objects.update_tags(dead, None) ->>> f1.tags = u'TEST5' ->>> f1.save() ->>> Tag.objects.get_for_object(f1) -[] ->>> 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) -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, bar) -[, , ] - -# Intersections are supported ->>> TaggedItem.objects.get_by_model(Parrot, [foo, baz]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [foo, bar]) -[] ->>> TaggedItem.objects.get_by_model(Parrot, [bar, ter]) -[, ] - -# 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'])) -[] ->>> TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) -[, ] - -# 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') -[] ->>> TaggedItem.objects.get_by_model(Parrot, 'bar ter') -[, ] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) -[] ->>> TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) -[, ] - -# Issue 50 - Get by non-existent tag ->>> TaggedItem.objects.get_by_model(Parrot, 'argatrons') -[] - -# Unions ->>> TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) -[, , , ] ->>> TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) -[, , ] - -# 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) -[, ] ->>> TaggedItem.objects.get_related(l1, Link, num=1) -[] ->>> TaggedItem.objects.get_related(l4, Link) -[] - -# Limit related items ->>> TaggedItem.objects.get_related(l1, Link.objects.exclude(name='link 3')) -[] - -# 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) -[, , ] ->>> Tag.objects.update_tags(a1, 'tag6') ->>> TaggedItem.objects.get_related(a1, Link) -[] - -# 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)] - +class TestBasicTagging(TestCase): + def setUp(self): + self.dead_parrot = Parrot.objects.create(state='dead') + + def test_update_tags(self): + Tag.objects.update_tags(self.dead_parrot, 'foo,bar,"ter"') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('foo') in tags) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('ter') in tags) + + Tag.objects.update_tags(self.dead_parrot, '"foo" bar "baz"') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + def test_add_tag(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + # try to add a tag that already exists + Tag.objects.add_tag(self.dead_parrot, 'foo') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + # now add a tag that doesn't already exist + Tag.objects.add_tag(self.dead_parrot, 'zip') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 4) + self.failUnless(get_tag('zip') in tags) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + def test_add_tag_invalid_input_no_tags_specified(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + try: + Tag.objects.add_tag(self.dead_parrot, ' ') + except AttributeError, ae: + self.assertEquals(str(ae), 'No tags were given: " ".') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('an AttributeError exception was supposed to be raised!') + + def test_add_tag_invalid_input_multiple_tags_specified(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + try: + Tag.objects.add_tag(self.dead_parrot, 'one two') + except AttributeError, ae: + self.assertEquals(str(ae), 'Multiple tags were given: "one two".') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('an AttributeError exception was supposed to be raised!') + + def test_update_tags_exotic_characters(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, u'ŠĐĆŽćžšđ') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0].name, u'ŠĐĆŽćžšđ') + + Tag.objects.update_tags(self.dead_parrot, u'你好') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0].name, u'你好') + + def test_update_tags_with_none(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, None) + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 0) + +class TestModelTagField(TestCase): + """ Test the 'tags' field on models. """ + + def test_create_with_tags_specified(self): + f1 = FormTest.objects.create(tags=u'test3 test2 test1') + tags = Tag.objects.get_for_object(f1) + test1_tag = get_tag('test1') + test2_tag = get_tag('test2') + test3_tag = get_tag('test3') + self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEquals(len(tags), 3) + self.failUnless(test1_tag in tags) + self.failUnless(test2_tag in tags) + self.failUnless(test3_tag in tags) + + def test_update_via_tags_field(self): + f1 = FormTest.objects.create(tags=u'test3 test2 test1') + tags = Tag.objects.get_for_object(f1) + test1_tag = get_tag('test1') + test2_tag = get_tag('test2') + test3_tag = get_tag('test3') + self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEquals(len(tags), 3) + self.failUnless(test1_tag in tags) + self.failUnless(test2_tag in tags) + self.failUnless(test3_tag in tags) + + f1.tags = u'test4' + f1.save() + tags = Tag.objects.get_for_object(f1) + test4_tag = get_tag('test4') + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0], test4_tag) + + f1.tags = '' + f1.save() + tags = Tag.objects.get_for_object(f1) + self.assertEquals(len(tags), 0) + +class TestSettings(TestCase): + def setUp(self): + self.original_force_lower_case_tags = settings.FORCE_LOWERCASE_TAGS + self.dead_parrot = Parrot.objects.create(state='dead') + + def tearDown(self): + settings.FORCE_LOWERCASE_TAGS = self.original_force_lower_case_tags + + def test_force_lowercase_tags(self): + """ Test forcing tags to lowercase. """ + + settings.FORCE_LOWERCASE_TAGS = True + + Tag.objects.update_tags(self.dead_parrot, 'foO bAr Ter') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + foo_tag = get_tag('foo') + bar_tag = get_tag('bar') + ter_tag = get_tag('ter') + self.failUnless(foo_tag in tags) + self.failUnless(bar_tag in tags) + self.failUnless(ter_tag in tags) + + Tag.objects.update_tags(self.dead_parrot, 'foO bAr baZ') + tags = Tag.objects.get_for_object(self.dead_parrot) + baz_tag = get_tag('baz') + self.assertEquals(len(tags), 3) + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + + Tag.objects.add_tag(self.dead_parrot, 'FOO') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + + Tag.objects.add_tag(self.dead_parrot, 'Zip') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 4) + zip_tag = get_tag('zip') + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + self.failUnless(zip_tag in tags) + + f1 = FormTest.objects.create() + f1.tags = u'TEST5' + f1.save() + tags = Tag.objects.get_for_object(f1) + test5_tag = get_tag('test5') + self.assertEquals(len(tags), 1) + self.failUnless(test5_tag in tags) + self.assertEquals(f1.tags, u'test5') + +class TestTagUsageForModelBaseCase(TestCase): + def test_tag_usage_for_model_empty(self): + self.assertEquals(Tag.objects.usage_for_model(Parrot), []) + +class TestTagUsageForModel(TestCase): + def setUp(self): + 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) + + def test_tag_usage_for_model(self): + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 3) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 3) in relevant_attribute_list) + + def test_tag_usage_for_model_with_min_count(self): + tag_usage = Tag.objects.usage_for_model(Parrot, min_count = 2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 3) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 3) in relevant_attribute_list) + + def test_tag_usage_with_filter_on_model_objects(self): + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more')) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p')) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'baz', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 0) + +class TestTagsRelatedForModel(TestCase): + def setUp(self): + 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) + + def test_related_for_model_with_tag_query_sets_as_input(self): + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False) + relevant_attribute_list = [tag.name for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless(u'baz' in relevant_attribute_list) + self.failUnless(u'foo' in relevant_attribute_list) + self.failUnless(u'ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 0) + + def test_related_for_model_with_tag_strings_as_input(self): + # Once again, with feeling (strings) + related_tags = Tag.objects.related_for_model('bar', Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model('bar', Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model('bar', Parrot, counts=False) + relevant_attribute_list = [tag.name for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless(u'baz' in relevant_attribute_list) + self.failUnless(u'foo' in relevant_attribute_list) + self.failUnless(u'ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 0) + +class TestGetTaggedObjectsByModel(TestCase): + def setUp(self): + 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) + + self.foo = Tag.objects.get(name='foo') + self.bar = Tag.objects.get(name='bar') + self.baz = Tag.objects.get(name='baz') + self.ter = Tag.objects.get(name='ter') + + self.pining_for_the_fjords_parrot = Parrot.objects.get(state='pining for the fjords') + self.passed_on_parrot = Parrot.objects.get(state='passed on') + self.no_more_parrot = Parrot.objects.get(state='no more') + self.late_parrot = Parrot.objects.get(state='late') + + def test_get_by_model_simple(self): + parrots = TaggedItem.objects.get_by_model(Parrot, self.foo) + self.assertEquals(len(parrots), 2) + self.failUnless(self.no_more_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, self.bar) + self.assertEquals(len(parrots), 3) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + def test_get_by_model_intersection(self): + parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.baz]) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.bar]) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, [self.bar, self.ter]) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + # Issue 114 - Intersection with non-existant tags + parrots = TaggedItem.objects.get_intersection_by_model(Parrot, []) + self.assertEquals(len(parrots), 0) + + def test_get_by_model_with_tag_querysets_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar'])) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_model_with_strings_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, 'foo baz') + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, 'foo bar') + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, 'bar ter') + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_model_with_lists_of_strings_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_nonexistent_tag(self): + # Issue 50 - Get by non-existent tag + parrots = TaggedItem.objects.get_by_model(Parrot, 'argatrons') + self.assertEquals(len(parrots), 0) + + def test_get_union_by_model(self): + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) + self.assertEquals(len(parrots), 4) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.no_more_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) + self.assertEquals(len(parrots), 3) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + # Issue 114 - Union with non-existant tags + parrots = TaggedItem.objects.get_union_by_model(Parrot, []) + self.assertEquals(len(parrots), 0) + +class TestGetRelatedTaggedItems(TestCase): + def setUp(self): + 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) + + self.l1 = Link.objects.create(name='link 1') + Tag.objects.update_tags(self.l1, 'tag1 tag2 tag3 tag4 tag5') + self.l2 = Link.objects.create(name='link 2') + Tag.objects.update_tags(self.l2, 'tag1 tag2 tag3') + self.l3 = Link.objects.create(name='link 3') + Tag.objects.update_tags(self.l3, 'tag1') + self.l4 = Link.objects.create(name='link 4') + + self.a1 = Article.objects.create(name='article 1') + Tag.objects.update_tags(self.a1, 'tag1 tag2 tag3 tag4') + + def test_get_related_objects_of_same_model(self): + related_objects = TaggedItem.objects.get_related(self.l1, Link) + self.assertEquals(len(related_objects), 2) + self.failUnless(self.l2 in related_objects) + self.failUnless(self.l3 in related_objects) + + related_objects = TaggedItem.objects.get_related(self.l4, Link) + self.assertEquals(len(related_objects), 0) + + def test_get_related_objects_of_same_model_limited_number_of_results(self): + # This fails on Oracle because it has no support for a 'LIMIT' clause. + # See http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:127412348064 + + # ask for no more than 1 result + related_objects = TaggedItem.objects.get_related(self.l1, Link, num=1) + self.assertEquals(len(related_objects), 1) + self.failUnless(self.l2 in related_objects) + + def test_get_related_objects_of_same_model_limit_related_items(self): + related_objects = TaggedItem.objects.get_related(self.l1, Link.objects.exclude(name='link 3')) + self.assertEquals(len(related_objects), 1) + self.failUnless(self.l2 in related_objects) + + def test_get_related_objects_of_different_model(self): + related_objects = TaggedItem.objects.get_related(self.a1, Link) + self.assertEquals(len(related_objects), 3) + self.failUnless(self.l1 in related_objects) + self.failUnless(self.l2 in related_objects) + self.failUnless(self.l3 in related_objects) + + Tag.objects.update_tags(self.a1, 'tag6') + related_objects = TaggedItem.objects.get_related(self.a1, Link) + self.assertEquals(len(related_objects), 0) + +class TestTagUsageForQuerySet(TestCase): + def setUp(self): + 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) + + def test_tag_usage_for_queryset(self): + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state='no more'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state__startswith='p'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'baz', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 0) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'bar', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l'))) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state='passed on'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state__startswith='p'), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(Q(perch__size__gt=6) | Q(perch__smelly=False)), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(perch__smelly=True).filter(state__startswith='l'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + ################ # 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.'] -""" +class TestTagFieldInForms(TestCase): + def test_tag_field_in_modelform(self): + # Ensure that automatically created forms use TagField + class TestForm(forms.ModelForm): + class Meta: + model = FormTest + + form = TestForm() + self.assertEquals(form.fields['tags'].__class__.__name__, 'TagField') + + def test_recreation_of_tag_list_string_representations(self): + plain = Tag.objects.create(name='plain') + spaces = Tag.objects.create(name='spa ces') + comma = Tag.objects.create(name='com,ma') + self.assertEquals(edit_string_for_tags([plain]), u'plain') + self.assertEquals(edit_string_for_tags([plain, spaces]), u'plain, spa ces') + self.assertEquals(edit_string_for_tags([plain, spaces, comma]), u'plain, spa ces, "com,ma"') + self.assertEquals(edit_string_for_tags([plain, comma]), u'plain "com,ma"') + self.assertEquals(edit_string_for_tags([comma, spaces]), u'"com,ma", spa ces') + + def test_tag_d_validation(self): + t = TagField() + self.assertEquals(t.clean('foo'), u'foo') + self.assertEquals(t.clean('foo bar baz'), u'foo bar baz') + self.assertEquals(t.clean('foo,bar,baz'), u'foo,bar,baz') + self.assertEquals(t.clean('foo, bar, baz'), u'foo, bar, baz') + self.assertEquals(t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'), + u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') + try: + t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') + except forms.ValidationError, ve: + self.assertEquals(str(ve), "[u'Each tag may be no more than 50 characters long.']") + except Exception, e: + raise e + else: + raise self.failureException('a ValidationError exception was supposed to have been raised.') -- cgit v1.2.3 From e54ad0924908aec2a7970c957551787875317b2b Mon Sep 17 00:00:00 2001 From: SVN-Git Migration Date: Thu, 8 Oct 2015 11:51:53 -0700 Subject: Fix tag_weight when using logarithmic distribution When logarithmic distribution is used _calculate_tag_weight may return weight greater than maximum threshold. Because of this the attribute font_size of the most frequently used tag may be left unset. Upstream: http://code.google.com/p/django-tagging/issues/detail?id=91 Debian: #525770 Patch-Name: fix_calc_tag_weight --- tagging/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tagging') diff --git a/tagging/utils.py b/tagging/utils.py index e89bab0..759e8d8 100644 --- a/tagging/utils.py +++ b/tagging/utils.py @@ -232,7 +232,7 @@ def _calculate_tag_weight(weight, max_weight, distribution): if distribution == LINEAR or max_weight == 1: return weight elif distribution == LOGARITHMIC: - return math.log(weight) * max_weight / math.log(max_weight) + return min((max_weight, 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): -- cgit v1.2.3 From f50f19a1d9495ca98bf0e5de941ab9c21bcdbf91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= Date: Thu, 8 Oct 2015 11:51:54 -0700 Subject: Fix settings for test suite Origin: vendor Patch-Name: fix-testsuite.patch --- tagging/tests/settings.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) (limited to 'tagging') diff --git a/tagging/tests/settings.py b/tagging/tests/settings.py index 74eb909..1985118 100644 --- a/tagging/tests/settings.py +++ b/tagging/tests/settings.py @@ -3,22 +3,14 @@ DIRNAME = os.path.dirname(__file__) DEFAULT_CHARSET = 'utf-8' -test_engine = os.environ.get("TAGGING_TEST_ENGINE", "sqlite3") - -DATABASE_ENGINE = test_engine -DATABASE_NAME = os.environ.get("TAGGING_DATABASE_NAME", "tagging_test") -DATABASE_USER = os.environ.get("TAGGING_DATABASE_USER", "") -DATABASE_PASSWORD = os.environ.get("TAGGING_DATABASE_PASSWORD", "") -DATABASE_HOST = os.environ.get("TAGGING_DATABASE_HOST", "localhost") - -if test_engine == "sqlite": - DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db') - DATABASE_HOST = "" -elif test_engine == "mysql": - DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 3306) -elif test_engine == "postgresql_psycopg2": - DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 5432) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(DIRNAME, 'tagging_test.db'), + } +} +SECRET_KEY = 'liewoo1jie7TahTao3ci7xayee8gieg9ukee' INSTALLED_APPS = ( 'django.contrib.contenttypes', -- cgit v1.2.3 From 210bfd7f97afe0162e682cbd2530115cf09fd51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= Date: Thu, 8 Oct 2015 11:51:55 -0700 Subject: Add django 1.7 compatibility Bug-Debian: http://bugs.debian.org/755624 Origin: vendor Last-Update: 2014-08-06 Patch-Name: django-1.7-compat.patch --- tagging/models.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'tagging') diff --git a/tagging/models.py b/tagging/models.py index 860cf81..e13b077 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -17,6 +17,8 @@ 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 +import collections + qn = connection.ops.quote_name ############ @@ -166,9 +168,16 @@ class TagManager(models.Manager): # Django 1.2+ compiler = queryset.query.get_compiler(using='default') extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) - where, params = queryset.query.where.as_sql( - compiler.quote_name_unless_alias, compiler.connection - ) + if isinstance(compiler, collections.Callable): + # Django 1.7+ + where, params = queryset.query.where.as_sql( + compiler, compiler.connection + ) + else: + # Django 1.2-1.6 + where, params = queryset.query.where.as_sql( + compiler.quote_name_unless_alias, compiler.connection + ) else: # Django pre-1.2 extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:]) -- cgit v1.2.3