diff --git a/setup.py b/setup.py index 0670dfe1..2ee53205 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ def fullsplit(path, result=None): setup( name = 'django-tagging', - version = tagging.get_version(), + version = '0.0.1', description = 'Generic tagging application for Django', author = 'Jonathan Buchanan', author_email = 'jonathan.buchanan@gmail.com', diff --git a/tagging/__init__.py b/tagging/__init__.py index fb378862..3482860a 100644 --- a/tagging/__init__.py +++ b/tagging/__init__.py @@ -1,62 +1,15 @@ -VERSION = (0, 4, 0, "dev", 1) +""" +Django-tagging +""" +__version__ = '0.4.6' +__license__ = 'BSD License' +__author__ = 'Jonathan Buchanan' +__author_email__ = 'jonathan.buchanan@gmail.com' +__maintainer__ = 'Fantomas42' +__maintainer_email__ = 'fantomas42@gmail.com' -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]) +__url__ = 'https://github.com/Fantomas42/django-tagging' - -__version__ = get_version() - - -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. - """ - - from tagging.managers import ModelTaggedItemManager, TagDescriptor - - if model in registry: - 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) - - # Finally register in registry - registry.append(model) +default_app_config = 'tagging.apps.TaggingConfig' diff --git a/tagging/admin.py b/tagging/admin.py index bec3922e..8eb9833a 100644 --- a/tagging/admin.py +++ b/tagging/admin.py @@ -1,13 +1,16 @@ +""" +Admin components for tagging. +""" from django.contrib import admin -from tagging.models import Tag, TaggedItem + +from tagging.models import Tag +from tagging.models import TaggedItem from tagging.forms import TagAdminForm + class TagAdmin(admin.ModelAdmin): form = TagAdminForm + admin.site.register(TaggedItem) admin.site.register(Tag, TagAdmin) - - - - diff --git a/tagging/apps.py b/tagging/apps.py new file mode 100644 index 00000000..492318c7 --- /dev/null +++ b/tagging/apps.py @@ -0,0 +1,14 @@ +""" +Apps for tagging. +""" +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class TaggingConfig(AppConfig): + """ + Config for Tagging application. + """ + name = 'tagging' + label = 'tagging' + verbose_name = _('Tagging') diff --git a/tagging/fields.py b/tagging/fields.py index d8a7b198..7a05f990 100644 --- a/tagging/fields.py +++ b/tagging/fields.py @@ -8,6 +8,8 @@ from tagging import settings from tagging.models import Tag from tagging.utils import edit_string_for_tags +from tagging.forms import TagField as TagFormField + class TagField(CharField): """ @@ -18,8 +20,6 @@ class TagField(CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 255) kwargs['blank'] = kwargs.get('blank', True) - kwargs['default'] = kwargs.get('default', '') - self._initialized = False super(TagField, self).__init__(*args, **kwargs) def contribute_to_class(self, cls, name): @@ -54,6 +54,14 @@ class Link(models.Model): 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): @@ -61,24 +69,19 @@ 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) + 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): + def _save(self, **kwargs): # signal, sender, instance): """ Save tags back to the database """ tags = self._get_instance_tag_cache(kwargs['instance']) - Tag.objects.update_tags(kwargs['instance'], tags) - - def _update(self, **kwargs): #signal, sender, instance): - """ - Update tag cache from TaggedItem objects. - """ - instance = kwargs['instance'] - self._update_instance_tag_cache(instance) + if tags is not None: + Tag.objects.update_tags(kwargs['instance'], tags) def __delete__(self, instance): """ @@ -90,31 +93,24 @@ def _get_instance_tag_cache(self, instance): """ Helper: get an instance's tag cache. """ - if not self._initialized: - self._initialized = True - self._update(instance=instance) return getattr(instance, '_%s_cache' % self.attname, None) def _set_instance_tag_cache(self, instance, tags): """ Helper: set an instance's tag cache. """ + # The next instruction does nothing particular, + # but needed to by-pass the deferred fields system + # when saving an instance, which check the keys present + # in instance.__dict__. + # The issue is introducted in Django 1.10 + instance.__dict__[self.attname] = tags setattr(instance, '_%s_cache' % self.attname, tags) - def _update_instance_tag_cache(self, instance): - """ - Helper: update an instance's tag cache from actual Tags. - """ - # for an unsaved object, leave the default value alone - if instance.pk is not None: - tags = edit_string_for_tags(Tag.objects.get_for_object(instance)) - self._set_instance_tag_cache(instance, tags) - def get_internal_type(self): return 'CharField' def formfield(self, **kwargs): - from tagging import forms - defaults = {'form_class': forms.TagField} + defaults = {'form_class': TagFormField} defaults.update(kwargs) return super(TagField, self).formfield(**defaults) diff --git a/tagging/forms.py b/tagging/forms.py index a2d9fd94..e597f2de 100644 --- a/tagging/forms.py +++ b/tagging/forms.py @@ -1,5 +1,5 @@ """ -Tagging components for Django's form library. +Form components for tagging. """ from django import forms from django.utils.translation import ugettext as _ @@ -8,21 +8,20 @@ from tagging.models import Tag from tagging.utils import parse_tag_input + class TagAdminForm(forms.ModelForm): class Meta: model = Tag + fields = ('name',) def clean_name(self): value = self.cleaned_data['name'] tag_names = parse_tag_input(value) if len(tag_names) > 1: 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.') % - settings.MAX_TAG_LENGTH) return value + class TagField(forms.CharField): """ A ``CharField`` which validates that its input is a valid list of @@ -30,11 +29,9 @@ class TagField(forms.CharField): """ 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) + settings.MAX_TAG_LENGTH) return value diff --git a/tagging/generic.py b/tagging/generic.py index 75d1b8e0..770e928b 100644 --- a/tagging/generic.py +++ b/tagging/generic.py @@ -1,5 +1,9 @@ +""" +Generic components for tagging. +""" 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 @@ -15,7 +19,8 @@ def fetch_content_objects(tagged_items, select_related_for=None): ``ContentType``) for which ``select_related`` should be used when retrieving model instances. """ - if select_related_for is None: select_related_for = [] + if select_related_for is None: + select_related_for = [] # Group content object pks by their content type pks objects = {} @@ -27,9 +32,11 @@ def fetch_content_objects(tagged_items, select_related_for=None): 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) + 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) + 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' diff --git a/tagging/managers.py b/tagging/managers.py index 02cd1c23..d85c260a 100644 --- a/tagging/managers.py +++ b/tagging/managers.py @@ -1,17 +1,18 @@ """ -Custom managers for Django models registered with the tagging -application. +Custom managers for tagging. """ -from django.contrib.contenttypes.models import ContentType from django.db import models +from django.contrib.contenttypes.models import ContentType + +from tagging.models import Tag +from tagging.models import TaggedItem -from tagging.models import Tag, TaggedItem class ModelTagManager(models.Manager): """ A manager for retrieving tags for a particular model. """ - def get_query_set(self): + def get_queryset(self): ctype = ContentType.objects.get_for_model(self.model) return Tag.objects.filter( items__content_type__pk=ctype.pk).distinct() @@ -25,6 +26,7 @@ def related(self, tags, *args, **kwargs): def usage(self, *args, **kwargs): return Tag.objects.usage_for_model(self.model, *args, **kwargs) + class ModelTaggedItemManager(models.Manager): """ A manager for retrieving model instances based on their tags. @@ -47,6 +49,7 @@ def with_any(self, tags, queryset=None): else: return TaggedItem.objects.get_union_by_model(queryset, tags) + class TagDescriptor(object): """ A descriptor which provides access to a ``ModelTagManager`` for diff --git a/tagging/migrations/0001_initial.py b/tagging/migrations/0001_initial.py new file mode 100644 index 00000000..b5f56a27 --- /dev/null +++ b/tagging/migrations/0001_initial.py @@ -0,0 +1,54 @@ +from django.db import models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('name', models.CharField( + unique=True, max_length=50, + verbose_name='name', db_index=True)), + ], + options={ + 'ordering': ('name',), + 'verbose_name': 'tag', + 'verbose_name_plural': 'tags', + }, + ), + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField( + verbose_name='object id', db_index=True)), + ('content_type', models.ForeignKey( + verbose_name='content type', + on_delete=models.SET_NULL, + to='contenttypes.ContentType')), + ('tag', models.ForeignKey( + related_name='items', verbose_name='tag', + on_delete=models.SET_NULL, + to='tagging.Tag')), + ], + options={ + 'verbose_name': 'tagged item', + 'verbose_name_plural': 'tagged items', + }, + ), + migrations.AlterUniqueTogether( + name='taggeditem', + unique_together=set([('tag', 'content_type', 'object_id')]), + ), + ] diff --git a/tagging/migrations/0002_on_delete.py b/tagging/migrations/0002_on_delete.py new file mode 100644 index 00000000..60be3050 --- /dev/null +++ b/tagging/migrations/0002_on_delete.py @@ -0,0 +1,30 @@ +from django.db import models +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tagging', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='taggeditem', + name='content_type', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='contenttypes.ContentType', + verbose_name='content type'), + ), + migrations.AlterField( + model_name='taggeditem', + name='tag', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='items', + to='tagging.Tag', + verbose_name='tag'), + ), + ] diff --git a/tagging/migrations/0003_adapt_max_tag_length.py b/tagging/migrations/0003_adapt_max_tag_length.py new file mode 100644 index 00000000..17236b04 --- /dev/null +++ b/tagging/migrations/0003_adapt_max_tag_length.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tagging', '0002_on_delete'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField( + unique=True, + max_length=getattr(settings, 'MAX_TAG_LENGTH', 50), + verbose_name='name', + db_index=True), + ), + ] diff --git a/tagging/migrations/__init__.py b/tagging/migrations/__init__.py new file mode 100644 index 00000000..805bae97 --- /dev/null +++ b/tagging/migrations/__init__.py @@ -0,0 +1,3 @@ +""" +Migrations for tagging. +""" diff --git a/tagging/models.py b/tagging/models.py index ff54d912..7867cb2e 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -1,35 +1,33 @@ """ -Models and managers for generic tagging. +Models and managers for tagging. """ -# Python 2.3 compatibility -try: - set -except NameError: - from sets import Set as set - import urllib -try: - # django 1.5-ish - from django.contrib.contenttypes.generic import GenericForeignKey -except ImportError: - # django 1.10-ish - from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.db import connection, models -from django.db.models.query import QuerySet + +from django.db import models +from django.db import connection +from django.utils.encoding import smart_text +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey 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.utils import get_tag_list +from tagging.utils import calculate_cloud +from tagging.utils import parse_tag_input +from tagging.utils import get_queryset_and_model + qn = connection.ops.quote_name + ############ # Managers # ############ class TagManager(models.Manager): + def update_tags(self, obj, tag_names): """ Update tags associated with an object. @@ -42,18 +40,23 @@ def update_tags(self, obj, tag_names): 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 \ + 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() + 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) + TaggedItem._default_manager.get_or_create( + content_type_id=ctype.pk, + object_id=obj.pk, + tag=tag, + ) def add_tag(self, obj, tag_name): """ @@ -61,9 +64,11 @@ def add_tag(self, obj, tag_name): """ tag_names = parse_tag_input(tag_name) if not len(tag_names): - raise AttributeError(_('No tags were given: "%s".') % tag_name) + raise AttributeError( + _('No tags were given: "%s".') % tag_name) if len(tag_names) > 1: - raise AttributeError(_('Multiple tags were given: "%s".') % tag_name) + raise AttributeError( + _('Multiple tags were given: "%s".') % tag_name) tag_name = tag_names[0] if settings.FORCE_LOWERCASE_TAGS: tag_name = tag_name.lower() @@ -81,12 +86,14 @@ def get_for_object(self, 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): + 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 + 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)) @@ -118,7 +125,8 @@ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extr params.append(min_count) cursor = connection.cursor() - cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params) + cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), + params) tags = [] for row in cursor.fetchall(): t = self.model(*row[:2]) @@ -127,7 +135,8 @@ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extr tags.append(t) return tags - def usage_for_model(self, model, counts=False, min_count=None, filters=None): + 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. @@ -145,7 +154,8 @@ def usage_for_model(self, model, counts=False, min_count=None, filters=None): of field lookups to be applied to the given Model as the ``filters`` argument. """ - if filters is None: filters = {} + if filters is None: + filters = {} queryset = model._default_manager.filter() for f in filters.items(): @@ -167,24 +177,16 @@ def usage_for_queryset(self, queryset, counts=False, min_count=None): greater than or equal to ``min_count`` will be returned. Passing a value for ``min_count`` implies ``counts=True``. """ - - 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() + compiler = queryset.query.get_compiler(using=queryset.db) + where, params = compiler.compile(queryset.query.where) + extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) if where: extra_criteria = 'AND %s' % where else: extra_criteria = '' - return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params) + 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): """ @@ -199,13 +201,16 @@ def related_for_model(self, tags, model, counts=False, min_count=None): 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 + 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 + 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 ( @@ -222,12 +227,14 @@ def related_for_model(self, tags, model, counts=False, min_count=None): %(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 '', + '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 '', + '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 @@ -273,6 +280,7 @@ def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC, 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`` @@ -286,6 +294,7 @@ class TaggedItemManager(models.Manager): 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): """ Create a ``QuerySet`` containing instances of the specified @@ -411,7 +420,8 @@ def get_related(self, obj, queryset_or_model, num=None): 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 + 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 @@ -427,12 +437,18 @@ def get_related(self, obj, queryset_or_model, num=None): GROUP BY %(model_pk)s ORDER BY %(count)s DESC %(limit_offset)s""" + try: + tagging_table = qn(self.model._meta.get_field( + 'tag').remote_field.model._meta.db_table) + except AttributeError: # Django < 1.9 + tagging_table = qn(self.model._meta.get_field( + 'tag').rel.to._meta.db_table) 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), + 'tag': tagging_table, 'content_type_id': content_type.pk, 'related_content_type_id': related_content_type.pk, # Hardcoding this for now just to get tests working again - this @@ -447,23 +463,27 @@ def get_related(self, obj, queryset_or_model, num=None): 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 - # clobber the ordering. + # 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 \ + return [object_dict[object_id] for object_id in object_ids if object_id in object_dict] else: return [] + ########## # Models # ########## +@python_2_unicode_compatible class Tag(models.Model): """ A tag. """ - name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) + name = models.CharField( + _('name'), max_length=settings.MAX_TAG_LENGTH, + unique=True, db_index=True) objects = TagManager() @@ -472,22 +492,36 @@ class Meta: verbose_name = _('tag') verbose_name_plural = _('tags') - def __unicode__(self): - return self.name - @property def url_name(self): - return urllib.quote(self.name.encode('UTF-8'), safe='') + return urllib.quote(self.name.encode('utf-8'), safe='') + + def __str__(self): + return self.name +@python_2_unicode_compatible 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 = GenericForeignKey('content_type', 'object_id') + tag = models.ForeignKey( + Tag, + verbose_name=_('tag'), + related_name='items', + on_delete=models.CASCADE) + + content_type = models.ForeignKey( + ContentType, + verbose_name=_('content type'), + on_delete=models.CASCADE) + + object_id = models.PositiveIntegerField( + _('object id'), + db_index=True) + + object = GenericForeignKey( + 'content_type', 'object_id') objects = TaggedItemManager() @@ -497,5 +531,5 @@ class Meta: verbose_name = _('tagged item') verbose_name_plural = _('tagged items') - def __unicode__(self): - return u'%s [%s]' % (self.object, self.tag) + def __str__(self): + return '%s [%s]' % (smart_text(self.object), smart_text(self.tag)) diff --git a/tagging/registry.py b/tagging/registry.py new file mode 100644 index 00000000..30c00ae7 --- /dev/null +++ b/tagging/registry.py @@ -0,0 +1,51 @@ +""" +Registery for tagging. +""" +from tagging.managers import TagDescriptor +from tagging.managers import ModelTaggedItemManager + +registry = [] + + +class AlreadyRegistered(Exception): + """ + An attempt was made to register a model more than once. + """ + pass + + +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._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) + + # Finally register in registry + registry.append(model) diff --git a/tagging/settings.py b/tagging/settings.py index 1d6224cd..558349c7 100644 --- a/tagging/settings.py +++ b/tagging/settings.py @@ -8,6 +8,6 @@ # 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. +# 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/__init__.py b/tagging/templatetags/__init__.py index e69de29b..ab524e26 100644 --- a/tagging/templatetags/__init__.py +++ b/tagging/templatetags/__init__.py @@ -0,0 +1,3 @@ +""" +Templatetags module for tagging. +""" diff --git a/tagging/templatetags/tagging_tags.py b/tagging/templatetags/tagging_tags.py index 11d31ccb..4e179fb4 100644 --- a/tagging/templatetags/tagging_tags.py +++ b/tagging/templatetags/tagging_tags.py @@ -1,12 +1,22 @@ -from django.db.models import get_model -from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable +""" +Templatetags for tagging. +""" +from django.template import Node +from django.template import Library +from django.template import Variable +from django.template import TemplateSyntaxError +from django.apps.registry import apps from django.utils.translation import ugettext as _ -from tagging.models import Tag, TaggedItem -from tagging.utils import LINEAR, LOGARITHMIC +from tagging.utils import LINEAR +from tagging.utils import LOGARITHMIC +from tagging.models import Tag +from tagging.models import TaggedItem + register = Library() + class TagsForModelNode(Node): def __init__(self, model, context_var, counts): self.model = model @@ -14,12 +24,16 @@ def __init__(self, model, context_var, counts): self.counts = counts def render(self, context): - model = get_model(*self.model.split('.')) + model = apps.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) + 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 @@ -27,13 +41,16 @@ def __init__(self, model, context_var, **kwargs): self.kwargs = kwargs def render(self, context): - model = get_model(*self.model.split('.')) + model = apps.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) + 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) @@ -44,6 +61,7 @@ def render(self, context): Tag.objects.get_for_object(self.obj.resolve(context)) return '' + class TaggedObjectsNode(Node): def __init__(self, tag, model, context_var): self.tag = Variable(tag) @@ -51,13 +69,16 @@ def __init__(self, tag, model, context_var): self.model = model def render(self, context): - model = get_model(*self.model.split('.')) + model = apps.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)) + 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 @@ -86,19 +107,26 @@ def do_tags_for_model(parser, token): 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]) + 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]) + 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]) + 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]) + 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 @@ -132,19 +160,25 @@ def do_tag_cloud_for_model(parser, token): 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 %} + {% 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]) + 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]) + 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]) + 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('=') @@ -152,32 +186,42 @@ def do_tag_cloud_for_model(parser, token): 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, - }) + 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] + 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'") % { + 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, - 'value': value, }) - else: - raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % { + except ValueError: + raise TemplateSyntaxError( + _("%(tag)s tag was given a badly " + "formatted option: '%(option)s'") % { 'tag': bits[0], - 'option': name, + 'option': bits[i], }) - 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 @@ -193,11 +237,14 @@ def do_tags_for_object(parser, token): """ bits = token.contents.split() if len(bits) != 4: - raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0]) + 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]) + 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 @@ -218,13 +265,17 @@ def do_tagged_objects(parser, token): """ bits = token.contents.split() if len(bits) != 6: - raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0]) + 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]) + 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]) + 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) diff --git a/tagging/tests/__init__.py b/tagging/tests/__init__.py index e69de29b..f0203780 100644 --- a/tagging/tests/__init__.py +++ b/tagging/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for tagging. +""" diff --git a/tagging/tests/models.py b/tagging/tests/models.py index e3274ff3..c240ff16 100644 --- a/tagging/tests/models.py +++ b/tagging/tests/models.py @@ -1,42 +1,57 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible from tagging.fields import TagField + class Perch(models.Model): size = models.IntegerField() smelly = models.BooleanField(default=True) + +@python_2_unicode_compatible class Parrot(models.Model): state = models.CharField(max_length=50) - perch = models.ForeignKey(Perch, null=True) + perch = models.ForeignKey(Perch, null=True, + on_delete=models.CASCADE) - def __unicode__(self): + def __str__(self): return self.state class Meta: ordering = ['state'] + +@python_2_unicode_compatible class Link(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name class Meta: ordering = ['name'] + +@python_2_unicode_compatible class Article(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name class Meta: ordering = ['name'] + class FormTest(models.Model): tags = TagField('Test', help_text='Test') + class FormTestNull(models.Model): tags = TagField(null=True) + +class FormMultipleFieldTest(models.Model): + tagging_field = TagField('Test', help_text='Test') + name = models.CharField(max_length=50) diff --git a/tagging/tests/settings.py b/tagging/tests/settings.py index 74eb9096..3648f2ea 100644 --- a/tagging/tests/settings.py +++ b/tagging/tests/settings.py @@ -1,27 +1,39 @@ +"""Tests settings""" import os -DIRNAME = os.path.dirname(__file__) -DEFAULT_CHARSET = 'utf-8' +SECRET_KEY = 'secret-key' -test_engine = os.environ.get("TAGGING_TEST_ENGINE", "sqlite3") +DATABASES = { + 'default': { + 'NAME': 'tagging.db', + 'ENGINE': 'django.db.backends.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") +DATABASE_ENGINE = os.environ.get('DATABASE_ENGINE') +if DATABASE_ENGINE == 'postgres': + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'tagging', + 'USER': 'postgres', + 'HOST': 'localhost' + } + } +elif DATABASE_ENGINE == 'mysql': + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'zinnia', + 'USER': 'root', + '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) - - -INSTALLED_APPS = ( +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.sessions', 'django.contrib.contenttypes', 'tagging', 'tagging.tests', -) +] diff --git a/tagging/tests/tests.py b/tagging/tests/tests.py index 18524445..13684144 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -1,453 +1,561 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import os + from django import forms +from django.utils import six from django.db.models import Q from django.test import TestCase -from tagging.forms import TagField +from django.test.utils import override_settings +from django.core.exceptions import ImproperlyConfigured + from tagging import settings -from tagging.models import Tag, TaggedItem -from tagging.tests.models import Article, Link, Perch, Parrot, FormTest, FormTestNull -from tagging.utils import calculate_cloud, edit_string_for_tags, get_tag_list, get_tag, parse_tag_input +from tagging.forms import TagField +from tagging.forms import TagAdminForm +from tagging.models import Tag +from tagging.models import TaggedItem +from tagging.tests.models import Article +from tagging.tests.models import Link +from tagging.tests.models import Perch +from tagging.tests.models import Parrot +from tagging.tests.models import FormTest +from tagging.tests.models import FormTestNull +from tagging.tests.models import FormMultipleFieldTest from tagging.utils import LINEAR +from tagging.utils import LOGARITHMIC +from tagging.utils import get_tag +from tagging.utils import get_tag_list +from tagging.utils import calculate_cloud +from tagging.utils import parse_tag_input +from tagging.utils import edit_string_for_tags +from tagging.utils import _calculate_tag_weight ############# # Utilities # ############# + 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']) - + + self.assertEqual(parse_tag_input('one'), ['one']) + self.assertEqual(parse_tag_input('one two'), ['one', 'two']) + self.assertEqual(parse_tag_input('one one two two'), ['one', 'two']) + self.assertEqual(parse_tag_input('one two three'), + ['one', 'three', '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']) - + + self.assertEqual(parse_tag_input(',one'), ['one']) + self.assertEqual(parse_tag_input(',one two'), ['one two']) + self.assertEqual(parse_tag_input(',one two three'), ['one two three']) + self.assertEqual(parse_tag_input('a-one, a-two and a-three'), + ['a-one', 'a-two and a-three']) + def test_with_double_quoted_multiple_words(self): """ Test with double-quoted multiple words. - A completed quote will trigger this. Unclosed quotes are ignored. """ - - self.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']) - + A completed quote will trigger this. Unclosed quotes are ignored. + """ + + self.assertEqual(parse_tag_input('"one'), ['one']) + self.assertEqual(parse_tag_input('"one two'), ['one', 'two']) + self.assertEqual(parse_tag_input('"one two three'), + ['one', 'three', 'two']) + self.assertEqual(parse_tag_input('"one two"'), ['one two']) + self.assertEqual(parse_tag_input('a-one "a-two and a-three"'), + ['a-one', 'a-two and a-three']) + def test_with_no_loose_commas(self): """ Test with no loose commas -- split on spaces. """ - self.assertEquals(parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) - + self.assertEqual(parse_tag_input('one two "thr,ee"'), + ['one', 'thr,ee', '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']) - + self.assertEqual(parse_tag_input('"one", two three'), + ['one', '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']) - + self.assertEqual(parse_tag_input('a-one "a-two, and a-three"'), + ['a-one', 'a-two, and a-three']) + self.assertEqual(parse_tag_input('"two", one, one, two, "one"'), + ['one', 'two']) + self.assertEqual(parse_tag_input('two", one'), + ['one', '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']) - + self.assertEqual(parse_tag_input(None), []) + self.assertEqual(parse_tag_input(''), []) + self.assertEqual(parse_tag_input('"'), []) + self.assertEqual(parse_tag_input('""'), []) + self.assertEqual(parse_tag_input('"' * 7), []) + self.assertEqual(parse_tag_input(',,,,,,'), []) + self.assertEqual(parse_tag_input('",",",",",",","'), [',']) + self.assertEqual(parse_tag_input('a-one "a-two" and "a-three'), + ['a-one', 'a-three', 'a-two', 'and']) + + class TestNormalisedTagListInput(TestCase): def setUp(self): - self.cheese = Tag.objects.create(name='cheese') self.toast = Tag.objects.create(name='toast') - + self.cheese = Tag.objects.create(name='cheese') + def test_single_tag_object_as_input(self): - self.assertEquals(get_tag_list(self.cheese), [self.cheese]) - + self.assertEqual(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(self.toast in ret) + def test_with_empty_list(self): - self.assertEquals(get_tag_list([]), []) - + self.assertEqual(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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) - + self.assertEqual(len(ret), 2) + self.assertTrue(self.cheese in ret) + self.assertTrue(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 [%]' %\ + except ValueError as ve: + self.assertEqual( + str(ve), + 'If a list or tuple of tags is provided, they must all ' + 'be tag names, Tag objects or Tag ids.') + except Exception as 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!') - + 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))) + except ValueError as ve: + self.assertEqual(str(ve), 'The tag input given was invalid.') + except Exception as e: + print('--', 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!') + 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) - + self.assertEqual(get_tag(self.cheese), self.cheese) + def test_with_string(self): - self.assertEquals(get_tag('cheese'), self.cheese) - + self.assertEqual(get_tag('cheese'), self.cheese) + def test_with_primary_key(self): - self.assertEquals(get_tag(self.cheese.id), self.cheese) - + self.assertEqual(get_tag(self.cheese.id), self.cheese) + def test_nonexistent_tag(self): - self.assertEquals(get_tag('mouse'), None) + self.assertEqual(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(): + 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) - + self.assertEqual(sizes[1], 48) + self.assertEqual(sizes[2], 30) + self.assertEqual(sizes[3], 19) + self.assertEqual(sizes[4], 15) + self.assertEqual(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) - + self.assertEqual(sizes[1], 97) + self.assertEqual(sizes[2], 12) + self.assertEqual(sizes[3], 7) + self.assertEqual(sizes[4], 2) + self.assertEqual(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))) + except ValueError as ve: + self.assertEqual( + str(ve), 'Invalid distribution algorithm specified: cheese.') + except Exception as 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!') - + raise self.failureException( + 'a ValueError exception was supposed to be raised!') + + def test_calculate_tag_weight(self): + self.assertEqual( + _calculate_tag_weight(10, 20, LINEAR), + 10) + self.assertEqual( + _calculate_tag_weight(10, 20, LOGARITHMIC), + 15.37243573680482) + + def test_calculate_tag_weight_invalid_size(self): + self.assertEqual( + _calculate_tag_weight(10, 10, LOGARITHMIC), + 10.0) + self.assertEqual( + _calculate_tag_weight(26, 26, LOGARITHMIC), + 26.0) + ########### # Tagging # ########### + 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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('foo') in tags) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 4) + self.assertTrue(get_tag('zip') in tags) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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))) + except AttributeError as ae: + self.assertEqual(str(ae), 'No tags were given: " ".') + except Exception as 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!') - + 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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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))) + except AttributeError as ae: + self.assertEqual(str(ae), 'Multiple tags were given: "one two".') + except Exception as 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!') - + 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'ŠĐĆŽćžšđ') + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, 'ŠĐĆŽćžšđ') 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'你好') + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0].name, 'ŠĐĆŽćžšđ') + + Tag.objects.update_tags(self.dead_parrot, '你好') tags = Tag.objects.get_for_object(self.dead_parrot) - self.assertEquals(len(tags), 1) - self.assertEquals(tags[0].name, u'你好') - + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0].name, '你好') + + def test_unicode_tagged_object(self): + self.dead_parrot.state = "dëad" + self.dead_parrot.save() + Tag.objects.update_tags(self.dead_parrot, 'föo') + items = TaggedItem.objects.all() + self.assertEqual(len(items), 1) + self.assertEqual(six.text_type(items[0]), "dëad [föo]") + 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) - + self.assertEqual(len(tags), 3) + self.assertTrue(get_tag('bar') in tags) + self.assertTrue(get_tag('baz') in tags) + self.assertTrue(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) + self.assertEqual(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') + f1 = FormTest.objects.create(tags='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) - + self.assertTrue(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEqual(len(tags), 3) + self.assertTrue(test1_tag in tags) + self.assertTrue(test2_tag in tags) + self.assertTrue(test3_tag in tags) + def test_update_via_tags_field(self): - f1 = FormTest.objects.create(tags=u'test3 test2 test1') + f1 = FormTest.objects.create(tags='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' + self.assertTrue(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEqual(len(tags), 3) + self.assertTrue(test1_tag in tags) + self.assertTrue(test2_tag in tags) + self.assertTrue(test3_tag in tags) + + f1.tags = '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) - + self.assertEqual(len(tags), 1) + self.assertEqual(tags[0], test4_tag) + f1.tags = '' f1.save() tags = Tag.objects.get_for_object(f1) - self.assertEquals(len(tags), 0) + self.assertEqual(len(tags), 0) - def test_update_via_tags(self): - f1 = FormTest.objects.create(tags=u'one two three') + def disabledtest_update_via_tags(self): + # TODO: make this test working by reverting + # https://github.com/Fantomas42/django-tagging/commit/bbc7f25ea471dd903f39e08684d84ce59081bdef + f1 = FormTest.objects.create(tags='one two three') Tag.objects.get(name='three').delete() t2 = Tag.objects.get(name='two') t2.name = 'new' t2.save() f1again = FormTest.objects.get(pk=f1.pk) - self.failIf('three' in f1again.tags) - self.failIf('two' in f1again.tags) - self.failUnless('new' in f1again.tags) + self.assertFalse('three' in f1again.tags) + self.assertFalse('two' in f1again.tags) + self.assertTrue('new' in f1again.tags) def test_creation_without_specifying_tags(self): f1 = FormTest() - self.assertEquals(f1.tags, '') + self.assertEqual(f1.tags, '') def test_creation_with_nullable_tags_field(self): f1 = FormTestNull() - self.assertEquals(f1.tags, '') - + self.assertEqual(f1.tags, '') + + def test_fix_update_tag_field_deferred(self): + """ + Bug introduced in Django 1.10 + the TagField is considered "deferred" on Django 1.10 + because instance.__dict__ is not populated by the TagField + instance, so it's excluded when updating a model instance. + + Note: this does not append if you only have one TagField + in your model... + """ + f1 = FormMultipleFieldTest.objects.create(tagging_field='one two') + self.assertEqual(f1.tagging_field, 'one two') + tags = Tag.objects.get_for_object(f1) + self.assertEqual(len(tags), 2) + test1_tag = get_tag('one') + test2_tag = get_tag('two') + self.assertTrue(test1_tag in tags) + self.assertTrue(test2_tag in tags) + + f1.tagging_field = f1.tagging_field + ' three' + f1.save() + self.assertEqual(f1.tagging_field, 'one two three') + tags = Tag.objects.get_for_object(f1) + self.assertEqual(len(tags), 3) + test3_tag = get_tag('three') + self.assertTrue(test3_tag in tags) + + f1again = FormMultipleFieldTest.objects.get(pk=f1.pk) + self.assertEqual(f1again.tagging_field, 'one two three') + + tags = Tag.objects.get_for_object(f1again) + self.assertEqual(len(tags), 3) + + 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) + self.assertEqual(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) - + self.assertTrue(foo_tag in tags) + self.assertTrue(bar_tag in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(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) - + self.assertEqual(len(tags), 3) + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(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) + self.assertEqual(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) - + self.assertTrue(bar_tag in tags) + self.assertTrue(baz_tag in tags) + self.assertTrue(foo_tag in tags) + self.assertTrue(zip_tag in tags) + f1 = FormTest.objects.create() - f1.tags = u'TEST5' + f1.tags = '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') + self.assertEqual(len(tags), 1) + self.assertTrue(test5_tag in tags) + self.assertEqual(f1.tags, 'test5') + class TestTagUsageForModelBaseCase(TestCase): def test_tag_usage_for_model_empty(self): - self.assertEquals(Tag.objects.usage_for_model(Parrot), []) + self.assertEqual(Tag.objects.usage_for_model(Parrot), []) + class TestTagUsageForModel(TestCase): def setUp(self): @@ -457,75 +565,85 @@ def setUp(self): ('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) - + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 3) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('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) + 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) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 3) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('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')) + 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')) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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)) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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)) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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)) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('baz', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 0) + class TestTagsRelatedForModel(TestCase): def setUp(self): @@ -535,71 +653,134 @@ def setUp(self): ('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) + 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.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue('baz' in relevant_attribute_list) + self.assertTrue('foo' in relevant_attribute_list) + self.assertTrue('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.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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.assertEqual(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) + related_tags = Tag.objects.related_for_model( + 'bar', Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) + for tag in related_tags] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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) - + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue('baz' in relevant_attribute_list) + self.assertTrue('foo' in relevant_attribute_list) + self.assertTrue('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.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 0) + + +class TestTagCloudForModel(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_cloud_for_model(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 3, 4) in relevant_attribute_list) + self.assertTrue(('baz', 1, 1) in relevant_attribute_list) + self.assertTrue(('foo', 2, 2) in relevant_attribute_list) + self.assertTrue(('ter', 3, 4) in relevant_attribute_list) + + def test_tag_cloud_for_model_filters(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot, + filters={'state': 'no more'}) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1, 1) in relevant_attribute_list) + self.assertTrue(('ter', 1, 1) in relevant_attribute_list) + + def test_tag_cloud_for_model_min_count(self): + tag_cloud = Tag.objects.cloud_for_model(Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count, tag.font_size) + for tag in tag_cloud] + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 3, 4) in relevant_attribute_list) + self.assertTrue(('foo', 2, 1) in relevant_attribute_list) + self.assertTrue(('ter', 3, 4) in relevant_attribute_list) + + class TestGetTaggedObjectsByModel(TestCase): def setUp(self): parrot_details = ( @@ -608,112 +789,123 @@ def setUp(self): ('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.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) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.no_more_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(len(parrots), 3) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(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) - + self.assertEqual(len(parrots), 1) + self.assertTrue(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) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(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) - + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) + self.assertEqual(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['foo', 'bar'])) + self.assertEqual(len(parrots), 1) + self.assertTrue(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model( + Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(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) - + self.assertEqual(len(parrots), 1) + self.assertTrue(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) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(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) - + self.assertEqual(len(parrots), 1) + self.assertTrue(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) - + self.assertEqual(len(parrots), 2) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(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) - + self.assertEqual(len(parrots), 4) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.no_more_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(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) - + self.assertEqual(len(parrots), 3) + self.assertTrue(self.late_parrot in parrots) + self.assertTrue(self.passed_on_parrot in parrots) + self.assertTrue(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) + self.assertEqual(len(parrots), 0) + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['albert']) + self.assertEqual(len(parrots), 0) + + Tag.objects.create(name='titi') + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['titi']) + self.assertEqual(len(parrots), 0) + class TestGetRelatedTaggedItems(TestCase): def setUp(self): @@ -723,12 +915,12 @@ def setUp(self): ('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') @@ -736,44 +928,46 @@ def setUp(self): 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) - + self.assertEqual(len(related_objects), 2) + self.assertTrue(self.l2 in related_objects) + self.assertTrue(self.l3 in related_objects) + related_objects = TaggedItem.objects.get_related(self.l4, Link) - self.assertEquals(len(related_objects), 0) - + self.assertEqual(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 - + # See http://bit.ly/1AYNEsa + # 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) - + self.assertEqual(len(related_objects), 1) + self.assertTrue(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) - + related_objects = TaggedItem.objects.get_related( + self.l1, Link.objects.exclude(name='link 3')) + self.assertEqual(len(related_objects), 1) + self.assertTrue(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) - + self.assertEqual(len(related_objects), 3) + self.assertTrue(self.l1 in related_objects) + self.assertTrue(self.l2 in related_objects) + self.assertTrue(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) - + self.assertEqual(len(related_objects), 0) + + class TestTagUsageForQuerySet(TestCase): def setUp(self): parrot_details = ( @@ -782,102 +976,125 @@ def setUp(self): ('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) + 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) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('baz', 1) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 4) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('baz', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('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.assertEqual(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) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', False) in relevant_attribute_list) + self.assertTrue(('foo', False) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 3) + self.assertTrue(('bar', 2) in relevant_attribute_list) + self.assertTrue(('foo', 2) in relevant_attribute_list) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 1) + self.assertTrue(('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) + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('foo', 1) in relevant_attribute_list) + self.assertTrue(('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) - + self.assertEqual(len(relevant_attribute_list), 2) + self.assertTrue(('bar', 1) in relevant_attribute_list) + self.assertTrue(('ter', 1) in relevant_attribute_list) + + ################ # Model Fields # ################ @@ -888,33 +1105,127 @@ def test_tag_field_in_modelform(self): class TestForm(forms.ModelForm): class Meta: model = FormTest - + fields = forms.ALL_FIELDS + form = TestForm() - self.assertEquals(form.fields['tags'].__class__.__name__, 'TagField') - + self.assertEqual(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') - + self.assertEqual(edit_string_for_tags([plain]), 'plain') + self.assertEqual(edit_string_for_tags([spaces]), '"spa ces"') + self.assertEqual(edit_string_for_tags([plain, spaces]), + 'plain, spa ces') + self.assertEqual(edit_string_for_tags([plain, spaces, comma]), + 'plain, spa ces, "com,ma"') + self.assertEqual(edit_string_for_tags([plain, comma]), + 'plain "com,ma"') + self.assertEqual(edit_string_for_tags([comma, spaces]), + '"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.') + t = TagField(required=False) + self.assertEqual(t.clean(''), '') + self.assertEqual(t.clean('foo'), 'foo') + self.assertEqual(t.clean('foo bar baz'), 'foo bar baz') + self.assertEqual(t.clean('foo,bar,baz'), 'foo,bar,baz') + self.assertEqual(t.clean('foo, bar, baz'), 'foo, bar, baz') + self.assertEqual( + t.clean('foo qwertyuiopasdfghjklzxcvbnm' + 'qwertyuiopasdfghjklzxcvb bar'), + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') + self.assertRaises( + forms.ValidationError, t.clean, + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') + + def test_tag_get_from_model(self): + FormTest.objects.create(tags='test3 test2 test1') + FormTest.objects.create(tags='toto titi') + self.assertEquals(FormTest.tags, 'test1 test2 test3 titi toto') + + +######### +# Forms # +######### + + +class TestTagAdminForm(TestCase): + + def test_clean_name(self): + datas = {'name': 'tag'} + form = TagAdminForm(datas) + self.assertTrue(form.is_valid()) + + def test_clean_name_multi(self): + datas = {'name': 'tag error'} + form = TagAdminForm(datas) + self.assertFalse(form.is_valid()) + + def test_clean_name_too_long(self): + datas = {'name': 't' * (settings.MAX_TAG_LENGTH + 1)} + form = TagAdminForm(datas) + self.assertFalse(form.is_valid()) + +######### +# Views # +######### + + +@override_settings( + ROOT_URLCONF='tagging.tests.urls', + TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'OPTIONS': { + 'loaders': ('tagging.tests.utils.VoidLoader',) + } + } + ] +) +class TestTaggedObjectList(TestCase): + + def setUp(self): + self.a1 = Article.objects.create(name='article 1') + self.a2 = Article.objects.create(name='article 2') + Tag.objects.update_tags(self.a1, 'static tag test') + Tag.objects.update_tags(self.a2, 'static test') + + def get_view(self, url, queries=1, code=200, + expected_items=1, + friendly_context='article_list', + template='tests/article_list.html'): + with self.assertNumQueries(queries): + response = self.client.get(url) + self.assertEquals(response.status_code, code) + + if code == 200: + self.assertTrue(isinstance(response.context['tag'], Tag)) + self.assertEqual(len(response.context['object_list']), + expected_items) + self.assertEqual(response.context['object_list'], + response.context[friendly_context]) + self.assertTemplateUsed(response, template) + return response + + def test_view_static(self): + self.get_view('/static/', expected_items=2) + + def test_view_dynamic(self): + self.get_view('/tag/', expected_items=1) + + def test_view_related(self): + response = self.get_view('/static/related/', + queries=2, expected_items=2) + self.assertEquals(len(response.context['related_tags']), 2) + + def test_view_no_queryset_no_model(self): + self.assertRaises(ImproperlyConfigured, self.get_view, + '/no-query-no-model/') + + def test_view_no_tag(self): + self.assertRaises(AttributeError, self.get_view, '/no-tag/') + + def test_view_404(self): + self.get_view('/unavailable/', code=404) diff --git a/tagging/tests/urls.py b/tagging/tests/urls.py new file mode 100644 index 00000000..a78dc8ba --- /dev/null +++ b/tagging/tests/urls.py @@ -0,0 +1,20 @@ +"""Test urls for tagging.""" +from django.conf.urls import url + +from tagging.views import TaggedObjectList +from tagging.tests.models import Article + + +class StaticTaggedObjectList(TaggedObjectList): + tag = 'static' + queryset = Article.objects.all() + + +urlpatterns = [ + url(r'^static/$', StaticTaggedObjectList.as_view()), + url(r'^static/related/$', StaticTaggedObjectList.as_view( + related_tags=True)), + url(r'^no-tag/$', TaggedObjectList.as_view(model=Article)), + url(r'^no-query-no-model/$', TaggedObjectList.as_view()), + url(r'^(?P[^/]+(?u))/$', TaggedObjectList.as_view(model=Article)), +] diff --git a/tagging/tests/utils.py b/tagging/tests/utils.py new file mode 100644 index 00000000..b86c3e16 --- /dev/null +++ b/tagging/tests/utils.py @@ -0,0 +1,32 @@ +""" +Tests utils for tagging. +""" +from django.template.loaders.base import Loader +try: + from django.template import Origin +except ImportError: + class Origin(object): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + +class VoidLoader(Loader): + """ + Template loader which is always returning + an empty template. + """ + is_usable = True + _accepts_engine_in_init = True + + def get_template_sources(self, template_name): + yield Origin( + name='voidloader', + template_name=template_name, + loader=self) + + def get_contents(self, origin): + return '' + + def load_template_source(self, template_name, template_dirs=None): + return ('', 'voidloader:%s' % template_name) diff --git a/tagging/utils.py b/tagging/utils.py index e89bab0e..cd7e0ee5 100644 --- a/tagging/utils.py +++ b/tagging/utils.py @@ -3,17 +3,15 @@ calculation. """ import math -import types +from django.utils import six from django.db.models.query import QuerySet -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.translation import ugettext as _ -# Python 2.3 compatibility -try: - set -except NameError: - from sets import Set as set +# Font size distribution algorithms +LOGARITHMIC, LINEAR = 1, 2 + def parse_tag_input(input): """ @@ -26,13 +24,13 @@ def parse_tag_input(input): if not input: return [] - input = force_unicode(input) + input = force_text(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' '))) + if ',' not in input and '"' not in input: + words = list(set(split_strip(input, ' '))) words.sort() return words @@ -46,56 +44,55 @@ def parse_tag_input(input): i = iter(input) try: while 1: - c = i.next() - if c == u'"': + c = next(i) + if c == '"': if buffer: - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) buffer = [] # Find the matching quote open_quote = True - c = i.next() - while c != u'"': + c = next(i) + while c != '"': buffer.append(c) - c = i.next() + c = next(i) if buffer: - word = u''.join(buffer).strip() + word = ''.join(buffer).strip() if word: words.append(word) buffer = [] open_quote = False else: - if not saw_loose_comma and c == u',': + if not saw_loose_comma and c == ',': saw_loose_comma = True buffer.append(c) except StopIteration: # If we were parsing an open quote which was never closed treat # the buffer as unquoted. if buffer: - if open_quote and u',' in buffer: + if open_quote and ',' in buffer: saw_loose_comma = True - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) if to_be_split: if saw_loose_comma: - delimiter = u',' + delimiter = ',' else: - delimiter = u' ' + delimiter = ' ' 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','): + +def split_strip(input, delimiter=','): """ 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 @@ -113,18 +110,27 @@ def edit_string_for_tags(tags): use_commas = False for tag in tags: name = tag.name - if u',' in name: + if ',' in name: names.append('"%s"' % name) continue - elif u' ' in name: + elif ' ' in name: if not use_commas: use_commas = True names.append(name) if use_commas: - glue = u', ' + glue = ', ' else: - glue = u' ' - return glue.join(names) + glue = ' ' + result = glue.join(names) + + # If we only had one name, and it had spaces, + # we need to enclose it in quotes. + # Otherwise, it's interpreted as two tags. + if len(names) == 1 and use_commas: + result = '"' + result + '"' + + return result + def get_queryset_and_model(queryset_or_model): """ @@ -139,6 +145,7 @@ def get_queryset_and_model(queryset_or_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. @@ -164,32 +171,35 @@ def get_tag_list(tags): return [tags] elif isinstance(tags, QuerySet) and tags.model is Tag: return tags - elif isinstance(tags, types.StringTypes): + elif isinstance(tags, six.string_types): return Tag.objects.filter(name__in=parse_tag_input(tags)) - elif isinstance(tags, (types.ListType, types.TupleType)): + elif isinstance(tags, (list, tuple)): if len(tags) == 0: return tags contents = set() for item in tags: - if isinstance(item, types.StringTypes): + if isinstance(item, six.string_types): contents.add('string') elif isinstance(item, Tag): contents.add('tag') - elif isinstance(item, (types.IntType, types.LongType)): + elif isinstance(item, six.integer_types): contents.add('int') if len(contents) == 1: if 'string' in contents: - return Tag.objects.filter(name__in=[force_unicode(tag) \ + return Tag.objects.filter(name__in=[force_text(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.')) + 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 @@ -206,34 +216,37 @@ def get_tag(tag): return tag try: - if isinstance(tag, types.StringTypes): + if isinstance(tag, six.string_types): return Tag.objects.get(name=tag) - elif isinstance(tag, (types.IntType, types.LongType)): + elif isinstance(tag, six.integer_types): 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* plugin for Mephisto, by Sven Fuchs. - .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-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) + return min( + math.log(weight) * max_weight / math.log(max_weight), + max_weight) + raise ValueError( + _('Invalid distribution algorithm specified: %s.') % distribution) + def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC): """ @@ -255,7 +268,8 @@ def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC): 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) + 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 diff --git a/tagging/views.py b/tagging/views.py index 65afc0b9..527bdd71 100644 --- a/tagging/views.py +++ b/tagging/views.py @@ -2,26 +2,77 @@ Tagging related views. """ from django.http import Http404 +from django.views.generic.list import ListView from django.utils.translation import ugettext as _ -from django.views.generic import ListView -from tagging.models import Tag, TaggedItem +from django.core.exceptions import ImproperlyConfigured + +from tagging.models import Tag +from tagging.models import TaggedItem from tagging.utils import get_tag +from tagging.utils import get_queryset_and_model + + +class TaggedObjectList(ListView): + """ + A thin wrapper around + ``django.views.generic.list.ListView`` 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. + """ + tag = None + related_tags = False + related_tag_counts = True -class TagListView(ListView): + def get_tag(self): + if self.tag is None: + try: + self.tag = self.kwargs.pop('tag') + except KeyError: + raise AttributeError( + _('TaggedObjectList must be called with a tag.')) - queryset_or_model = None + tag_instance = get_tag(self.tag) + if tag_instance is None: + raise Http404(_('No Tag found matching "%s".') % self.tag) + + return tag_instance + + def get_queryset_or_model(self): + if self.queryset is not None: + return self.queryset + elif self.model is not None: + return self.model + else: + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset_or_model()." % { + 'cls': self.__class__.__name__ + } + ) def get_queryset(self): - tag = self.kwargs['tag'] - self.tag_instance = get_tag(tag) - if self.tag_instance is None: - raise Http404(_('No Tag found matching "%s".') % tag) + self.queryset_or_model = self.get_queryset_or_model() + self.tag_instance = self.get_tag() return TaggedItem.objects.get_by_model( - self.queryset_or_model, self.tag_instance - ) + self.queryset_or_model, self.tag_instance) def get_context_data(self, **kwargs): - context = super(TagListView, self).get_context_data(**kwargs) + context = super(TaggedObjectList, self).get_context_data(**kwargs) context['tag'] = self.tag_instance + + if self.related_tags: + queryset, model = get_queryset_and_model(self.queryset_or_model) + context['related_tags'] = Tag.objects.related_for_model( + self.tag_instance, model, counts=self.related_tag_counts) return context