diff --git a/.travis.yml b/.travis.yml index faf28d3..d7002df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,8 @@ language: python python: - "2.7" env: - - TOX_ENV=py27-django14 - - TOX_ENV=py27-django15 - - TOX_ENV=py27-django16 - TOX_ENV=py27-django17 - TOX_ENV=py27-django18 - - TOX_ENV=py33-django15 - - TOX_ENV=py33-django16 - TOX_ENV=py33-django17 - TOX_ENV=py33-django18 install: diff --git a/tagging/managers.py b/tagging/managers.py index 02cd1c2..d17028d 100644 --- a/tagging/managers.py +++ b/tagging/managers.py @@ -7,6 +7,7 @@ from tagging.models import Tag, TaggedItem + class ModelTagManager(models.Manager): """ A manager for retrieving tags for a particular model. @@ -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/models.py b/tagging/models.py index 38d534c..8f3212c 100644 --- a/tagging/models.py +++ b/tagging/models.py @@ -1,22 +1,20 @@ """ Models and managers for generic tagging. """ -# Python 2.3 compatibility -try: - set -except NameError: - from sets import Set as set - from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.conf import settings from django.db import connection, models -from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ from tagging.settings import DEFAULT_FORCE_LOWERCASE_TAGS -from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input +from tagging.utils import ( + calculate_cloud, + get_tag_list, + get_queryset_and_model, + parse_tag_input, +) from tagging.utils import LOGARITHMIC qn = connection.ops.quote_name @@ -25,8 +23,9 @@ # Managers # ############ + class TagManager(models.Manager): - def update_tags(self, obj, tag_names): + def update_tags(self, obj, tag_names, replace_existing=True): """ Update tags associated with an object. """ @@ -34,16 +33,25 @@ def update_tags(self, obj, tag_names): current_tags = list(self.filter(items__content_type__pk=ctype.pk, items__object_id=obj.pk)) updated_tag_names = parse_tag_input(tag_names) - if getattr(settings, 'FORCE_LOWERCASE_TAGS', DEFAULT_FORCE_LOWERCASE_TAGS): + force_lowercase = getattr( + settings, + 'FORCE_LOWERCASE_TAGS', + DEFAULT_FORCE_LOWERCASE_TAGS, + ) + if force_lowercase: updated_tag_names = [t.lower() for t in updated_tag_names] - # Remove tags which no longer apply - tags_for_removal = [tag for tag in current_tags \ - if tag.name not in updated_tag_names] - if len(tags_for_removal): - TaggedItem._default_manager.filter(content_type__pk=ctype.pk, - object_id=obj.pk, - tag__in=tags_for_removal).delete() + if replace_existing: + # Remove tags which no longer apply + tags_for_removal = [tag for tag in current_tags + if tag.name not in updated_tag_names] + if len(tags_for_removal): + TaggedItem._default_manager.filter( + content_type__pk=ctype.pk, + object_id=obj.pk, + tag__in=tags_for_removal, + ).delete() + # Add new tags current_tag_names = [tag.name for tag in current_tags] for tag_name in updated_tag_names: @@ -59,9 +67,16 @@ def add_tag(self, obj, tag_name): if not len(tag_names): raise AttributeError(_('No tags were given: "%s".') % tag_name) if len(tag_names) > 1: - raise AttributeError(_('Multiple tags were given: "%s".') % tag_name) + raise AttributeError( + _('Multiple tags were given: "%s".') % tag_name + ) tag_name = tag_names[0] - if getattr(settings, 'FORCE_LOWERCASE_TAGS', DEFAULT_FORCE_LOWERCASE_TAGS): + force_lowercase = getattr( + settings, + 'FORCE_LOWERCASE_TAGS', + DEFAULT_FORCE_LOWERCASE_TAGS, + ) + if force_lowercase: tag_name = tag_name.lower() tag, created = self.get_or_create(name=tag_name) ctype = ContentType.objects.get_for_model(obj) @@ -77,12 +92,21 @@ 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)) @@ -114,7 +138,10 @@ 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]) @@ -123,7 +150,13 @@ 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. @@ -141,7 +174,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(): @@ -183,7 +217,14 @@ def usage_for_queryset(self, queryset, counts=False, min_count=None): 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): """ @@ -198,13 +239,15 @@ 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 ( @@ -221,12 +264,20 @@ 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 @@ -272,6 +323,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`` @@ -410,7 +462,12 @@ 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 @@ -446,10 +503,11 @@ 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 + # 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 [] @@ -458,11 +516,17 @@ def get_related(self, obj, queryset_or_model, num=None): # Models # ########## + class Tag(models.Model): """ A tag. """ - name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) + name = models.CharField( + _('name'), + max_length=50, + unique=True, + db_index=True, + ) objects = TagManager() @@ -474,14 +538,18 @@ class Meta: def __unicode__(self): return self.name + class TaggedItem(models.Model): """ Holds the relationship between a tag and the item being tagged. """ - tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items') - content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) - object_id = models.PositiveIntegerField(_('object id'), db_index=True) - object = generic.GenericForeignKey('content_type', 'object_id') + tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items') + content_type = models.ForeignKey( + ContentType, + verbose_name=_('content type'), + ) + object_id = models.PositiveIntegerField(_('object id'), db_index=True) + object = generic.GenericForeignKey('content_type', 'object_id') objects = TaggedItemManager() diff --git a/tagging/tests/models.py b/tagging/tests/models.py index e3274ff..7de4594 100644 --- a/tagging/tests/models.py +++ b/tagging/tests/models.py @@ -2,10 +2,12 @@ from tagging.fields import TagField + class Perch(models.Model): size = models.IntegerField() smelly = models.BooleanField(default=True) + class Parrot(models.Model): state = models.CharField(max_length=50) perch = models.ForeignKey(Perch, null=True) @@ -16,6 +18,7 @@ def __unicode__(self): class Meta: ordering = ['state'] + class Link(models.Model): name = models.CharField(max_length=50) @@ -25,6 +28,7 @@ def __unicode__(self): class Meta: ordering = ['name'] + class Article(models.Model): name = models.CharField(max_length=50) @@ -34,9 +38,10 @@ def __unicode__(self): class Meta: ordering = ['name'] + class FormTest(models.Model): tags = TagField('Test', help_text='Test') + class FormTestNull(models.Model): tags = TagField(null=True) - diff --git a/tagging/tests/tests.py b/tagging/tests/tests.py index b2c3765..93370f3 100644 --- a/tagging/tests/tests.py +++ b/tagging/tests/tests.py @@ -5,8 +5,21 @@ from django.test import TestCase from tagging.forms import TagField 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.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.tests import tags as default_tags from tagging.utils import LINEAR @@ -14,13 +27,17 @@ # 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 two three'), + [u'one', u'three', u'two'] + ) self.assertEquals(parse_tag_input('one one two two'), [u'one', u'two']) def test_with_comma_delimited_multiple_words(self): @@ -29,34 +46,52 @@ def test_with_comma_delimited_multiple_words(self): 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.assertEquals( + parse_tag_input(',one two three'), + [u'one two three'] + ) + self.assertEquals( + parse_tag_input('a-one, a-two and a-three'), + [u'a-one', u'a-two and a-three'] + ) def test_with_double_quoted_multiple_words(self): - """ Test with double-quoted multiple words. - A completed quote will trigger this. Unclosed quotes are ignored. """ + """ + 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 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']) + self.assertEquals( + parse_tag_input('a-one "a-two and a-three"'), + [u'a-one', u'a-two and a-three'] + ) def test_with_no_loose_commas(self): """ Test with no loose commas -- split on spaces. """ - self.assertEquals(parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) + self.assertEquals( + parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) def test_with_loose_commas(self): """ Loose commas - split on commas """ - self.assertEquals(parse_tag_input('"one", two three'), [u'one', u'two three']) + self.assertEquals( + parse_tag_input('"one", two three'), + [u'one', u'two three'] + ) def test_tags_with_double_quotes_can_contain_commas(self): """ Double quotes can contain commas """ - self.assertEquals(parse_tag_input('a-one "a-two, and a-three"'), + 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"'), + self.assertEquals( + parse_tag_input('"two", one, one, two, "one"'), [u'one', u'two']) def test_with_naughty_input(self): @@ -70,9 +105,11 @@ def test_with_naughty_input(self): 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'), + self.assertEquals( + parse_tag_input('a-one "a-two" and "a-three'), [u'a-one', u'a-three', u'a-two', u'and']) + class TestNormalisedTagListInput(TestCase): def setUp(self): self.cheese = Tag.objects.create(name='cheese') @@ -136,13 +173,21 @@ def test_with_invalid_input_mix_of_string_and_instance(self): try: get_tag_list(['cheese', self.toast]) except ValueError as 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.') + 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 as e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%]' %\ + 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: @@ -150,10 +195,16 @@ def test_with_invalid_input(self): except ValueError as ve: self.assertEquals(str(ve), 'The tag input given was invalid.') except Exception as e: - raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ - (str(type(e)), str(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) @@ -167,6 +218,7 @@ def test_with_primary_key(self): def test_nonexistent_tag(self): self.assertEquals(get_tag('mouse'), None) + class TestCalculateCloud(TestCase): def setUp(self): self.tags = [] @@ -204,17 +256,26 @@ def test_invalid_distribution(self): try: calculate_cloud(self.tags, steps=5, distribution='cheese') except ValueError as ve: - self.assertEquals(str(ve), 'Invalid distribution algorithm specified: cheese.') + self.assertEquals( + 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))) + 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!', + ) ########### # Tagging # ########### + class TestBasicTagging(TestCase): def setUp(self): self.dead_parrot = Parrot.objects.create(state='dead') @@ -234,6 +295,26 @@ def test_update_tags(self): self.failUnless(get_tag('baz') in tags) self.failUnless(get_tag('foo') in tags) + def test_update_tags_without_replace(self): + Tag.objects.update_tags(self.dead_parrot, 'foo,bar,"ter"') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('foo') in tags) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('ter') in tags) + + Tag.objects.update_tags( + self.dead_parrot, + '"foo" bar "baz"', + replace_existing=False, + ) + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 4) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + self.failUnless(get_tag('ter') 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') @@ -274,10 +355,15 @@ def test_add_tag_invalid_input_no_tags_specified(self): except AttributeError as ae: self.assertEquals(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))) + 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 @@ -293,10 +379,15 @@ def test_add_tag_invalid_input_multiple_tags_specified(self): except AttributeError as ae: self.assertEquals(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))) + 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 @@ -330,6 +421,7 @@ def test_update_tags_with_none(self): tags = Tag.objects.get_for_object(self.dead_parrot) self.assertEquals(len(tags), 0) + class TestModelTagField(TestCase): """ Test the 'tags' field on models. """ @@ -388,6 +480,7 @@ def test_creation_with_nullable_tags_field(self): f1 = FormTestNull() self.assertEquals(f1.tags, '') + class TestSettings(TestCase): def setUp(self): self.dead_parrot = Parrot.objects.create(state='dead') @@ -443,10 +536,12 @@ def test_force_lowercase_tags(self): self.failUnless(test5_tag in tags) self.assertEquals(f1.tags, u'test5') + class TestTagUsageForModelBaseCase(TestCase): def test_tag_usage_for_model_empty(self): self.assertEquals(Tag.objects.usage_for_model(Parrot), []) + class TestTagUsageForModel(TestCase): def setUp(self): parrot_details = ( @@ -471,7 +566,10 @@ def test_tag_usage_for_model(self): self.failUnless((u'ter', 3) in relevant_attribute_list) def test_tag_usage_for_model_with_min_count(self): - tag_usage = Tag.objects.usage_for_model(Parrot, min_count = 2) + 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) @@ -479,13 +577,21 @@ def test_tag_usage_for_model_with_min_count(self): self.failUnless((u'ter', 3) in relevant_attribute_list) def test_tag_usage_with_filter_on_model_objects(self): - tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more')) + 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')) + 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) @@ -493,7 +599,11 @@ def test_tag_usage_with_filter_on_model_objects(self): 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)) + 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) @@ -501,30 +611,49 @@ def test_tag_usage_with_filter_on_model_objects(self): 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)) + 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)) + 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] + 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] + tag_usage = Tag.objects.usage_for_model( + Parrot, + filters=dict(perch__size__gt=99), + ) + relevant_attribute_list = [ + (tag.name, hasattr(tag, 'counts')) for tag in tag_usage + ] self.assertEquals(len(relevant_attribute_list), 0) + class TestTagsRelatedForModel(TestCase): def setUp(self): parrot_details = ( @@ -540,64 +669,121 @@ def setUp(self): 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] + 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] + 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=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] + 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] + related_tags = Tag.objects.related_for_model( + Tag.objects.filter(name__in=['bar', 'ter', 'baz']), + Parrot, + counts=True, + ) + relevant_attribute_list = [ + (tag.name, tag.count) for tag in related_tags + ] self.assertEquals(len(relevant_attribute_list), 0) def test_related_for_model_with_tag_strings_as_input(self): # Once again, with feeling (strings) - related_tags = Tag.objects.related_for_model('bar', Parrot, counts=True) - relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + 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] + 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=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] + 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] + related_tags = Tag.objects.related_for_model( + ['bar', 'ter', 'baz'], + Parrot, + counts=True, + ) + relevant_attribute_list = [ + (tag.name, tag.count) for tag in related_tags + ] self.assertEquals(len(relevant_attribute_list), 0) + class TestGetTaggedObjectsByModel(TestCase): def setUp(self): parrot_details = ( @@ -617,7 +803,9 @@ def setUp(self): 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') @@ -652,14 +840,23 @@ def test_get_by_model_intersection(self): self.assertEquals(len(parrots), 0) def test_get_by_model_with_tag_querysets_as_input(self): - parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) + 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'])) + 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'])) + 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) @@ -713,6 +910,7 @@ def test_get_union_by_model(self): parrots = TaggedItem.objects.get_union_by_model(Parrot, []) self.assertEquals(len(parrots), 0) + class TestGetRelatedTaggedItems(TestCase): def setUp(self): parrot_details = ( @@ -749,7 +947,7 @@ def test_get_related_objects_of_same_model(self): 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://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:127412348064 # noqa # ask for no more than 1 result related_objects = TaggedItem.objects.get_related(self.l1, Link, num=1) @@ -757,7 +955,10 @@ def test_get_related_objects_of_same_model_limited_number_of_results(self): self.failUnless(self.l2 in related_objects) def test_get_related_objects_of_same_model_limit_related_items(self): - related_objects = TaggedItem.objects.get_related(self.l1, Link.objects.exclude(name='link 3')) + 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) @@ -772,6 +973,7 @@ def test_get_related_objects_of_different_model(self): related_objects = TaggedItem.objects.get_related(self.a1, Link) self.assertEquals(len(related_objects), 0) + class TestTagUsageForQuerySet(TestCase): def setUp(self): parrot_details = ( @@ -787,13 +989,19 @@ def setUp(self): 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) + 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) @@ -801,7 +1009,10 @@ def test_tag_usage_for_queryset(self): 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) + 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) @@ -809,68 +1020,114 @@ def test_tag_usage_for_queryset(self): 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) + 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) + 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] + 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] + 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) + 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) + 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] + 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) + 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) + 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) + 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) + 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) @@ -880,6 +1137,7 @@ def test_tag_usage_for_queryset(self): # Model Fields # ################ + class TestTagFieldInForms(TestCase): def test_tag_field_in_modelform(self): # Ensure that automatically created forms use TagField @@ -897,11 +1155,26 @@ def test_recreation_of_tag_list_string_representations(self): comma = Tag.objects.create(name='com,ma') comma_and_space = Tag.objects.create(name='a b, c d e') 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.assertEquals(edit_string_for_tags([comma_and_space]), u'"a b, c d e"') + 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.assertEquals( + edit_string_for_tags([comma_and_space]), + u'"a b, c d e"', + ) # Regression test for #184 self.assertEquals(edit_string_for_tags([spaces]), u'"spa ces"') @@ -912,13 +1185,26 @@ def test_tag_d_validation(self): 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'), + self.assertEquals( + t.clean( + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar', + ), u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') try: - t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') + t.clean( + 'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar', + ) except forms.ValidationError as ve: - self.assertEquals(ve.messages, ['Each tag may be no more than 50 characters long.']) + self.assertEquals( + ve.messages, + ['Each tag may be no more than 50 characters long.'], + ) except Exception as e: raise e else: - raise self.failureException('a ValidationError exception was supposed to have been raised.') + raise self.failureException( + ( + 'a ValidationError exception ' + 'was supposed to have been raised.' + ), + ) diff --git a/tox.ini b/tox.ini index 61cd98c..e6869f1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,15 +5,12 @@ [tox] envlist = - py27-django{14,15,16,17,18}, - py33-django{15,16,17,18} + py27-django{17,18}, + py33-django{17,18} [testenv] commands = django-admin.py test --settings=tagging.tests.settings deps = django-nose - django14: Django>=1.4, <1.5 - django15: Django>=1.5, <1.6 - django16: Django>=1.6, <1.7 django17: Django>=1.7, <1.8 django18: Django>=1.8, <1.9