Commit ed8af524 by Florian Apolloner

Merge branch 'develop'

parents 316ee157 20780f93
Changelog
=========
0.12.2 (21.09.2014)
~~~~~~~~~~~~~~~~~~~
* Fixed 1.7 migrations.
0.12.1 (10.08.2014)
~~~~~~~~~~~~~~~~~~~
* Final (hopefully) fixes for the upcoming Django 1.7 release.
......
......@@ -3,3 +3,14 @@ license-file = LICENSE
[wheel]
universal=1
[flake8]
# ignore=NONE
# max-line-length=100
# E302: expected 2 blank lines, found 1 [E302]
# E501: line too long
ignore=E501,E302
[isort]
forced_separate=tests,taggit
......@@ -7,7 +7,7 @@ f.close()
setup(
name='django-taggit',
version='0.12.1',
version='0.12.2',
description='django-taggit is a reusable Django application for simple tagging.',
long_description=readme,
author='Alex Gaynor',
......
VERSION = (0, 12, 1)
VERSION = (0, 12, 2)
from __future__ import unicode_literals
from django import forms
from django.utils.translation import ugettext as _
from django.utils import six
from django.utils.translation import ugettext as _
from taggit.utils import parse_tags, edit_string_for_tags
from taggit.utils import edit_string_for_tags, parse_tags
class TagWidget(forms.TextInput):
def render(self, name, value, attrs=None):
if value is not None and not isinstance(value, six.string_types):
value = edit_string_for_tags([o.tag for o in value.select_related("tag")])
value = edit_string_for_tags([
o.tag for o in value.select_related("tag")])
return super(TagWidget, self).render(name, value, attrs)
class TagField(forms.CharField):
widget = TagWidget
......@@ -21,4 +23,5 @@ class TagField(forms.CharField):
try:
return parse_tags(value)
except ValueError:
raise forms.ValidationError(_("Please provide a comma-separated list of tags."))
raise forms.ValidationError(
_("Please provide a comma-separated list of tags."))
from __future__ import unicode_literals
from operator import attrgetter
from django import VERSION
try:
from django.contrib.contenttypes.fields import GenericRelation
except ImportError: # django < 1.7
from django.contrib.contenttypes.generic import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models, router
from django.db.models.fields import Field
from django.db.models.fields.related import ManyToManyRel, RelatedField, add_lazy_relation
from django.db.models.fields.related import (add_lazy_relation, ManyToManyRel,
RelatedField)
from django.db.models.related import RelatedObject
from django.utils import six
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from django.utils import six
from taggit.forms import TagField
from taggit.models import GenericTaggedItemBase, TaggedItem
from taggit.utils import require_instance_manager
try:
from django.contrib.contenttypes.fields import GenericRelation
except ImportError: # django < 1.7
from django.contrib.contenttypes.generic import GenericRelation
try:
from django.db.models.related import PathInfo
except ImportError:
pass # PathInfo is not used on Django < 1.6
from taggit.forms import TagField
from taggit.models import TaggedItem, GenericTaggedItemBase
from taggit.utils import require_instance_manager
def _model_name(model):
......@@ -102,7 +106,7 @@ class _TaggableManager(models.Manager):
else 'content_object')
fk = self.through._meta.get_field(fieldname)
query = {
'%s__%s__in' % (self.through.tag_relname(), fk.name) :
'%s__%s__in' % (self.through.tag_relname(), fk.name):
set(obj._get_pk_val() for obj in instances)
}
join_table = self.through._meta.db_table
......@@ -110,13 +114,13 @@ class _TaggableManager(models.Manager):
connection = connections[db]
qn = connection.ops.quote_name
qs = self.get_queryset().using(db)._next_is_sticky().filter(**query).extra(
select = {
'_prefetch_related_val' : '%s.%s' % (qn(join_table), qn(source_col))
select={
'_prefetch_related_val': '%s.%s' % (qn(join_table), qn(source_col))
}
)
return (qs,
attrgetter('_prefetch_related_val'),
attrgetter(instance._meta.pk.name),
lambda obj: obj._get_pk_val(),
False,
self.prefetch_cache_name)
......@@ -129,12 +133,17 @@ class _TaggableManager(models.Manager):
@require_instance_manager
def add(self, *tags):
str_tags = set([
t
for t in tags
if not isinstance(t, self.through.tag_model())
])
tag_objs = set(tags) - str_tags
str_tags = set()
tag_objs = set()
for t in tags:
if isinstance(t, self.through.tag_model()):
tag_objs.add(t)
elif isinstance(t, six.string_types):
str_tags.add(t)
else:
raise ValueError("Cannot add {0} ({1}). Expected {2} or str.".format(
t, type(t), type(self.through.tag_model())))
# If str_tags has 0 elements Django actually optimizes that to not do a
# query. Malcolm is very smart.
existing = self.through.tag_model().objects.filter(
......@@ -220,10 +229,12 @@ class _TaggableManager(models.Manager):
class TaggableManager(RelatedField, Field):
_related_name_counter = 0
def __init__(self, verbose_name=_("Tags"), help_text=_("A comma-separated list of tags."),
def __init__(self, verbose_name=_("Tags"),
help_text=_("A comma-separated list of tags."),
through=None, blank=False, related_name=None, to=None,
manager=_TaggableManager):
Field.__init__(self, verbose_name=verbose_name, help_text=help_text, blank=blank, null=True, serialize=False)
Field.__init__(self, verbose_name=verbose_name, help_text=help_text,
blank=blank, null=True, serialize=False)
self.through = through or TaggedItem
self.rel = TaggableRel(self, related_name, self.through, to=to)
self.swappable = False
......@@ -238,7 +249,7 @@ class TaggableManager(RelatedField, Field):
through=self.through,
model=model,
instance=instance,
prefetch_cache_name = self.name
prefetch_cache_name=self.name
)
return manager
......@@ -287,7 +298,6 @@ class TaggableManager(RelatedField, Field):
else:
self.post_through_setup(cls)
def __lt__(self, other):
"""
Required contribute_to_class as Django uses bisect
......@@ -364,7 +374,7 @@ class TaggableManager(RelatedField, Field):
def extra_filters(self, pieces, pos, negate):
if negate or not self.use_gfk:
return []
prefix = "__".join(["tagged_items"] + pieces[:pos-2])
prefix = "__".join(["tagged_items"] + pieces[:pos - 2])
get = ContentType.objects.get_for_model
cts = [get(obj) for obj in _get_subclasses(self.model)]
if len(cts) == 1:
......@@ -378,13 +388,18 @@ class TaggableManager(RelatedField, Field):
else:
alias_to_join = lhs_alias
extra_col = self.through._meta.get_field_by_name('content_type')[0].column
content_type_ids = [ContentType.objects.get_for_model(subclass).pk for subclass in _get_subclasses(self.model)]
content_type_ids = [ContentType.objects.get_for_model(subclass).pk for
subclass in _get_subclasses(self.model)]
if len(content_type_ids) == 1:
content_type_id = content_type_ids[0]
extra_where = " AND %s.%s = %%s" % (qn(alias_to_join), qn(extra_col))
extra_where = " AND %s.%s = %%s" % (qn(alias_to_join),
qn(extra_col))
params = [content_type_id]
else:
extra_where = " AND %s.%s IN (%s)" % (qn(alias_to_join), qn(extra_col), ','.join(['%s']*len(content_type_ids)))
extra_where = " AND %s.%s IN (%s)" % (qn(alias_to_join),
qn(extra_col),
','.join(['%s'] *
len(content_type_ids)))
params = content_type_ids
return extra_where, params
......
# encoding: utf8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
......@@ -27,9 +28,9 @@ class Migration(migrations.Migration):
name='TaggedItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('tag', models.ForeignKey(to='taggit.Tag', to_field='id')),
('object_id', models.IntegerField(verbose_name='Object id', db_index=True)),
('content_type', models.ForeignKey(to='contenttypes.ContentType', to_field='id', verbose_name='Content type')),
('content_type', models.ForeignKey(related_name='taggit_taggeditem_tagged_items', verbose_name='Content type', to='contenttypes.ContentType')),
('tag', models.ForeignKey(related_name='taggit_taggeditem_items', to='taggit.Tag')),
],
options={
'verbose_name': 'Tagged Item',
......
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.db import IntegrityError, models, transaction
from django.db.models.query import QuerySet
from django.template.defaultfilters import slugify as default_slugify
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
try:
from django.contrib.contenttypes.fields import GenericForeignKey
except ImportError: # django < 1.7
from django.contrib.contenttypes.generic import GenericForeignKey
from django.db import models, IntegrityError, transaction
from django.db.models.query import QuerySet
from django.template.defaultfilters import slugify as default_slugify
from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import python_2_unicode_compatible
try:
......@@ -59,7 +61,7 @@ class TagBase(models.Model):
except IntegrityError:
pass
# Now try to find existing slugs with similar names
slugs = set(Tag.objects.filter(slug__startswith=self.slug)\
slugs = set(Tag.objects.filter(slug__startswith=self.slug)
.values_list('slug', flat=True))
i = 1
while True:
......@@ -145,7 +147,7 @@ class GenericTaggedItemBase(ItemBase):
content_object = GenericForeignKey()
class Meta:
abstract=True
abstract = True
@classmethod
def lookup_kwargs(cls, instance):
......
from __future__ import unicode_literals
from django.utils import six
from django.utils.encoding import force_text
from django.utils.functional import wraps
from django.utils import six
def parse_tags(tagstring):
......
......@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from django.views.generic.list import ListView
from taggit.models import TaggedItem, Tag
from taggit.models import Tag, TaggedItem
def tagged_object_list(request, slug, queryset, **kwargs):
......@@ -18,4 +18,3 @@ def tagged_object_list(request, slug, queryset, **kwargs):
kwargs["extra_context"] = {}
kwargs["extra_context"]["tag"] = tag
return ListView.as_view(request, qs, **kwargs)
from __future__ import unicode_literals, absolute_import
from __future__ import absolute_import, unicode_literals
from django import forms, VERSION
from .models import Food, DirectFood, CustomPKFood, OfficialFood
from .models import CustomPKFood, DirectFood, Food, OfficialFood
fields = None
if VERSION >= (1,6):
if VERSION >= (1, 6):
fields = '__all__'
......
......@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CustomPKHousePet',
fields=[
('custompkpet_ptr', models.OneToOneField(auto_created=True, primary_key=True, serialize=False, to='tests.CustomPKPet')),
('custompkpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.CustomPKPet')),
('trained', models.BooleanField(default=False)),
],
options={
......@@ -84,7 +84,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DirectHousePet',
fields=[
('directpet_ptr', models.OneToOneField(auto_created=True, primary_key=True, serialize=False, to='tests.DirectPet')),
('directpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.DirectPet')),
('trained', models.BooleanField(default=False)),
],
options={
......@@ -155,7 +155,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='OfficialHousePet',
fields=[
('officialpet_ptr', models.OneToOneField(auto_created=True, primary_key=True, serialize=False, to='tests.OfficialPet')),
('officialpet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.OfficialPet')),
('trained', models.BooleanField(default=False)),
],
options={
......@@ -180,8 +180,8 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('object_id', models.IntegerField(verbose_name='Object id', db_index=True)),
('content_type', models.ForeignKey(verbose_name='Content type', to='contenttypes.ContentType')),
('tag', models.ForeignKey(to='tests.OfficialTag')),
('content_type', models.ForeignKey(related_name='tests_officialthroughmodel_tagged_items', verbose_name='Content type', to='contenttypes.ContentType')),
('tag', models.ForeignKey(related_name='tagged_items', to='tests.OfficialTag')),
],
options={
'abstract': False,
......@@ -201,6 +201,30 @@ class Migration(migrations.Migration):
preserve_default=True,
),
migrations.CreateModel(
name='Parent',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Child',
fields=[
('parent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.Parent')),
],
options={
},
bases=('tests.parent',),
),
migrations.AddField(
model_name='parent',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', help_text='A comma-separated list of tags.', verbose_name='Tags'),
preserve_default=True,
),
migrations.CreateModel(
name='Pet',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
......@@ -213,7 +237,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='HousePet',
fields=[
('pet_ptr', models.OneToOneField(auto_created=True, primary_key=True, serialize=False, to='tests.Pet')),
('pet_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='tests.Pet')),
('trained', models.BooleanField(default=False)),
],
options={
......@@ -262,7 +286,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='taggedcustompkfood',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_taggedcustompkfood_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -290,7 +314,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='taggedcustompkpet',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_taggedcustompkpet_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -318,7 +342,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='taggedfood',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_taggedfood_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -346,7 +370,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='taggedpet',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_taggedpet_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -374,7 +398,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='through1',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_through1_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -402,7 +426,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='through2',
name='tag',
field=models.ForeignKey(to='taggit.Tag'),
field=models.ForeignKey(related_name='tests_through2_items', to='taggit.Tag'),
preserve_default=True,
),
migrations.CreateModel(
......@@ -410,8 +434,8 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('object_id', models.IntegerField(verbose_name='Object id', db_index=True)),
('content_type', models.ForeignKey(verbose_name='Content type', to='contenttypes.ContentType')),
('tag', models.ForeignKey(to='taggit.Tag')),
('content_type', models.ForeignKey(related_name='tests_throughgfk_tagged_items', verbose_name='Content type', to='contenttypes.ContentType')),
('tag', models.ForeignKey(related_name='tagged_items', to='taggit.Tag')),
],
options={
'abstract': False,
......
......@@ -4,8 +4,8 @@ from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from taggit.managers import TaggableManager
from taggit.models import (TaggedItemBase, GenericTaggedItemBase, TaggedItem,
TagBase, Tag)
from taggit.models import (GenericTaggedItemBase, Tag, TagBase, TaggedItem,
TaggedItemBase)
# Ensure that two TaggableManagers with custom through model are allowed.
......@@ -192,3 +192,11 @@ class CustomManager(models.Model):
pass
tags = TaggableManager(manager=Foo)
class Parent(models.Model):
tags = TaggableManager()
class Child(Parent):
pass
from __future__ import unicode_literals, absolute_import
from __future__ import absolute_import, unicode_literals
from unittest import TestCase as UnitTestCase
try:
from unittest import skipIf, skipUnless
except:
from django.utils.unittest import skipIf, skipUnless
import django
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.db import connection
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.test import TestCase, TransactionTestCase
from django.utils import six
from django.utils.encoding import force_text
from django.contrib.contenttypes.models import ContentType
from .forms import CustomPKFoodForm, DirectFoodForm, FoodForm, OfficialFoodForm
from .models import (Article, Child, CustomManager, CustomPKFood,
CustomPKHousePet, CustomPKPet, DirectFood,
DirectHousePet, DirectPet, Food, HousePet, Movie,
OfficialFood, OfficialHousePet, OfficialPet,
OfficialTag, OfficialThroughModel, Pet, Photo,
TaggedCustomPKFood, TaggedCustomPKPet, TaggedFood,
TaggedPet)
from taggit.managers import TaggableManager, _TaggableManager, _model_name
from taggit.managers import _model_name, _TaggableManager, TaggableManager
from taggit.models import Tag, TaggedItem
from .forms import (FoodForm, DirectFoodForm, CustomPKFoodForm,
OfficialFoodForm)
from .models import (Food, Pet, HousePet, DirectFood, DirectPet,
DirectHousePet, TaggedFood, CustomPKFood, CustomPKPet, CustomPKHousePet,
TaggedCustomPKFood, OfficialFood, OfficialPet, OfficialHousePet,
OfficialThroughModel, OfficialTag, Photo, Movie, Article, CustomManager)
from taggit.utils import parse_tags, edit_string_for_tags
from taggit.utils import edit_string_for_tags, parse_tags
try:
from unittest import skipIf, skipUnless
except ImportError:
from django.utils.unittest import skipIf, skipUnless
class BaseTaggingTest(object):
......@@ -87,6 +88,14 @@ class TagModelTestCase(BaseTaggingTransactionTestCase):
"category-awesome-1"
], attr="slug")
def test_integers(self):
"""Adding an integer as a tag should raise a ValueError (#237)."""
apple = self.food_model.objects.create(name="apple")
with self.assertRaisesRegexp(ValueError, (
r"Cannot add 1 \(<(type|class) 'int'>\). "
r"Expected <class 'django.db.models.base.ModelBase'> or str.")):
apple.tags.add(1)
class TagModelDirectTestCase(TagModelTestCase):
food_model = DirectFood
tag_model = Tag
......@@ -150,7 +159,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
# make sure we don't double create.
# + 12 on Django 1.6 for save points.
queries = 22
if django.VERSION < (1,6):
if django.VERSION < (1, 6):
queries -= 12
self.assertNumQueries(queries, apple.tags.add, "red", "delicious", "green")
......@@ -160,7 +169,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
# make sure we dont't double create.
# + 4 on Django 1.6 for save points.
queries = 9
if django.VERSION < (1,6):
if django.VERSION < (1, 6):
queries -= 4
self.assertNumQueries(queries, pear.tags.add, "green", "delicious")
......@@ -254,7 +263,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
pear = self.food_model.objects.create(name="pear")
pear.tags.add("green", "delicious")
guava = self.food_model.objects.create(name="guava")
self.food_model.objects.create(name="guava")
pks = self.food_model.objects.exclude(tags__name__in=["red"])
model_name = self.food_model.__name__
......@@ -571,3 +580,20 @@ class SouthSupportTests(TestCase):
except ImproperlyConfigured as e:
exception = e
self.assertIn("SOUTH_MIGRATION_MODULES", exception.args[0])
class InheritedPrefetchTests(TestCase):
def test_inherited_tags_with_prefetch(self):
child = Child()
child.save()
child.tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
child = Child.objects.get()
no_prefetch_tags = child.tags.all()
self.assertEquals(4, no_prefetch_tags.count())
child = Child.objects.prefetch_related('tags').get()
prefetch_tags = child.tags.all()
self.assertEquals(4, prefetch_tags.count())
self.assertEquals(set([t.name for t in no_prefetch_tags]),
set([t.name for t in prefetch_tags]))
......@@ -13,7 +13,7 @@ deps17 =
https://github.com/django/django/archive/stable/1.7.x.zip#egg=django
commands =
python ./runtests.py
python ./runtests.py {posargs}
[testenv:py26-1.4.x]
......
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