Commit 08579a12 by Florian Apolloner

Merge branch 'EnTeQuAk-py3k'

Conflicts:
	MANIFEST.in
	docs/changelog.txt
	docs/index.txt
	setup.py
	taggit/tests/tests.py
parents 6f844384 d9fb9b16
...@@ -3,17 +3,28 @@ language: python ...@@ -3,17 +3,28 @@ language: python
python: python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.2"
- "3.3"
env: env:
- DJANGO=https://github.com/django/django/archive/master.tar.gz
- DJANGO=django==1.5 --use-mirrors - DJANGO=django==1.5 --use-mirrors
- DJANGO=django==1.4.5 --use-mirrors - DJANGO=django==1.4.5 --use-mirrors
- DJANGO=django==1.3.7 --use-mirrors
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install -r requirements/travis-ci.txt --use-mirrors
script: script:
- python setup.py test - coverage run --source django_taggit runtests.py
- coverage report
notifications: notifications:
email: false email: false
matrix:
exclude:
- python: "3.2"
env: DJANGO=django==1.4.5 --use-mirrors
- python: "3.3"
env: DJANGO=django==1.4.5 --use-mirrors
Changelog Changelog
========= =========
0.11.0 (unreleased)
~~~~~~~~~~~~~~~~~~~
* Python3 support
* *Backwards incompatible* Dropped support for Django < 1.4.5.
0.10.0 0.10.0
~~~~~~ ~~~~~~
......
...@@ -4,7 +4,7 @@ Welcome to django-taggit's documentation! ...@@ -4,7 +4,7 @@ Welcome to django-taggit's documentation!
``django-taggit`` is a reusable Django application designed to making adding ``django-taggit`` is a reusable Django application designed to making adding
tagging to your project easy and fun. tagging to your project easy and fun.
``django-taggit`` works with Django 1.3.X and newer and Python 2.4-2.X. ``django-taggit`` works with Django 1.4.5+ and Python 2.7-3.X.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
......
...@@ -27,9 +27,14 @@ setup( ...@@ -27,9 +27,14 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Framework :: Django', 'Framework :: Django',
], ],
test_suite='taggit.tests.runtests.runtests', test_suite='runtests.runtests',
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
) )
from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from taggit.models import Tag, TaggedItem from taggit.models import Tag, TaggedItem
......
from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import six
from taggit.utils import parse_tags, edit_string_for_tags from taggit.utils import parse_tags, edit_string_for_tags
class TagWidget(forms.TextInput): class TagWidget(forms.TextInput):
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
if value is not None and not isinstance(value, basestring): 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) return super(TagWidget, self).render(name, value, attrs)
......
from __future__ import unicode_literals
from django.contrib.contenttypes.generic import GenericRelation from django.contrib.contenttypes.generic import GenericRelation
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
...@@ -5,27 +7,13 @@ from django.db.models.fields.related import ManyToManyRel, RelatedField, add_laz ...@@ -5,27 +7,13 @@ from django.db.models.fields.related import ManyToManyRel, RelatedField, add_laz
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import six
from taggit.forms import TagField from taggit.forms import TagField
from taggit.models import TaggedItem, GenericTaggedItemBase from taggit.models import TaggedItem, GenericTaggedItemBase
from taggit.utils import require_instance_manager from taggit.utils import require_instance_manager
try:
all
except NameError:
# 2.4 compat
try:
from django.utils.itercompat import all
except ImportError:
# 1.1.X compat
def all(iterable):
for item in iterable:
if not item:
return False
return True
class TaggableRel(ManyToManyRel): class TaggableRel(ManyToManyRel):
def __init__(self): def __init__(self):
self.related_name = None self.related_name = None
...@@ -68,7 +56,7 @@ class TaggableManager(RelatedField): ...@@ -68,7 +56,7 @@ class TaggableManager(RelatedField):
cls._meta.add_field(self) cls._meta.add_field(self)
setattr(cls, name, self) setattr(cls, name, self)
if not cls._meta.abstract: if not cls._meta.abstract:
if isinstance(self.through, basestring): if isinstance(self.through, six.string_types):
def resolve_related_class(field, model, cls): def resolve_related_class(field, model, cls):
self.through = model self.through = model
self.post_through_setup(cls) self.post_through_setup(cls)
...@@ -78,6 +66,14 @@ class TaggableManager(RelatedField): ...@@ -78,6 +66,14 @@ class TaggableManager(RelatedField):
else: else:
self.post_through_setup(cls) self.post_through_setup(cls)
def __lt__(self, other):
"""
Required contribute_to_class as Django uses bisect
for ordered class contribution and bisect requires
a orderable type in py3.
"""
return False
def post_through_setup(self, cls): def post_through_setup(self, cls):
self.use_gfk = ( self.use_gfk = (
self.through is None or issubclass(self.through, GenericTaggedItemBase) self.through is None or issubclass(self.through, GenericTaggedItemBase)
...@@ -132,7 +128,8 @@ class TaggableManager(RelatedField): ...@@ -132,7 +128,8 @@ class TaggableManager(RelatedField):
if negate or not self.use_gfk: if negate or not self.use_gfk:
return [] return []
prefix = "__".join(["tagged_items"] + pieces[:pos-2]) prefix = "__".join(["tagged_items"] + pieces[:pos-2])
cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model)) get = ContentType.objects.get_for_model
cts = [get(obj) for obj in _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])]
return [("%s__content_type__in" % prefix, cts)] return [("%s__content_type__in" % prefix, cts)]
...@@ -197,7 +194,7 @@ class _TaggableManager(models.Manager): ...@@ -197,7 +194,7 @@ class _TaggableManager(models.Manager):
def similar_objects(self): def similar_objects(self):
lookup_kwargs = self._lookup_kwargs() lookup_kwargs = self._lookup_kwargs()
lookup_keys = sorted(lookup_kwargs) lookup_keys = sorted(lookup_kwargs)
qs = self.through.objects.values(*lookup_kwargs.keys()) qs = self.through.objects.values(*six.iterkeys(lookup_kwargs))
qs = qs.annotate(n=models.Count('pk')) qs = qs.annotate(n=models.Count('pk'))
qs = qs.exclude(**lookup_kwargs) qs = qs.exclude(**lookup_kwargs)
qs = qs.filter(tag__in=self.all()) qs = qs.filter(tag__in=self.all())
...@@ -220,7 +217,7 @@ class _TaggableManager(models.Manager): ...@@ -220,7 +217,7 @@ class _TaggableManager(models.Manager):
preload.setdefault(result['content_type'], set()) preload.setdefault(result['content_type'], set())
preload[result["content_type"]].add(result["object_id"]) preload[result["content_type"]].add(result["object_id"])
for ct, obj_ids in preload.iteritems(): for ct, obj_ids in preload.items():
ct = ContentType.objects.get_for_id(ct) ct = ContentType.objects.get_for_id(ct)
for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids): for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids):
items[(ct.pk, obj.pk)] = obj items[(ct.pk, obj.pk)] = obj
...@@ -243,3 +240,11 @@ def _get_subclasses(model): ...@@ -243,3 +240,11 @@ def _get_subclasses(model):
getattr(field.field.rel, "parent_link", None)): getattr(field.field.rel, "parent_link", None)):
subclasses.extend(_get_subclasses(field.model)) subclasses.extend(_get_subclasses(field.model))
return subclasses return subclasses
# `total_ordering` does not exist in Django 1.4, as such
# we special case this import to be py3k specific which
# is not supported by Django 1.4
if six.PY3:
from django.utils.functional import total_ordering
TaggableManager = total_ordering(TaggableManager)
\ No newline at end of file
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime import datetime
from south.db import db from south.db import db
from south.v2 import SchemaMigration from south.v2 import SchemaMigration
...@@ -9,51 +11,51 @@ class Migration(SchemaMigration): ...@@ -9,51 +11,51 @@ class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'Tag' # Adding model 'Tag'
db.create_table(u'taggit_tag', ( db.create_table('taggit_tag', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=100)), ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=100)), ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=100)),
)) ))
db.send_create_signal(u'taggit', ['Tag']) db.send_create_signal('taggit', ['Tag'])
# Adding model 'TaggedItem' # Adding model 'TaggedItem'
db.create_table(u'taggit_taggeditem', ( db.create_table('taggit_taggeditem', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('tag', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'taggit_taggeditem_items', to=orm['taggit.Tag'])), ('tag', self.gf('django.db.models.fields.related.ForeignKey')(related_name='taggit_taggeditem_items', to=orm['taggit.Tag'])),
('object_id', self.gf('django.db.models.fields.IntegerField')(db_index=True)), ('object_id', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'taggit_taggeditem_tagged_items', to=orm['contenttypes.ContentType'])), ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='taggit_taggeditem_tagged_items', to=orm['contenttypes.ContentType'])),
)) ))
db.send_create_signal(u'taggit', ['TaggedItem']) db.send_create_signal('taggit', ['TaggedItem'])
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'Tag' # Deleting model 'Tag'
db.delete_table(u'taggit_tag') db.delete_table('taggit_tag')
# Deleting model 'TaggedItem' # Deleting model 'TaggedItem'
db.delete_table(u'taggit_taggeditem') db.delete_table('taggit_taggeditem')
models = { models = {
u'contenttypes.contenttype': { 'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}, },
u'taggit.tag': { 'taggit.tag': {
'Meta': {'object_name': 'Tag'}, 'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
}, },
u'taggit.taggeditem': { 'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'}, 'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
} }
} }
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime import datetime
from south.db import db from south.db import db
from south.v2 import SchemaMigration from south.v2 import SchemaMigration
...@@ -9,34 +11,34 @@ class Migration(SchemaMigration): ...@@ -9,34 +11,34 @@ class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding unique constraint on 'Tag', fields ['name'] # Adding unique constraint on 'Tag', fields ['name']
db.create_unique(u'taggit_tag', ['name']) db.create_unique('taggit_tag', ['name'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'Tag', fields ['name'] # Removing unique constraint on 'Tag', fields ['name']
db.delete_unique(u'taggit_tag', ['name']) db.delete_unique('taggit_tag', ['name'])
models = { models = {
u'contenttypes.contenttype': { 'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}, },
u'taggit.tag': { 'taggit.tag': {
'Meta': {'object_name': 'Tag'}, 'Meta': {'object_name': 'Tag'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
}, },
u'taggit.taggeditem': { 'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'}, 'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}) 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
} }
} }
......
from __future__ import unicode_literals
import django 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, transaction from django.db import models, IntegrityError, transaction
from django.template.defaultfilters import slugify as default_slugify from django.template.defaultfilters import slugify as default_slugify
from django.utils.translation import ugettext_lazy as _, ugettext from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class TagBase(models.Model): class TagBase(models.Model):
name = models.CharField(verbose_name=_('Name'), unique=True, max_length=100) name = models.CharField(verbose_name=_('Name'), unique=True, 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)
def __unicode__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
...@@ -57,9 +61,9 @@ class Tag(TagBase): ...@@ -57,9 +61,9 @@ class Tag(TagBase):
verbose_name_plural = _("Tags") verbose_name_plural = _("Tags")
@python_2_unicode_compatible
class ItemBase(models.Model): class ItemBase(models.Model):
def __unicode__(self): def __str__(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,
"tag": self.tag "tag": self.tag
...@@ -90,9 +94,6 @@ class ItemBase(models.Model): ...@@ -90,9 +94,6 @@ class ItemBase(models.Model):
class TaggedItemBase(ItemBase): 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") tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items")
class Meta: class Meta:
......
from __future__ import unicode_literals
from django import forms from django import forms
from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood
......
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from taggit.models import (TaggedItemBase, GenericTaggedItemBase, TaggedItem, from taggit.models import (TaggedItemBase, GenericTaggedItemBase, TaggedItem,
TagBase, Tag) TagBase, Tag)
@python_2_unicode_compatible
class Food(models.Model): class Food(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager() tags = TaggableManager()
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class Pet(models.Model): class Pet(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager() tags = TaggableManager()
def __unicode__(self): def __str__(self):
return self.name return self.name
class HousePet(Pet): class HousePet(Pet):
trained = models.BooleanField() trained = models.BooleanField()
...@@ -30,22 +36,31 @@ class HousePet(Pet): ...@@ -30,22 +36,31 @@ class HousePet(Pet):
class TaggedFood(TaggedItemBase): class TaggedFood(TaggedItemBase):
content_object = models.ForeignKey('DirectFood') content_object = models.ForeignKey('DirectFood')
class TaggedPet(TaggedItemBase): class TaggedPet(TaggedItemBase):
content_object = models.ForeignKey('DirectPet') content_object = models.ForeignKey('DirectPet')
@python_2_unicode_compatible
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")
def __str__(self):
return self.name
@python_2_unicode_compatible
class DirectPet(models.Model): class DirectPet(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager(through=TaggedPet) tags = TaggableManager(through=TaggedPet)
def __unicode__(self): def __str__(self):
return self.name return self.name
class DirectHousePet(DirectPet): class DirectHousePet(DirectPet):
trained = models.BooleanField() trained = models.BooleanField()
...@@ -58,20 +73,22 @@ class TaggedCustomPKFood(TaggedItemBase): ...@@ -58,20 +73,22 @@ class TaggedCustomPKFood(TaggedItemBase):
class TaggedCustomPKPet(TaggedItemBase): class TaggedCustomPKPet(TaggedItemBase):
content_object = models.ForeignKey('CustomPKPet') content_object = models.ForeignKey('CustomPKPet')
@python_2_unicode_compatible
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)
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
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)
tags = TaggableManager(through=TaggedCustomPKPet) tags = TaggableManager(through=TaggedCustomPKPet)
def __unicode__(self): def __str__(self):
return self.name return self.name
class CustomPKHousePet(CustomPKPet): class CustomPKHousePet(CustomPKPet):
...@@ -85,20 +102,22 @@ class OfficialTag(TagBase): ...@@ -85,20 +102,22 @@ class OfficialTag(TagBase):
class OfficialThroughModel(GenericTaggedItemBase): class OfficialThroughModel(GenericTaggedItemBase):
tag = models.ForeignKey(OfficialTag, related_name="tagged_items") tag = models.ForeignKey(OfficialTag, related_name="tagged_items")
@python_2_unicode_compatible
class OfficialFood(models.Model): class OfficialFood(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager(through=OfficialThroughModel) tags = TaggableManager(through=OfficialThroughModel)
def __unicode__(self): def __str__(self):
return self.name return self.name
@python_2_unicode_compatible
class OfficialPet(models.Model): class OfficialPet(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
tags = TaggableManager(through=OfficialThroughModel) tags = TaggableManager(through=OfficialThroughModel)
def __unicode__(self): def __str__(self):
return self.name return self.name
class OfficialHousePet(OfficialPet): class OfficialHousePet(OfficialPet):
...@@ -129,6 +148,7 @@ class ArticleTag(Tag): ...@@ -129,6 +148,7 @@ class ArticleTag(Tag):
slug += "-%d" % i slug += "-%d" % i
return slug return slug
class ArticleTaggedItem(TaggedItem): class ArticleTaggedItem(TaggedItem):
class Meta: class Meta:
proxy = True proxy = True
...@@ -137,6 +157,7 @@ class ArticleTaggedItem(TaggedItem): ...@@ -137,6 +157,7 @@ class ArticleTaggedItem(TaggedItem):
def tag_model(self): def tag_model(self):
return ArticleTag return ArticleTag
class Article(models.Model): class Article(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
......
from __future__ import unicode_literals
from unittest import TestCase as UnitTestCase from unittest import TestCase as UnitTestCase
import django import django
...@@ -5,6 +7,8 @@ from django.conf import settings ...@@ -5,6 +7,8 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection from django.db import connection
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from django.utils import six
from django.utils.encoding import force_text
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from taggit.models import Tag, TaggedItem from taggit.models import Tag, TaggedItem
...@@ -19,7 +23,7 @@ from taggit.utils import parse_tags, edit_string_for_tags ...@@ -19,7 +23,7 @@ from taggit.utils import parse_tags, edit_string_for_tags
class BaseTaggingTest(object): class BaseTaggingTest(object):
def assert_tags_equal(self, qs, tags, sort=True, attr="name"): def assert_tags_equal(self, qs, tags, sort=True, attr="name"):
got = map(lambda tag: getattr(tag, attr), qs) got = [getattr(obj, attr) for obj in qs]
if sort: if sort:
got.sort() got.sort()
tags.sort() tags.sort()
...@@ -52,15 +56,13 @@ class BaseTaggingTest(object): ...@@ -52,15 +56,13 @@ class BaseTaggingTest(object):
return form_str return form_str
def assert_form_renders(self, form, html): def assert_form_renders(self, form, html):
try:
self.assertHTMLEqual(str(form), self._get_form_str(html)) self.assertHTMLEqual(str(form), self._get_form_str(html))
except AttributeError:
self.assertEqual(str(form), self._get_form_str(html))
class BaseTaggingTestCase(TestCase, BaseTaggingTest): class BaseTaggingTestCase(TestCase, BaseTaggingTest):
pass pass
class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest): class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
pass pass
...@@ -214,10 +216,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -214,10 +216,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
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( pks = self.pet_model.objects.filter(tags__name__in=["fuzzy"])
map(lambda o: o.pk, self.pet_model.objects.filter(tags__name__in=["fuzzy"])), model_name = self.pet_model.__name__
[kitty.pk, cat.pk] self.assertQuerysetEqual(pks,
) ['<{0}: kitty>'.format(model_name),
'<{0}: cat>'.format(model_name)],
ordered=False)
def test_exclude(self): def test_exclude(self):
apple = self.food_model.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
...@@ -228,10 +232,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -228,10 +232,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
guava = self.food_model.objects.create(name="guava") guava = self.food_model.objects.create(name="guava")
self.assertEqual( pks = self.food_model.objects.exclude(tags__name__in=["red"])
sorted(map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"]))), model_name = self.food_model.__name__
sorted([pear.pk, guava.pk]), self.assertQuerysetEqual(pks,
) ['<{0}: pear>'.format(model_name),
'<{0}: guava>'.format(model_name)],
ordered=False)
def test_similarity_by_tag(self): def test_similarity_by_tag(self):
"""Test that pears are more similar to apples than watermelons""" """Test that pears are more similar to apples than watermelons"""
...@@ -246,7 +252,8 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -246,7 +252,8 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
similar_objs = apple.tags.similar_objects() similar_objs = apple.tags.similar_objects()
self.assertEqual(similar_objs, [pear, watermelon]) self.assertEqual(similar_objs, [pear, watermelon])
self.assertEqual(map(lambda x: x.similar_tags, similar_objs), [3, 2]) self.assertEqual([obj.similar_tags for obj in similar_objs],
[3, 2])
def test_tag_reuse(self): def test_tag_reuse(self):
apple = self.food_model.objects.create(name="apple") apple = self.food_model.objects.create(name="apple")
...@@ -272,7 +279,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase): ...@@ -272,7 +279,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
ross.tags.add("president") ross.tags.add("president")
self.assertEqual( self.assertEqual(
unicode(self.taggeditem_model.objects.all()[0]), force_text(self.taggeditem_model.objects.all()[0]),
"ross tagged with president" "ross tagged with president"
) )
...@@ -332,10 +339,7 @@ class TaggableManagerOfficialTestCase(TaggableManagerTestCase): ...@@ -332,10 +339,7 @@ class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
pear = self.food_model.objects.create(name="Pear") pear = self.food_model.objects.create(name="Pear")
pear.tags.add("delicious") pear.tags.add("delicious")
self.assertEqual( self.assertEqual(apple, self.food_model.objects.get(tags__official=False))
map(lambda o: o.pk, self.food_model.objects.filter(tags__official=False)),
[apple.pk],
)
class TaggableFormTestCase(BaseTaggingTestCase): class TaggableFormTestCase(BaseTaggingTestCase):
...@@ -343,7 +347,7 @@ class TaggableFormTestCase(BaseTaggingTestCase): ...@@ -343,7 +347,7 @@ class TaggableFormTestCase(BaseTaggingTestCase):
food_model = Food food_model = Food
def test_form(self): def test_form(self):
self.assertEqual(self.form_class.base_fields.keys(), ['name', 'tags']) self.assertEqual(list(self.form_class.base_fields), ['name', 'tags'])
f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'}) f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'})
self.assert_form_renders(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> self.assert_form_renders(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>
...@@ -379,7 +383,7 @@ class TaggableFormTestCase(BaseTaggingTestCase): ...@@ -379,7 +383,7 @@ class TaggableFormTestCase(BaseTaggingTestCase):
tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True) tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
ff = tm.formfield() ff = tm.formfield()
self.assertEqual(ff.label, 'Categories') self.assertEqual(ff.label, 'Categories')
self.assertEqual(ff.help_text, u'Add some categories') self.assertEqual(ff.help_text, 'Add some categories')
self.assertEqual(ff.required, False) self.assertEqual(ff.required, False)
self.assertEqual(ff.clean(""), []) self.assertEqual(ff.clean(""), [])
...@@ -411,54 +415,54 @@ class TagStringParseTestCase(UnitTestCase): ...@@ -411,54 +415,54 @@ class TagStringParseTestCase(UnitTestCase):
""" """
Test with simple space-delimited tags. Test with simple space-delimited tags.
""" """
self.assertEqual(parse_tags('one'), [u'one']) self.assertEqual(parse_tags('one'), ['one'])
self.assertEqual(parse_tags('one two'), [u'one', u'two']) self.assertEqual(parse_tags('one two'), ['one', 'two'])
self.assertEqual(parse_tags('one two three'), [u'one', u'three', u'two']) self.assertEqual(parse_tags('one two three'), ['one', 'three', 'two'])
self.assertEqual(parse_tags('one one two two'), [u'one', u'two']) self.assertEqual(parse_tags('one one two two'), ['one', 'two'])
def test_with_comma_delimited_multiple_words(self): def test_with_comma_delimited_multiple_words(self):
""" """
Test with comma-delimited multiple words. Test with comma-delimited multiple words.
An unquoted comma in the input will trigger this. An unquoted comma in the input will trigger this.
""" """
self.assertEqual(parse_tags(',one'), [u'one']) self.assertEqual(parse_tags(',one'), ['one'])
self.assertEqual(parse_tags(',one two'), [u'one two']) self.assertEqual(parse_tags(',one two'), ['one two'])
self.assertEqual(parse_tags(',one two three'), [u'one two three']) self.assertEqual(parse_tags(',one two three'), ['one two three'])
self.assertEqual(parse_tags('a-one, a-two and a-three'), self.assertEqual(parse_tags('a-one, a-two and a-three'),
[u'a-one', u'a-two and a-three']) ['a-one', 'a-two and a-three'])
def test_with_double_quoted_multiple_words(self): def test_with_double_quoted_multiple_words(self):
""" """
Test with double-quoted multiple words. Test with double-quoted multiple words.
A completed quote will trigger this. Unclosed quotes are ignored. A completed quote will trigger this. Unclosed quotes are ignored.
""" """
self.assertEqual(parse_tags('"one'), [u'one']) self.assertEqual(parse_tags('"one'), ['one'])
self.assertEqual(parse_tags('"one two'), [u'one', u'two']) self.assertEqual(parse_tags('"one two'), ['one', 'two'])
self.assertEqual(parse_tags('"one two three'), [u'one', u'three', u'two']) self.assertEqual(parse_tags('"one two three'), ['one', 'three', 'two'])
self.assertEqual(parse_tags('"one two"'), [u'one two']) self.assertEqual(parse_tags('"one two"'), ['one two'])
self.assertEqual(parse_tags('a-one "a-two and a-three"'), self.assertEqual(parse_tags('a-one "a-two and a-three"'),
[u'a-one', u'a-two and a-three']) ['a-one', 'a-two and a-three'])
def test_with_no_loose_commas(self): def test_with_no_loose_commas(self):
""" """
Test with no loose commas -- split on spaces. Test with no loose commas -- split on spaces.
""" """
self.assertEqual(parse_tags('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) self.assertEqual(parse_tags('one two "thr,ee"'), ['one', 'thr,ee', 'two'])
def test_with_loose_commas(self): def test_with_loose_commas(self):
""" """
Loose commas - split on commas Loose commas - split on commas
""" """
self.assertEqual(parse_tags('"one", two three'), [u'one', u'two three']) self.assertEqual(parse_tags('"one", two three'), ['one', 'two three'])
def test_tags_with_double_quotes_can_contain_commas(self): def test_tags_with_double_quotes_can_contain_commas(self):
""" """
Double quotes can contain commas Double quotes can contain commas
""" """
self.assertEqual(parse_tags('a-one "a-two, and a-three"'), self.assertEqual(parse_tags('a-one "a-two, and a-three"'),
[u'a-one', u'a-two, and a-three']) ['a-one', 'a-two, and a-three'])
self.assertEqual(parse_tags('"two", one, one, two, "one"'), self.assertEqual(parse_tags('"two", one, one, two, "one"'),
[u'one', u'two']) ['one', 'two'])
def test_with_naughty_input(self): def test_with_naughty_input(self):
""" """
...@@ -471,16 +475,16 @@ class TagStringParseTestCase(UnitTestCase): ...@@ -471,16 +475,16 @@ class TagStringParseTestCase(UnitTestCase):
self.assertEqual(parse_tags('""'), []) self.assertEqual(parse_tags('""'), [])
self.assertEqual(parse_tags('"' * 7), []) self.assertEqual(parse_tags('"' * 7), [])
self.assertEqual(parse_tags(',,,,,,'), []) self.assertEqual(parse_tags(',,,,,,'), [])
self.assertEqual(parse_tags('",",",",",",","'), [u',']) self.assertEqual(parse_tags('",",",",",",","'), [','])
self.assertEqual(parse_tags('a-one "a-two" and "a-three'), self.assertEqual(parse_tags('a-one "a-two" and "a-three'),
[u'a-one', u'a-three', u'a-two', u'and']) ['a-one', 'a-three', 'a-two', 'and'])
def test_recreation_of_tag_list_string_representations(self): def test_recreation_of_tag_list_string_representations(self):
plain = Tag.objects.create(name='plain') plain = Tag.objects.create(name='plain')
spaces = Tag.objects.create(name='spa ces') spaces = Tag.objects.create(name='spa ces')
comma = Tag.objects.create(name='com,ma') comma = Tag.objects.create(name='com,ma')
self.assertEqual(edit_string_for_tags([plain]), u'plain') self.assertEqual(edit_string_for_tags([plain]), 'plain')
self.assertEqual(edit_string_for_tags([plain, spaces]), u'"spa ces", plain') self.assertEqual(edit_string_for_tags([plain, spaces]), '"spa ces", plain')
self.assertEqual(edit_string_for_tags([plain, spaces, comma]), u'"com,ma", "spa ces", plain') self.assertEqual(edit_string_for_tags([plain, spaces, comma]), '"com,ma", "spa ces", plain')
self.assertEqual(edit_string_for_tags([plain, comma]), u'"com,ma", plain') self.assertEqual(edit_string_for_tags([plain, comma]), '"com,ma", plain')
self.assertEqual(edit_string_for_tags([comma, spaces]), u'"com,ma", "spa ces"') self.assertEqual(edit_string_for_tags([comma, spaces]), '"com,ma", "spa ces"')
from django.utils.encoding import force_unicode from __future__ import unicode_literals
from django.utils.encoding import force_text
from django.utils.functional import wraps from django.utils.functional import wraps
from django.utils import six
def parse_tags(tagstring): def parse_tags(tagstring):
...@@ -16,13 +19,13 @@ def parse_tags(tagstring): ...@@ -16,13 +19,13 @@ def parse_tags(tagstring):
if not tagstring: if not tagstring:
return [] return []
tagstring = force_unicode(tagstring) tagstring = force_text(tagstring)
# Special case - if there are no commas or double quotes in the # Special case - if there are no commas or double quotes in the
# input, we don't *do* a recall... I mean, we know we only need to # input, we don't *do* a recall... I mean, we know we only need to
# split on spaces. # split on spaces.
if u',' not in tagstring and u'"' not in tagstring: if ',' not in tagstring and '"' not in tagstring:
words = list(set(split_strip(tagstring, u' '))) words = list(set(split_strip(tagstring, ' ')))
words.sort() words.sort()
return words return words
...@@ -36,39 +39,39 @@ def parse_tags(tagstring): ...@@ -36,39 +39,39 @@ def parse_tags(tagstring):
i = iter(tagstring) i = iter(tagstring)
try: try:
while True: while True:
c = i.next() c = six.next(i)
if c == u'"': if c == '"':
if buffer: if buffer:
to_be_split.append(u''.join(buffer)) to_be_split.append(''.join(buffer))
buffer = [] buffer = []
# Find the matching quote # Find the matching quote
open_quote = True open_quote = True
c = i.next() c = six.next(i)
while c != u'"': while c != '"':
buffer.append(c) buffer.append(c)
c = i.next() c = six.next(i)
if buffer: if buffer:
word = u''.join(buffer).strip() word = ''.join(buffer).strip()
if word: if word:
words.append(word) words.append(word)
buffer = [] buffer = []
open_quote = False open_quote = False
else: else:
if not saw_loose_comma and c == u',': if not saw_loose_comma and c == ',':
saw_loose_comma = True saw_loose_comma = True
buffer.append(c) buffer.append(c)
except StopIteration: except StopIteration:
# If we were parsing an open quote which was never closed treat # If we were parsing an open quote which was never closed treat
# the buffer as unquoted. # the buffer as unquoted.
if buffer: if buffer:
if open_quote and u',' in buffer: if open_quote and ',' in buffer:
saw_loose_comma = True saw_loose_comma = True
to_be_split.append(u''.join(buffer)) to_be_split.append(''.join(buffer))
if to_be_split: if to_be_split:
if saw_loose_comma: if saw_loose_comma:
delimiter = u',' delimiter = ','
else: else:
delimiter = u' ' delimiter = ' '
for chunk in to_be_split: for chunk in to_be_split:
words.extend(split_strip(chunk, delimiter)) words.extend(split_strip(chunk, delimiter))
words = list(set(words)) words = list(set(words))
...@@ -76,7 +79,7 @@ def parse_tags(tagstring): ...@@ -76,7 +79,7 @@ def parse_tags(tagstring):
return words return words
def split_strip(string, delimiter=u','): def split_strip(string, delimiter=','):
""" """
Splits ``string`` on ``delimiter``, stripping each resulting string Splits ``string`` on ``delimiter``, stripping each resulting string
and returning a list of non-empty strings. and returning a list of non-empty strings.
...@@ -110,11 +113,11 @@ def edit_string_for_tags(tags): ...@@ -110,11 +113,11 @@ def edit_string_for_tags(tags):
names = [] names = []
for tag in tags: for tag in tags:
name = tag.name name = tag.name
if u',' in name or u' ' in name: if ',' in name or ' ' in name:
names.append('"%s"' % name) names.append('"%s"' % name)
else: else:
names.append(name) names.append(name)
return u', '.join(sorted(names)) return ', '.join(sorted(names))
def require_instance_manager(func): def require_instance_manager(func):
......
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.generic.list import ListView from django.views.generic.list import ListView
......
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