Commit 31cbafbb by Alex Gaynor

Merge branch 'api-refactor'

parents f3657783 17ebe7b3
...@@ -56,7 +56,7 @@ Django ORM API. For example if you had a ``Food`` model, whose ...@@ -56,7 +56,7 @@ Django ORM API. For example if you had a ``Food`` model, whose
``TaggableManager`` was named ``tags``, you could find all the delicious fruit ``TaggableManager`` was named ``tags``, you could find all the delicious fruit
like so:: like so::
>>> Food.objects.filter(tags__in=["delicious"]) >>> Food.objects.filter(tags__name__in=["delicious"])
[<Food: apple>, <Food: pear>, <Food: plum>] [<Food: apple>, <Food: pear>, <Food: plum>]
...@@ -64,7 +64,10 @@ If you're filtering on multiple tags, it's very common to get duplicate ...@@ -64,7 +64,10 @@ If you're filtering on multiple tags, it's very common to get duplicate
results, because of the way relational databases work. Often you'll want to results, because of the way relational databases work. Often you'll want to
make use of the ``distinct()`` method on ``QuerySets``:: make use of the ``distinct()`` method on ``QuerySets``::
>>> Food.objects.filter(tags__in=["delicious", "red"]) >>> Food.objects.filter(tags__name__in=["delicious", "red"])
[<Food: apple>, <Food: apple>] [<Food: apple>, <Food: apple>]
>>> Food.objects.filter(tags__in=["delicious", "red"]).distinct() >>> Food.objects.filter(tags__name__in=["delicious", "red"]).distinct()
[<Food: apple>] [<Food: apple>]
You can also filter by the slug on tags. If you're using a custom ``Tag``
model you can use this API to filter on any fields it has.
...@@ -10,6 +10,10 @@ Unreleased. ...@@ -10,6 +10,10 @@ Unreleased.
* Added an index on the ``object_id`` field of ``TaggedItem``. * Added an index on the ``object_id`` field of ``TaggedItem``.
* When displaying tags always join them with commas, never spaces. * When displaying tags always join them with commas, never spaces.
* The docs are now available `online <http://django-taggit.readthedocs.org/>`_. * The docs are now available `online <http://django-taggit.readthedocs.org/>`_.
* Custom ``Tag`` models are now allowed.
* *Backwards incompatible* Filtering on tags is no longer
``filter(tags__in=["foo"])``, it is written
``filter(tags__name__in=["foo"])``.
0.8.0 0.8.0
~~~~~ ~~~~~
......
Using a Custom Tag or Through Model
===================================
By default ``django-taggit`` uses a "through model" with a
``GenericForeignKey`` on it, that has another ``ForeignKey`` to an included
``Tag`` model. However, there are some cases where this isn't desirable, for
example if you want the speed and referential guarantees of a real
``ForeignKey``, if you have a model with a non-integer primary key, or if you
want to store additional data about a tag, such as whether it is official. In
these cases ``django-taggit`` makes it easy to substitute your own through
model, or ``Tag`` model.
Your intermediary model must be a subclass of
``taggit.models.TaggedItemBase`` with a foreign key to your content
model named ``content_object``. Pass this intermediary model as the
``through`` argument to ``TaggableManager``::
from django.db import models
from taggit.managers import TaggableManager
from taggit.models import TaggedItemBase
class TaggedFood(TaggedItemBase):
content_object = models.ForeignKey('Food')
class Food(models.Model):
# ... fields here
tags = TaggableManager(through=TaggedFood)
Once this is done, the API works the same as for GFK-tagged models.
To change the behavior in other ways there are a number of other classes you
can subclass to obtain different behavior:
========================= ===========================================================
Class name Behavior
========================= ===========================================================
``TaggedItemBase`` Allows custom ``ForeignKeys`` to models.
``GenericTaggedItemBase`` Allows custom ``Tag`` models.
``ItemBase`` Allows custom ``Tag`` models and ``ForeignKeys`` to models.
========================= ===========================================================
When providing a custom ``Tag`` model it should be a ``ForeignKey`` to your tag model named ``"tag"``.
Using a Custom Through Model
============================
By default ``django-taggit`` uses a "through model" with a
``GenericForeignKey`` on it. However, there are some cases where this
isn't desirable, for example if you want the speed and referential
guarantees of a real ``ForeignKey``, or if you have a model with a
non-integer primary key. In these cases ``django-taggit`` makes it
easy to substitute your own through model.
Youe intermediary model must be a subclass of
``taggit.models.TaggedItemBase`` with a foreign key to your content
model named ``content_object``. Pass this intermediary model as the
``through`` argument to ``TaggableManager``::
from django.db import models
from taggit.managers import TaggableManager
from taggit.models import TaggedItemBase
class TaggedFood(TaggedItemBase):
content_object = models.ForeignKey('Food')
class Food(models.Model):
# ... fields here
tags = TaggableManager(through=TaggedFood)
Once this is done, the API works the same as for GFK-tagged models.
...@@ -13,7 +13,7 @@ for known issues with older versions of Django), and Python 2.4-2.X. ...@@ -13,7 +13,7 @@ for known issues with older versions of Django), and Python 2.4-2.X.
getting_started getting_started
forms forms
api api
custom_through custom_tagging
issues issues
changelog changelog
......
...@@ -24,7 +24,7 @@ def runtests(*test_args): ...@@ -24,7 +24,7 @@ def runtests(*test_args):
test_args = ['tests', 'suggest'] test_args = ['tests', 'suggest']
parent = dirname(abspath(__file__)) parent = dirname(abspath(__file__))
sys.path.insert(0, parent) sys.path.insert(0, parent)
failures = run_tests(test_args, verbosity=1, interactive=True) failures = run_tests(test_args, verbosity=1, interactive=True, failfast=True)
sys.exit(failures) sys.exit(failures)
......
...@@ -9,7 +9,7 @@ from taggit.models import Tag ...@@ -9,7 +9,7 @@ from taggit.models import Tag
class SuggestCase(TestCase): class SuggestCase(TestCase):
def test_simple_suggest(self): def test_simple_suggest(self):
ku_tag = Tag.objects.create(name='ku') ku_tag = Tag.objects.create(name='ku')
ku_keyword1 = TagKeyword.objects.create( TagKeyword.objects.create(
tag=ku_tag, tag=ku_tag,
keyword='kansas university' keyword='kansas university'
) )
...@@ -31,7 +31,7 @@ class SuggestCase(TestCase): ...@@ -31,7 +31,7 @@ class SuggestCase(TestCase):
def test_bad_regex(self): def test_bad_regex(self):
ku_tag = Tag.objects.create(name='ku') ku_tag = Tag.objects.create(name='ku')
ku_keyword1 = TagKeyword.objects.create( TagKeyword.objects.create(
tag=ku_tag, tag=ku_tag,
keyword='kansas university' keyword='kansas university'
) )
......
import re import re
from django.conf import settings
from taggit.contrib.suggest.models import TagKeyword, TagRegex from taggit.contrib.suggest.models import TagKeyword, TagRegex
from taggit.models import Tag from taggit.models import Tag
......
import django from django.contrib.contenttypes.generic import GenericRelation
from django.contrib.contenttypes.generic import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyRel
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.db.models.query_utils import QueryWrapper
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taggit.forms import TagField from taggit.forms import TagField
from taggit.models import Tag, TaggedItem from taggit.models import Tag, TaggedItem, GenericTaggedItemBase
from taggit.utils import require_instance_manager from taggit.utils import require_instance_manager
...@@ -39,9 +37,9 @@ class TaggableRel(ManyToManyRel): ...@@ -39,9 +37,9 @@ class TaggableRel(ManyToManyRel):
class TaggableManager(object): class TaggableManager(object):
def __init__(self, verbose_name=_("Tags"), through=None): def __init__(self, verbose_name=_("Tags"), through=None):
self.use_gfk = through is None self.use_gfk = through is None or issubclass(through, GenericTaggedItemBase)
self.through = through or TaggedItem self.through = through or TaggedItem
self.rel = TaggableRel(to=self.through) self.rel = TaggableRel(to=self.through._meta.get_field("tag").rel.to)
self.verbose_name = verbose_name self.verbose_name = verbose_name
self.editable = True self.editable = True
self.unique = False self.unique = False
...@@ -67,46 +65,18 @@ class TaggableManager(object): ...@@ -67,46 +65,18 @@ class TaggableManager(object):
self.model = cls self.model = cls
cls._meta.add_field(self) cls._meta.add_field(self)
setattr(cls, name, self) setattr(cls, name, self)
if self.use_gfk:
tagged_items = GenericRelation(self.through)
tagged_items.contribute_to_class(cls, "tagged_items")
def save_form_data(self, instance, value): def save_form_data(self, instance, value):
getattr(instance, self.name).set(*value) getattr(instance, self.name).set(*value)
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
if lookup_type not in ["in", "isnull"]: return models.Field().get_prep_lookup(lookup_type, value)
# Users really shouldn't do "isnull" lookups, again: the ORM can,
# you can't.
raise ValueError("You can't do lookups other than \"in\" on Tags: "
"__%s=%s" % (lookup_type, value))
if hasattr(value, 'prepare'):
return value.prepare()
if hasattr(value, '_prepare'):
return value._prepare()
if lookup_type == "in":
if all(isinstance(v, Tag) for v in value):
value = self.through.objects.filter(tag__in=value)
elif all(isinstance(v, basestring) for v in value):
value = self.through.objects.filter(tag__name__in=value)
elif all(isinstance(v, (int, long)) for v in value):
# This one is really ackward, just don't do it. The ORM does
# it for deletes, but no one else gets to.
return value
else:
# Fucking flip-floppers.
raise ValueError("You can't combine Tag objects and strings. '%s' "
"was provided." % value)
if hasattr(models.Field, "get_prep_lookup"):
return models.Field().get_prep_lookup(lookup_type, value)
return models.Field().get_db_prep_lookup(lookup_type, value)
if django.VERSION < (1, 2): def get_db_prep_lookup(self, *args, **kwargs):
get_db_prep_lookup = get_prep_lookup return models.Field().get_db_prep_lookup(*args, **kwargs)
else:
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
if not prepared:
return self.get_prep_lookup(lookup_type, value)
return models.Field().get_db_prep_lookup(lookup_type, value, connection=connection, prepared=True)
def formfield(self, form_class=TagField, **kwargs): def formfield(self, form_class=TagField, **kwargs):
defaults = { defaults = {
...@@ -122,12 +92,10 @@ class TaggableManager(object): ...@@ -122,12 +92,10 @@ class TaggableManager(object):
return self.through.objects.none() return self.through.objects.none()
def related_query_name(self): def related_query_name(self):
return self.model._meta.object_name.lower() return self.model._meta.module_name
def m2m_reverse_name(self): def m2m_reverse_name(self):
if self.use_gfk: return self.through._meta.get_field_by_name("tag")[0].column
return "id"
return self.through._meta.pk.column
def m2m_column_name(self): def m2m_column_name(self):
if self.use_gfk: if self.use_gfk:
...@@ -143,7 +111,7 @@ class TaggableManager(object): ...@@ -143,7 +111,7 @@ class TaggableManager(object):
def extra_filters(self, pieces, pos, negate): def extra_filters(self, pieces, pos, negate):
if negate or not self.use_gfk: if negate or not self.use_gfk:
return [] return []
prefix = "__".join(pieces[:pos+1]) prefix = "__".join(["tagged_items"] + pieces[:pos-2])
cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model)) cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model))
if len(cts) == 1: if len(cts) == 1:
return [("%s__content_type" % prefix, cts[0])] return [("%s__content_type" % prefix, cts[0])]
...@@ -163,8 +131,8 @@ class _TaggableManager(models.Manager): ...@@ -163,8 +131,8 @@ class _TaggableManager(models.Manager):
@require_instance_manager @require_instance_manager
def add(self, *tags): def add(self, *tags):
for tag in tags: for tag in tags:
if not isinstance(tag, Tag): if not isinstance(tag, self.through.tag_model()):
tag, _ = Tag.objects.get_or_create(name=tag) tag, _ = self.through.tag_model().objects.get_or_create(name=tag)
self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs()) self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs())
@require_instance_manager @require_instance_manager
......
...@@ -6,7 +6,7 @@ from django.template.defaultfilters import slugify ...@@ -6,7 +6,7 @@ from django.template.defaultfilters import slugify
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
class Tag(models.Model): class TagBase(models.Model):
name = models.CharField(verbose_name=_('Name'), max_length=100) name = models.CharField(verbose_name=_('Name'), max_length=100)
slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100) slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100)
...@@ -14,8 +14,7 @@ class Tag(models.Model): ...@@ -14,8 +14,7 @@ class Tag(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = _("Tag") abstract = True
verbose_name_plural = _("Tags")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.pk and not self.slug: if not self.pk and not self.slug:
...@@ -35,7 +34,7 @@ class Tag(models.Model): ...@@ -35,7 +34,7 @@ class Tag(models.Model):
while True: while True:
try: try:
sid = transaction.savepoint(**trans_kwargs) sid = transaction.savepoint(**trans_kwargs)
res = super(Tag, self).save(*args, **kwargs) res = super(TagBase, self).save(*args, **kwargs)
transaction.savepoint_commit(sid, **trans_kwargs) transaction.savepoint_commit(sid, **trans_kwargs)
return res return res
except IntegrityError: except IntegrityError:
...@@ -43,15 +42,16 @@ class Tag(models.Model): ...@@ -43,15 +42,16 @@ class Tag(models.Model):
i += 1 i += 1
self.slug = "%s_%d" % (slug, i) self.slug = "%s_%d" % (slug, i)
else: else:
return super(Tag, self).save(*args, **kwargs) return super(TagBase, self).save(*args, **kwargs)
class Tag(TagBase):
class Meta:
verbose_name = _("Tag")
verbose_name_plural = _("Tags")
class TaggedItemBase(models.Model):
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")
class ItemBase(models.Model):
def __unicode__(self): def __unicode__(self):
return ugettext("%(object)s tagged with %(tag)s") % { return ugettext("%(object)s tagged with %(tag)s") % {
"object": self.content_object, "object": self.content_object,
...@@ -62,6 +62,10 @@ class TaggedItemBase(models.Model): ...@@ -62,6 +62,10 @@ class TaggedItemBase(models.Model):
abstract = True abstract = True
@classmethod @classmethod
def tag_model(cls):
return cls._meta.get_field_by_name("tag")[0].rel.to
@classmethod
def tag_relname(cls): def tag_relname(cls):
return cls._meta.get_field_by_name('tag')[0].rel.related_name return cls._meta.get_field_by_name('tag')[0].rel.related_name
...@@ -71,27 +75,46 @@ class TaggedItemBase(models.Model): ...@@ -71,27 +75,46 @@ class TaggedItemBase(models.Model):
'content_object': instance 'content_object': instance
} }
class TaggedItemBase(ItemBase):
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")
class Meta:
abstract = True
@classmethod @classmethod
def tags_for(cls, model, instance=None): def tags_for(cls, model, instance=None):
if instance is not None: if instance is not None:
return Tag.objects.filter(**{ return cls.tag_model().objects.filter(**{
'%s__content_object' % cls.tag_relname(): instance '%s__content_object' % cls.tag_relname(): instance
}) })
return Tag.objects.filter(**{ return cls.tag_model().objects.filter(**{
'%s__content_object__isnull' % cls.tag_relname(): False '%s__content_object__isnull' % cls.tag_relname(): False
}).distinct() }).distinct()
class TaggedItem(TaggedItemBase): class GenericTaggedItemBase(ItemBase):
object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True) object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True)
content_type = models.ForeignKey(ContentType, verbose_name=_('Content type'), if django.VERSION < (1, 2):
related_name="tagged_items") content_type = models.ForeignKey(
ContentType,
verbose_name=_('Content type'),
related_name="%(class)s_tagged_items"
)
else:
content_type = models.ForeignKey(
ContentType,
verbose_name=_('Content type'),
related_name="%(app_label)s_%(class)s_tagged_items"
)
content_object = GenericForeignKey() content_object = GenericForeignKey()
class Meta: class Meta:
verbose_name = _("Tagged Item") abstract=True
verbose_name_plural = _("Tagged Items")
@classmethod @classmethod
def lookup_kwargs(cls, instance): def lookup_kwargs(cls, instance):
return { return {
...@@ -102,12 +125,16 @@ class TaggedItem(TaggedItemBase): ...@@ -102,12 +125,16 @@ class TaggedItem(TaggedItemBase):
@classmethod @classmethod
def tags_for(cls, model, instance=None): def tags_for(cls, model, instance=None):
ct = ContentType.objects.get_for_model(model) ct = ContentType.objects.get_for_model(model)
kwargs = {
"%s__content_type" % cls.tag_relname(): ct
}
if instance is not None: if instance is not None:
return Tag.objects.filter(**{ kwargs["%s__object_id" % cls.tag_relname()] = instance.pk
'%s__object_id' % cls.tag_relname(): instance.pk, return cls.tag_model().objects.filter(**kwargs).distinct()
'%s__content_type' % cls.tag_relname(): ct
})
return Tag.objects.filter(**{ class TaggedItem(GenericTaggedItemBase, TaggedItemBase):
'%s__content_type' % cls.tag_relname(): ct class Meta:
}).distinct() verbose_name = _("Tagged Item")
verbose_name_plural = _("Tagged Items")
from django import forms from django import forms
from taggit.tests.models import Food, DirectFood, CustomPKFood from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood
class FoodForm(forms.ModelForm): class FoodForm(forms.ModelForm):
...@@ -14,3 +14,7 @@ class DirectFoodForm(forms.ModelForm): ...@@ -14,3 +14,7 @@ class DirectFoodForm(forms.ModelForm):
class CustomPKFoodForm(forms.ModelForm): class CustomPKFoodForm(forms.ModelForm):
class Meta: class Meta:
model = CustomPKFood model = CustomPKFood
class OfficialFoodForm(forms.ModelForm):
class Meta:
model = OfficialFood
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 from taggit.models import TaggedItemBase, GenericTaggedItemBase, TagBase
class Food(models.Model): class Food(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
...@@ -19,23 +20,23 @@ class Pet(models.Model): ...@@ -19,23 +20,23 @@ class Pet(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class HousePet(Pet): class HousePet(Pet):
trained = models.BooleanField() trained = models.BooleanField()
# test direct-tagging with custom through model
# Test direct-tagging with custom through model
class TaggedFood(TaggedItemBase): class TaggedFood(TaggedItemBase):
content_object = models.ForeignKey('DirectFood') content_object = models.ForeignKey('DirectFood')
class TaggedPet(TaggedItemBase):
content_object = models.ForeignKey('DirectPet')
class DirectFood(models.Model): class DirectFood(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager(through=TaggedFood) tags = TaggableManager(through=TaggedFood)
class TaggedPet(TaggedItemBase):
content_object = models.ForeignKey('DirectPet')
class DirectPet(models.Model): class DirectPet(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
...@@ -43,24 +44,27 @@ class DirectPet(models.Model): ...@@ -43,24 +44,27 @@ class DirectPet(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class DirectHousePet(DirectPet): class DirectHousePet(DirectPet):
trained = models.BooleanField() trained = models.BooleanField()
# test custom through model to model with custom PK
# Test custom through model to model with custom PK
class TaggedCustomPKFood(TaggedItemBase): class TaggedCustomPKFood(TaggedItemBase):
content_object = models.ForeignKey('CustomPKFood') content_object = models.ForeignKey('CustomPKFood')
class TaggedCustomPKPet(TaggedItemBase):
content_object = models.ForeignKey('CustomPKPet')
class CustomPKFood(models.Model): class CustomPKFood(models.Model):
name = models.CharField(max_length=50, primary_key=True) name = models.CharField(max_length=50, primary_key=True)
tags = TaggableManager(through=TaggedCustomPKFood) tags = TaggableManager(through=TaggedCustomPKFood)
class TaggedCustomPKPet(TaggedItemBase): def __unicode__(self):
content_object = models.ForeignKey('CustomPKPet') return self.name
class CustomPKPet(models.Model): class CustomPKPet(models.Model):
name = models.CharField(max_length=50, primary_key=True) name = models.CharField(max_length=50, primary_key=True)
...@@ -71,3 +75,31 @@ class CustomPKPet(models.Model): ...@@ -71,3 +75,31 @@ class CustomPKPet(models.Model):
class CustomPKHousePet(CustomPKPet): class CustomPKHousePet(CustomPKPet):
trained = models.BooleanField() trained = models.BooleanField()
# Test custom through model to a custom tag model
class OfficialTag(TagBase):
official = models.BooleanField()
class OfficialThroughModel(GenericTaggedItemBase):
tag = models.ForeignKey(OfficialTag, related_name="tagged_items")
class OfficialFood(models.Model):
name = models.CharField(max_length=50)
tags = TaggableManager(through=OfficialThroughModel)
def __unicode__(self):
return self.name
class OfficialPet(models.Model):
name = models.CharField(max_length=50)
tags = TaggableManager(through=OfficialThroughModel)
def __unicode__(self):
return self.name
class OfficialHousePet(OfficialPet):
trained = models.BooleanField()
...@@ -3,10 +3,12 @@ from unittest import TestCase as UnitTestCase ...@@ -3,10 +3,12 @@ from unittest import TestCase as UnitTestCase
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from taggit.models import Tag, TaggedItem from taggit.models import Tag, TaggedItem
from taggit.tests.forms import FoodForm, DirectFoodForm, CustomPKFoodForm from taggit.tests.forms import (FoodForm, DirectFoodForm, CustomPKFoodForm,
OfficialFoodForm)
from taggit.tests.models import (Food, Pet, HousePet, DirectFood, DirectPet, from taggit.tests.models import (Food, Pet, HousePet, DirectFood, DirectPet,
DirectHousePet, TaggedPet, CustomPKFood, CustomPKPet, CustomPKHousePet, DirectHousePet, TaggedPet, CustomPKFood, CustomPKPet, CustomPKHousePet,
TaggedCustomPKPet) TaggedCustomPKPet, OfficialFood, OfficialPet, OfficialHousePet,
OfficialThroughModel, OfficialTag)
from taggit.utils import parse_tags, edit_string_for_tags from taggit.utils import parse_tags, edit_string_for_tags
...@@ -27,23 +29,39 @@ class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest): ...@@ -27,23 +29,39 @@ class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
class TagModelTestCase(BaseTaggingTransactionTestCase): class TagModelTestCase(BaseTaggingTransactionTestCase):
food_model = Food food_model = Food
tag_model = Tag
def test_unique_slug(self): def test_unique_slug(self):
apple = self.food_model.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
apple.tags.add("Red", "red") apple.tags.add("Red", "red")
def test_update(self):
special = self.tag_model.objects.create(name="special")
special.save()
def test_add(self):
apple = self.food_model.objects.create(name="apple")
yummy = self.tag_model.objects.create(name="yummy")
apple.tags.add(yummy)
class TagModelDirectTestCase(TagModelTestCase): class TagModelDirectTestCase(TagModelTestCase):
food_model = DirectFood food_model = DirectFood
tag_model = Tag
class TagModelCustomPKTestCase(TagModelTestCase): class TagModelCustomPKTestCase(TagModelTestCase):
food_model = CustomPKFood food_model = CustomPKFood
tag_model = Tag
class TagModelOfficialTestCase(TagModelTestCase):
food_model = OfficialFood
tag_model = OfficialTag
class TaggableManagerTestCase(BaseTaggingTestCase): class TaggableManagerTestCase(BaseTaggingTestCase):
food_model = Food food_model = Food
pet_model = Pet pet_model = Pet
housepet_model = HousePet housepet_model = HousePet
taggeditem_model = TaggedItem taggeditem_model = TaggedItem
tag_model = Tag
def test_add_tag(self): def test_add_tag(self):
apple = self.food_model.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
...@@ -72,7 +90,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -72,7 +90,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
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(self.food_model.tags.all(), ['green', 'red']) self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
tag = Tag.objects.create(name="delicious") tag = self.tag_model.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"])
...@@ -108,13 +126,13 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -108,13 +126,13 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
apple.tags.add("red", "green") apple.tags.add("red", "green")
pear = self.food_model.objects.create(name="pear") pear = self.food_model.objects.create(name="pear")
pear.tags.add("green") pear.tags.add("green")
self.assertEqual( self.assertEqual(
list(self.food_model.objects.filter(tags__in=["red"])), list(self.food_model.objects.filter(tags__name__in=["red"])),
[apple] [apple]
) )
self.assertEqual( self.assertEqual(
list(self.food_model.objects.filter(tags__in=["green"])), list(self.food_model.objects.filter(tags__name__in=["green"])),
[apple, pear] [apple, pear]
) )
...@@ -123,18 +141,18 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -123,18 +141,18 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
dog = self.pet_model.objects.create(name="dog") dog = self.pet_model.objects.create(name="dog")
dog.tags.add("woof", "red") dog.tags.add("woof", "red")
self.assertEqual( self.assertEqual(
list(self.food_model.objects.filter(tags__in=["red"]).distinct()), list(self.food_model.objects.filter(tags__name__in=["red"]).distinct()),
[apple] [apple]
) )
tag = Tag.objects.get(name="woof") tag = self.tag_model.objects.get(name="woof")
self.assertEqual(list(self.pet_model.objects.filter(tags__in=[tag])), [dog]) self.assertEqual(list(self.pet_model.objects.filter(tags__name__in=[tag])), [dog])
cat = self.housepet_model.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, self.pet_model.objects.filter(tags__in=["fuzzy"])), map(lambda o: o.pk, self.pet_model.objects.filter(tags__name__in=["fuzzy"])),
[kitty.pk, cat.pk] [kitty.pk, cat.pk]
) )
...@@ -148,7 +166,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -148,7 +166,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
guava = self.food_model.objects.create(name="guava") guava = self.food_model.objects.create(name="guava")
self.assertEqual( self.assertEqual(
map(lambda o: o.pk, self.food_model.objects.exclude(tags__in=["red"])), map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"])),
[pear.pk, guava.pk], [pear.pk, guava.pk],
) )
...@@ -177,9 +195,11 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -177,9 +195,11 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
spike = self.pet_model.objects.create(name='Spike') spike = self.pet_model.objects.create(name='Spike')
spot.tags.add('scary') spot.tags.add('scary')
spike.tags.add('fluffy') spike.tags.add('fluffy')
lookup_kwargs = {'%s__name' % (self.pet_model._meta.object_name.lower()): 'Spot'} lookup_kwargs = {
'%s__name' % self.pet_model._meta.module_name: 'Spot'
}
self.assert_tags_equal( self.assert_tags_equal(
[i.tag for i in self.taggeditem_model.objects.filter(**lookup_kwargs)], self.tag_model.objects.filter(**lookup_kwargs),
['scary'] ['scary']
) )
...@@ -211,6 +231,28 @@ class TaggableManagerCustomPKTestCase(TaggableManagerTestCase): ...@@ -211,6 +231,28 @@ class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
# tell if the instance is saved or not # tell if the instance is saved or not
pass pass
class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
food_model = OfficialFood
pet_model = OfficialPet
housepet_model = OfficialHousePet
taggeditem_model = OfficialThroughModel
tag_model = OfficialTag
def test_extra_fields(self):
self.tag_model.objects.create(name="red")
self.tag_model.objects.create(name="delicious", official=True)
apple = self.food_model.objects.create(name="apple")
apple.tags.add("delicious", "red")
pear = self.food_model.objects.create(name="Pear")
pear.tags.add("delicious")
self.assertEqual(
map(lambda o: o.pk, self.food_model.objects.filter(tags__official=False)),
[apple.pk],
)
class TaggableFormTestCase(BaseTaggingTestCase): class TaggableFormTestCase(BaseTaggingTestCase):
form_class = FoodForm form_class = FoodForm
food_model = Food food_model = Food
...@@ -254,6 +296,10 @@ class TaggableFormCustomPKTestCase(TaggableFormTestCase): ...@@ -254,6 +296,10 @@ class TaggableFormCustomPKTestCase(TaggableFormTestCase):
form_class = CustomPKFoodForm form_class = CustomPKFoodForm
food_model = CustomPKFood food_model = CustomPKFood
class TaggableFormOfficialTestCase(TaggableFormTestCase):
form_class = OfficialFoodForm
food_model = OfficialFood
class TagStringParseTestCase(UnitTestCase): class TagStringParseTestCase(UnitTestCase):
""" """
......
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