Commit f8e0d4d2 by Carl Meyer

tests for direct-tagging

parent 7ee9fd3a
...@@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType ...@@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey from django.contrib.contenttypes.generic import GenericForeignKey
from django.db import models from django.db import models
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyRel
from django.db.models.query_utils import QueryWrapper from django.db.models.query_utils import QueryWrapper
...@@ -126,17 +125,16 @@ class TaggableManager(object): ...@@ -126,17 +125,16 @@ class TaggableManager(object):
if len(cts) == 1: if len(cts) == 1:
return [("%s__content_type" % prefix, cts[0])] return [("%s__content_type" % prefix, cts[0])]
return [("%s__content_type__in" % prefix, cts)] return [("%s__content_type__in" % prefix, cts)]
return self.through._meta.db_table
class _TaggableManager(models.Manager): class _TaggableManager(models.Manager):
def __init__(self, through=None): def __init__(self, through):
self.through = through or TaggedItem self.through = through
def get_query_set(self): def get_query_set(self):
return self.through.tags_for(self.model, self.instance) return self.through.tags_for(self.model, self.instance)
def lookup_kwargs(self): def _lookup_kwargs(self):
return self.through.lookup_kwargs(self.instance) return self.through.lookup_kwargs(self.instance)
@require_instance_manager @require_instance_manager
...@@ -144,7 +142,7 @@ class _TaggableManager(models.Manager): ...@@ -144,7 +142,7 @@ class _TaggableManager(models.Manager):
for tag in tags: for tag in tags:
if not isinstance(tag, Tag): if not isinstance(tag, Tag):
tag, _ = Tag.objects.get_or_create(name=tag) tag, _ = Tag.objects.get_or_create(name=tag)
self.through.objects.get_or_create(**dict(self.lookup_kwargs(), self.through.objects.get_or_create(**dict(self._lookup_kwargs(),
tag=tag)) tag=tag))
@require_instance_manager @require_instance_manager
...@@ -154,12 +152,12 @@ class _TaggableManager(models.Manager): ...@@ -154,12 +152,12 @@ class _TaggableManager(models.Manager):
@require_instance_manager @require_instance_manager
def remove(self, *tags): def remove(self, *tags):
self.through.objects.filter(**self.lookup_kwargs()).filter( self.through.objects.filter(**self._lookup_kwargs()).filter(
tag__name__in=tags).delete() tag__name__in=tags).delete()
@require_instance_manager @require_instance_manager
def clear(self): def clear(self):
self.through.objects.filter(**self.lookup_kwargs()).delete() self.through.objects.filter(**self._lookup_kwargs()).delete()
def most_common(self): def most_common(self):
return self.get_query_set().annotate( return self.get_query_set().annotate(
...@@ -168,13 +166,19 @@ class _TaggableManager(models.Manager): ...@@ -168,13 +166,19 @@ class _TaggableManager(models.Manager):
@require_instance_manager @require_instance_manager
def similar_objects(self): def similar_objects(self):
qs = self.through.objects.values(*self.lookup_kwargs().keys()) qs = self.through.objects.values(*self._lookup_kwargs().keys())
qs = qs.annotate(n=models.Count('pk')) qs = qs.annotate(n=models.Count('pk'))
qs = qs.exclude(**self.lookup_kwargs()) qs = qs.exclude(**self._lookup_kwargs())
qs = qs.filter(tag__in=self.all()) qs = qs.filter(tag__in=self.all())
qs = qs.order_by('-n') qs = qs.order_by('-n')
if not 'content_object' in self.lookup_kwargs(): if 'content_object' in self._lookup_kwargs():
using_gfk = False
items = dict([(o.pk, o) for o in
self.through._meta.get_field('content_object').rel.to.objects.filter(
pk__in=[r['content_object'] for r in qs])])
else:
using_gfk = True
preload = defaultdict(set) preload = defaultdict(set)
for result in qs: for result in qs:
preload[result["content_type"]].add(result["object_id"]) preload[result["content_type"]].add(result["object_id"])
...@@ -187,10 +191,10 @@ class _TaggableManager(models.Manager): ...@@ -187,10 +191,10 @@ class _TaggableManager(models.Manager):
results = [] results = []
for result in qs: for result in qs:
try: if using_gfk:
obj = result['content_object']
except KeyError:
obj = items[result["content_type"]][result["object_id"]] obj = items[result["content_type"]][result["object_id"]]
else:
obj = items[result["content_object"]]
obj.similar_tags = result["n"] obj.similar_tags = result["n"]
results.append(obj) results.append(obj)
return results return results
......
import django
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey from django.contrib.contenttypes.generic import GenericForeignKey
from django.db import models, IntegrityError from django.db import models, IntegrityError
...@@ -26,7 +27,10 @@ class Tag(models.Model): ...@@ -26,7 +27,10 @@ class Tag(models.Model):
class TaggedItemBase(models.Model): class TaggedItemBase(models.Model):
tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items") if django.VERSION < (1, 2):
tag = models.ForeignKey(Tag, related_name="%(class)s_items")
else:
tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items")
def __unicode__(self): def __unicode__(self):
return "%s tagged with %s" % (self.content_object, self.tag) return "%s tagged with %s" % (self.content_object, self.tag)
...@@ -47,7 +51,7 @@ class TaggedItemBase(models.Model): ...@@ -47,7 +51,7 @@ class TaggedItemBase(models.Model):
if instance is not None: if instance is not None:
return Tag.objects.filter(**{'%s__content_object' % cls.tag_relname(): instance}) return Tag.objects.filter(**{'%s__content_object' % cls.tag_relname(): instance})
else: else:
return Tag.objects.filter(**{'%s__content_object__isnull' % cls.tag_relname(): False}) return Tag.objects.filter(**{'%s__content_object__isnull' % cls.tag_relname(): False}).distinct()
class TaggedItem(TaggedItemBase): class TaggedItem(TaggedItemBase):
......
from taggit.tests.tests import (AddTagTestCase, DeleteObjecTestCase, from taggit.tests.tests import (TaggableManagerTestCase, TaggableManagerDirectTestCase,
LookupByTagTestCase, TaggableFormTestCase, SimilarityByTagTestCase, TaggableFormTestCase, TaggableFormDirectTestCase)
TagReuseTestCase)
from django import forms from django import forms
from taggit.tests.models import Food from taggit.tests.models import Food, DirectFood
class FoodForm(forms.ModelForm): class FoodForm(forms.ModelForm):
class Meta: class Meta:
model = Food model = Food
class DirectFoodForm(forms.ModelForm):
class Meta:
model = DirectFood
from django.db import models from django.db import models
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from taggit.models import TaggedItemBase
class Food(models.Model): class Food(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
...@@ -11,11 +11,29 @@ class Food(models.Model): ...@@ -11,11 +11,29 @@ class Food(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class Pet(models.Model): class Pet(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager() tags = TaggableManager()
class HousePet(Pet): class HousePet(Pet):
trained = models.BooleanField() trained = models.BooleanField()
\ No newline at end of file
class TaggedFood(TaggedItemBase):
content_object = models.ForeignKey('DirectFood')
class DirectFood(models.Model):
name = models.CharField(max_length=50)
tags = TaggableManager(through=TaggedFood)
class TaggedPet(TaggedItemBase):
content_object = models.ForeignKey('DirectPet')
class DirectPet(models.Model):
name = models.CharField(max_length=50)
tags = TaggableManager(through=TaggedPet)
class DirectHousePet(DirectPet):
trained = models.BooleanField()
...@@ -4,8 +4,8 @@ from contextlib import contextmanager ...@@ -4,8 +4,8 @@ from contextlib import contextmanager
from django.test import TestCase from django.test import TestCase
from taggit.models import Tag from taggit.models import Tag
from taggit.tests.forms import FoodForm from taggit.tests.forms import FoodForm, DirectFoodForm
from taggit.tests.models import Food, Pet, HousePet from taggit.tests.models import Food, Pet, HousePet, DirectFood, DirectPet, DirectHousePet
class BaseTaggingTest(TestCase): class BaseTaggingTest(TestCase):
...@@ -27,128 +27,136 @@ class BaseTaggingTest(TestCase): ...@@ -27,128 +27,136 @@ class BaseTaggingTest(TestCase):
self.fail("No exception raised, expected %s" % exc_type) self.fail("No exception raised, expected %s" % exc_type)
class AddTagTestCase(BaseTaggingTest): class TaggableManagerTestCase(BaseTaggingTest):
food_model = Food
pet_model = Pet
housepet_model = HousePet
def test_add_tag(self): def test_add_tag(self):
apple = Food.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
self.assertEqual(list(apple.tags.all()), []) self.assertEqual(list(apple.tags.all()), [])
self.assertEqual(list(Food.tags.all()), []) self.assertEqual(list(self.food_model.tags.all()), [])
apple.tags.add('green') apple.tags.add('green')
self.assert_tags_equal(apple.tags.all(), ['green']) self.assert_tags_equal(apple.tags.all(), ['green'])
self.assert_tags_equal(Food.tags.all(), ['green']) self.assert_tags_equal(self.food_model.tags.all(), ['green'])
pear = Food.objects.create(name="pear") pear = self.food_model.objects.create(name="pear")
pear.tags.add('green') pear.tags.add('green')
self.assert_tags_equal(pear.tags.all(), ['green']) self.assert_tags_equal(pear.tags.all(), ['green'])
self.assert_tags_equal(Food.tags.all(), ['green']) self.assert_tags_equal(self.food_model.tags.all(), ['green'])
apple.tags.add('red') apple.tags.add('red')
self.assert_tags_equal(apple.tags.all(), ['green', 'red']) self.assert_tags_equal(apple.tags.all(), ['green', 'red'])
self.assert_tags_equal(Food.tags.all(), ['green', 'red']) self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
self.assert_tags_equal(Food.tags.most_common(), ['green', 'red'], sort=False) self.assert_tags_equal(self.food_model.tags.most_common(), ['green', 'red'], sort=False)
apple.tags.remove('green') apple.tags.remove('green')
self.assert_tags_equal(apple.tags.all(), ['red']) self.assert_tags_equal(apple.tags.all(), ['red'])
self.assert_tags_equal(Food.tags.all(), ['green', 'red']) self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
tag = Tag.objects.create(name="delicious") tag = Tag.objects.create(name="delicious")
apple.tags.add(tag) apple.tags.add(tag)
self.assert_tags_equal(apple.tags.all(), ["red", "delicious"]) self.assert_tags_equal(apple.tags.all(), ["red", "delicious"])
apple.delete() apple.delete()
self.assert_tags_equal(Food.tags.all(), ["green"]) self.assert_tags_equal(self.food_model.tags.all(), ["green"])
f = Food() f = self.food_model()
with self.assert_raises(ValueError): with self.assert_raises(ValueError):
f.tags.all() f.tags.all()
def test_unique_slug(self): def test_unique_slug(self):
apple = Food.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
apple.tags.add("Red", "red") apple.tags.add("Red", "red")
class DeleteObjecTestCase(BaseTaggingTest):
def test_delete_obj(self): def test_delete_obj(self):
apple = Food.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
apple.tags.add("red") apple.tags.add("red")
self.assert_tags_equal(apple.tags.all(), ["red"]) self.assert_tags_equal(apple.tags.all(), ["red"])
strawberry = Food.objects.create(name="strawberry") strawberry = self.food_model.objects.create(name="strawberry")
strawberry.tags.add("red") strawberry.tags.add("red")
apple.delete() apple.delete()
self.assert_tags_equal(strawberry.tags.all(), ["red"]) self.assert_tags_equal(strawberry.tags.all(), ["red"])
class LookupByTagTestCase(BaseTaggingTest):
def test_lookup_by_tag(self): def test_lookup_by_tag(self):
apple = Food.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
apple.tags.add("red", "green") apple.tags.add("red", "green")
pear = Food.objects.create(name="pear") pear = self.food_model.objects.create(name="pear")
pear.tags.add("green") pear.tags.add("green")
self.assertEqual(list(Food.objects.filter(tags__in=["red"])), [apple]) self.assertEqual(list(self.food_model.objects.filter(tags__in=["red"])), [apple])
self.assertEqual(list(Food.objects.filter(tags__in=["green"])), [apple, pear]) self.assertEqual(list(self.food_model.objects.filter(tags__in=["green"])), [apple, pear])
kitty = Pet.objects.create(name="kitty") kitty = self.pet_model.objects.create(name="kitty")
kitty.tags.add("fuzzy", "red") kitty.tags.add("fuzzy", "red")
dog = Pet.objects.create(name="dog") dog = self.pet_model.objects.create(name="dog")
dog.tags.add("woof", "red") dog.tags.add("woof", "red")
self.assertEqual(list(Food.objects.filter(tags__in=["red"]).distinct()), [apple]) self.assertEqual(list(self.food_model.objects.filter(tags__in=["red"]).distinct()), [apple])
tag = Tag.objects.get(name="woof") tag = Tag.objects.get(name="woof")
self.assertEqual(list(Pet.objects.filter(tags__in=[tag])), [dog]) self.assertEqual(list(self.pet_model.objects.filter(tags__in=[tag])), [dog])
cat = HousePet.objects.create(name="cat", trained=True) cat = self.housepet_model.objects.create(name="cat", trained=True)
cat.tags.add("fuzzy") cat.tags.add("fuzzy")
self.assertEqual( self.assertEqual(
map(lambda o: o.pk, Pet.objects.filter(tags__in=["fuzzy"])), map(lambda o: o.pk, self.pet_model.objects.filter(tags__in=["fuzzy"])),
[kitty.pk, cat.pk] [kitty.pk, cat.pk]
) )
def test_similarity_by_tag(self):
"""Test that pears are more similar to apples than watermelons"""
apple = self.food_model.objects.create(name="apple")
apple.tags.add("green", "juicy", "small", "sour")
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green", "juicy", "small", "sweet")
watermelon = self.food_model.objects.create(name="watermelon")
watermelon.tags.add("green", "juicy", "large", "sweet")
similar_objs = apple.tags.similar_objects()
self.assertEqual(similar_objs, [pear, watermelon])
self.assertEqual(map(lambda x: x.similar_tags, similar_objs), [3, 2])
def test_tag_reuse(self):
apple = self.food_model.objects.create(name="apple")
apple.tags.add("juicy", "juicy")
self.assert_tags_equal(apple.tags.all(), ['juicy'])
class TaggableManagerDirectTestCase(TaggableManagerTestCase):
food_model = DirectFood
pet_model = DirectPet
housepet_model = DirectHousePet
class TaggableFormTestCase(BaseTaggingTest): class TaggableFormTestCase(BaseTaggingTest):
form_class = FoodForm
food_model = Food
def test_form(self): def test_form(self):
self.assertEqual(FoodForm.base_fields.keys(), ['name', 'tags']) self.assertEqual(self.form_class.base_fields.keys(), ['name', 'tags'])
f = FoodForm({'name': 'apple', 'tags': 'green, red, yummy'}) f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'})
self.assertEqual(str(f), """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>\n<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /></td></tr>""") self.assertEqual(str(f), """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>\n<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /></td></tr>""")
f.save() f.save()
apple = Food.objects.get(name='apple') apple = self.food_model.objects.get(name='apple')
self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy']) self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy'])
f = FoodForm({'name': 'apple', 'tags': 'green, red, yummy, delicious'}, instance=apple) f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy, delicious'}, instance=apple)
f.save() f.save()
apple = Food.objects.get(name='apple') apple = self.food_model.objects.get(name='apple')
self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy', 'delicious']) self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy', 'delicious'])
self.assertEqual(Food.objects.count(), 1) self.assertEqual(self.food_model.objects.count(), 1)
f = FoodForm({"name": "raspberry"}) f = self.form_class({"name": "raspberry"})
raspberry = f.save() raspberry = f.save()
self.assert_tags_equal(raspberry.tags.all(), []) self.assert_tags_equal(raspberry.tags.all(), [])
f = FoodForm(instance=apple) f = self.form_class(instance=apple)
self.assertEqual(str(f), """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>\n<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy, delicious" id="id_tags" /></td></tr>""") self.assertEqual(str(f), """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>\n<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy, delicious" id="id_tags" /></td></tr>""")
class TaggableFormDirectTestCase(TaggableFormTestCase):
class SimilarityByTagTestCase(BaseTaggingTest): form_class = DirectFoodForm
def test_similarity_by_tag(self): food_model = DirectFood
"""Test that pears are more similar to apples than watermelons"""
apple = Food.objects.create(name="apple")
apple.tags.add("green", "juicy", "small", "sour")
pear = Food.objects.create(name="pear")
pear.tags.add("green", "juicy", "small", "sweet")
watermelon = Food.objects.create(name="watermelon")
watermelon.tags.add("green", "juicy", "large", "sweet")
similar_objs = apple.tags.similar_objects()
self.assertEqual(similar_objs, [pear, watermelon])
self.assertEqual(map(lambda x: x.similar_tags, similar_objs), [3, 2])
class TagReuseTestCase(BaseTaggingTest):
def test_tag_reuse(self):
apple = Food.objects.create(name="apple")
apple.tags.add("juicy", "juicy")
self.assert_tags_equal(apple.tags.all(), ['juicy'])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment