From 485b508375b2713150d56a080a584790678dc193 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 12:22:47 +0100 Subject: [PATCH 01/10] Fixed manifest, README was missing --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index c6b60943..4399ef03 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ +include README.rst AUTHORS CHANGES.rst LICENSE recursive-include docs *.txt recursive-include taggit/locale * From 20a1b6bc206be63952b45c725201c31d6d0e9a6a Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 18:45:50 +0100 Subject: [PATCH 02/10] Moved runtests.py to root. --- taggit/tests/runtests.py => runtests.py | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename taggit/tests/runtests.py => runtests.py (100%) diff --git a/taggit/tests/runtests.py b/runtests.py similarity index 100% rename from taggit/tests/runtests.py rename to runtests.py diff --git a/setup.py b/setup.py index 1a2cb20d..f9fd0975 100644 --- a/setup.py +++ b/setup.py @@ -31,5 +31,5 @@ 'Programming Language :: Python', 'Framework :: Django', ], - test_suite='taggit.tests.runtests.runtests', + test_suite='runtests.runtests', ) From fb2d1cc45f1760c5317a62719f811b7be9a0a051 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 18:46:37 +0100 Subject: [PATCH 03/10] We now run on py26, py27, pypy, py32, py33 using django14 and django15. --- taggit/admin.py | 2 + taggit/forms.py | 5 +- taggit/managers.py | 38 +++++----- taggit/migrations/0001_initial.py | 40 +++++----- taggit/migrations/0002_unique_tagnames.py | 24 +++--- taggit/models.py | 12 ++- taggit/tests/forms.py | 2 + taggit/tests/models.py | 37 ++++++++-- taggit/tests/tests.py | 90 ++++++++++++----------- taggit/utils.py | 41 ++++++----- taggit/views.py | 2 + 11 files changed, 167 insertions(+), 126 deletions(-) diff --git a/taggit/admin.py b/taggit/admin.py index 6c012d6f..0498c9dd 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import admin from taggit.models import Tag, TaggedItem diff --git a/taggit/forms.py b/taggit/forms.py index e0198bd9..cb372f22 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -1,12 +1,15 @@ +from __future__ import unicode_literals + from django import forms from django.utils.translation import ugettext as _ +from django.utils import six from taggit.utils import parse_tags, edit_string_for_tags class TagWidget(forms.TextInput): 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")]) return super(TagWidget, self).render(name, value, attrs) diff --git a/taggit/managers.py b/taggit/managers.py index ca1700fd..cc3dc746 100644 --- a/taggit/managers.py +++ b/taggit/managers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.contenttypes.generic import GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models @@ -5,27 +7,13 @@ from django.db.models.related import RelatedObject 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 TaggedItem, GenericTaggedItemBase 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): def __init__(self): self.related_name = None @@ -68,7 +56,7 @@ def contribute_to_class(self, cls, name): cls._meta.add_field(self) setattr(cls, name, self) if not cls._meta.abstract: - if isinstance(self.through, basestring): + if isinstance(self.through, six.string_types): def resolve_related_class(field, model, cls): self.through = model self.post_through_setup(cls) @@ -78,6 +66,9 @@ def resolve_related_class(field, model, cls): else: self.post_through_setup(cls) + def __lt__(self, other): + return False + def post_through_setup(self, cls): self.use_gfk = ( self.through is None or issubclass(self.through, GenericTaggedItemBase) @@ -132,7 +123,8 @@ def extra_filters(self, pieces, pos, negate): if negate or not self.use_gfk: return [] 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: return [("%s__content_type" % prefix, cts[0])] return [("%s__content_type__in" % prefix, cts)] @@ -197,7 +189,7 @@ def most_common(self): def similar_objects(self): lookup_kwargs = self._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.exclude(**lookup_kwargs) qs = qs.filter(tag__in=self.all()) @@ -220,7 +212,7 @@ def similar_objects(self): preload.setdefault(result['content_type'], set()) 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) for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids): items[(ct.pk, obj.pk)] = obj @@ -243,3 +235,11 @@ def _get_subclasses(model): getattr(field.field.rel, "parent_link", None)): subclasses.extend(_get_subclasses(field.model)) 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 diff --git a/taggit/migrations/0001_initial.py b/taggit/migrations/0001_initial.py index 666ca619..f4009846 100644 --- a/taggit/migrations/0001_initial.py +++ b/taggit/migrations/0001_initial.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + import datetime from south.db import db from south.v2 import SchemaMigration @@ -9,52 +11,52 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'Tag' - db.create_table(u'taggit_tag', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + db.create_table('taggit_tag', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(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' - db.create_table(u'taggit_taggeditem', ( - (u'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'])), + db.create_table('taggit_taggeditem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('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)), - ('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): # Deleting model 'Tag' - db.delete_table(u'taggit_tag') + db.delete_table('taggit_tag') # Deleting model 'TaggedItem' - db.delete_table(u'taggit_taggeditem') + db.delete_table('taggit_taggeditem') models = { - u'contenttypes.contenttype': { + 'contenttypes.contenttype': { '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'}), - 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'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, - u'taggit.tag': { + 'taggit.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'}), 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) }, - u'taggit.taggeditem': { + 'taggit.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']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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']"}) } } - complete_apps = ['taggit'] \ No newline at end of file + complete_apps = ['taggit'] diff --git a/taggit/migrations/0002_unique_tagnames.py b/taggit/migrations/0002_unique_tagnames.py index e5eb033b..f18510ba 100644 --- a/taggit/migrations/0002_unique_tagnames.py +++ b/taggit/migrations/0002_unique_tagnames.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + import datetime from south.db import db from south.v2 import SchemaMigration @@ -9,35 +11,35 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding unique constraint on 'Tag', fields ['name'] - db.create_unique(u'taggit_tag', ['name']) + db.create_unique('taggit_tag', ['name']) def backwards(self, orm): # Removing unique constraint on 'Tag', fields ['name'] - db.delete_unique(u'taggit_tag', ['name']) + db.delete_unique('taggit_tag', ['name']) models = { - u'contenttypes.contenttype': { + 'contenttypes.contenttype': { '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'}), - 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'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, - u'taggit.tag': { + 'taggit.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'}), 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) }, - u'taggit.taggeditem': { + 'taggit.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']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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']"}) } } - complete_apps = ['taggit'] \ No newline at end of file + complete_apps = ['taggit'] diff --git a/taggit/models.py b/taggit/models.py index 581a5b19..61d6a751 100644 --- a/taggit/models.py +++ b/taggit/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.generic import GenericForeignKey @@ -10,7 +12,7 @@ class TagBase(models.Model): name = models.CharField(verbose_name=_('Name'), 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 class Meta: @@ -57,9 +59,8 @@ class Meta: verbose_name_plural = _("Tags") - class ItemBase(models.Model): - def __unicode__(self): + def __str__(self): return ugettext("%(object)s tagged with %(tag)s") % { "object": self.content_object, "tag": self.tag @@ -90,10 +91,7 @@ def bulk_lookup_kwargs(cls, instances): 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: abstract = True diff --git a/taggit/tests/forms.py b/taggit/tests/forms.py index 2cdc6a8d..cb4020fc 100644 --- a/taggit/tests/forms.py +++ b/taggit/tests/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood diff --git a/taggit/tests/models.py b/taggit/tests/models.py index a0e21e04..15e97f2d 100644 --- a/taggit/tests/models.py +++ b/taggit/tests/models.py @@ -1,26 +1,32 @@ +from __future__ import unicode_literals + 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) +@python_2_unicode_compatible class Food(models.Model): name = models.CharField(max_length=50) tags = TaggableManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Pet(models.Model): name = models.CharField(max_length=50) tags = TaggableManager() - def __unicode__(self): + def __str__(self): return self.name + class HousePet(Pet): trained = models.BooleanField() @@ -30,22 +36,31 @@ class HousePet(Pet): class TaggedFood(TaggedItemBase): content_object = models.ForeignKey('DirectFood') + class TaggedPet(TaggedItemBase): content_object = models.ForeignKey('DirectPet') + +@python_2_unicode_compatible class DirectFood(models.Model): name = models.CharField(max_length=50) tags = TaggableManager(through="TaggedFood") + def __str__(self): + return self.name + + +@python_2_unicode_compatible class DirectPet(models.Model): name = models.CharField(max_length=50) tags = TaggableManager(through=TaggedPet) - def __unicode__(self): + def __str__(self): return self.name + class DirectHousePet(DirectPet): trained = models.BooleanField() @@ -58,20 +73,22 @@ class TaggedCustomPKFood(TaggedItemBase): class TaggedCustomPKPet(TaggedItemBase): content_object = models.ForeignKey('CustomPKPet') +@python_2_unicode_compatible class CustomPKFood(models.Model): name = models.CharField(max_length=50, primary_key=True) tags = TaggableManager(through=TaggedCustomPKFood) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class CustomPKPet(models.Model): name = models.CharField(max_length=50, primary_key=True) tags = TaggableManager(through=TaggedCustomPKPet) - def __unicode__(self): + def __str__(self): return self.name class CustomPKHousePet(CustomPKPet): @@ -85,20 +102,22 @@ class OfficialTag(TagBase): class OfficialThroughModel(GenericTaggedItemBase): tag = models.ForeignKey(OfficialTag, related_name="tagged_items") +@python_2_unicode_compatible class OfficialFood(models.Model): name = models.CharField(max_length=50) tags = TaggableManager(through=OfficialThroughModel) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class OfficialPet(models.Model): name = models.CharField(max_length=50) tags = TaggableManager(through=OfficialThroughModel) - def __unicode__(self): + def __str__(self): return self.name class OfficialHousePet(OfficialPet): @@ -129,6 +148,7 @@ def slugify(self, tag, i=None): slug += "-%d" % i return slug + class ArticleTaggedItem(TaggedItem): class Meta: proxy = True @@ -137,7 +157,8 @@ class Meta: def tag_model(self): return ArticleTag + class Article(models.Model): title = models.CharField(max_length=100) - tags = TaggableManager(through=ArticleTaggedItem) + tags = TaggableManager(through=ArticleTaggedItem) \ No newline at end of file diff --git a/taggit/tests/tests.py b/taggit/tests/tests.py index 4dbf44dc..4b8d7a42 100644 --- a/taggit/tests/tests.py +++ b/taggit/tests/tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from unittest import TestCase as UnitTestCase import django @@ -5,6 +7,8 @@ from django.core.exceptions import ValidationError from django.db import connection 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.models import Tag, TaggedItem @@ -19,7 +23,7 @@ class BaseTaggingTest(object): 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: got.sort() tags.sort() @@ -52,7 +56,7 @@ def _get_form_str(self, form_str): return form_str def assert_form_renders(self, form, html): - self.assertEqual(str(form), self._get_form_str(html)) + self.assertHTMLEqual(str(form), self._get_form_str(html)) class BaseTaggingTestCase(TestCase, BaseTaggingTest): pass @@ -210,10 +214,12 @@ def test_lookup_by_tag(self): cat = self.housepet_model.objects.create(name="cat", trained=True) cat.tags.add("fuzzy") - self.assertEqual( - map(lambda o: o.pk, self.pet_model.objects.filter(tags__name__in=["fuzzy"])), - [kitty.pk, cat.pk] - ) + pks = self.pet_model.objects.filter(tags__name__in=["fuzzy"]) + model_name = self.pet_model.__name__ + self.assertQuerysetEqual(pks, + ['<{0}: kitty>'.format(model_name), + '<{0}: cat>'.format(model_name)], + ordered=False) def test_exclude(self): apple = self.food_model.objects.create(name="apple") @@ -224,10 +230,12 @@ def test_exclude(self): guava = self.food_model.objects.create(name="guava") - self.assertEqual( - map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"])), - [pear.pk, guava.pk], - ) + pks = self.food_model.objects.exclude(tags__name__in=["red"]) + model_name = self.food_model.__name__ + self.assertQuerysetEqual(pks, + ['<{0}: pear>'.format(model_name), + '<{0}: guava>'.format(model_name)], + ordered=False) def test_similarity_by_tag(self): """Test that pears are more similar to apples than watermelons""" @@ -242,7 +250,8 @@ def test_similarity_by_tag(self): similar_objs = apple.tags.similar_objects() 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): apple = self.food_model.objects.create(name="apple") @@ -268,7 +277,7 @@ def test_taggeditem_unicode(self): ross.tags.add("president") self.assertEqual( - unicode(self.taggeditem_model.objects.all()[0]), + force_text(self.taggeditem_model.objects.all()[0]), "ross tagged with president" ) @@ -328,10 +337,7 @@ def test_extra_fields(self): 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], - ) + self.assertEqual(apple, self.food_model.objects.get(tags__official=False)) class TaggableFormTestCase(BaseTaggingTestCase): @@ -339,7 +345,7 @@ class TaggableFormTestCase(BaseTaggingTestCase): food_model = Food 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'}) self.assert_form_renders(f, """ @@ -375,7 +381,7 @@ def test_formfield(self): tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True) ff = tm.formfield() 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.clean(""), []) @@ -407,54 +413,54 @@ def test_with_simple_space_delimited_tags(self): """ Test with simple space-delimited tags. """ - self.assertEqual(parse_tags('one'), [u'one']) - self.assertEqual(parse_tags('one two'), [u'one', u'two']) - self.assertEqual(parse_tags('one two three'), [u'one', u'three', u'two']) - self.assertEqual(parse_tags('one one two two'), [u'one', u'two']) + self.assertEqual(parse_tags('one'), ['one']) + self.assertEqual(parse_tags('one two'), ['one', 'two']) + self.assertEqual(parse_tags('one two three'), ['one', 'three', 'two']) + self.assertEqual(parse_tags('one one two two'), ['one', 'two']) def test_with_comma_delimited_multiple_words(self): """ Test with comma-delimited multiple words. An unquoted comma in the input will trigger this. """ - self.assertEqual(parse_tags(',one'), [u'one']) - self.assertEqual(parse_tags(',one two'), [u'one two']) - self.assertEqual(parse_tags(',one two three'), [u'one two three']) + self.assertEqual(parse_tags(',one'), ['one']) + self.assertEqual(parse_tags(',one two'), ['one two']) + self.assertEqual(parse_tags(',one two three'), ['one two 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): """ Test with double-quoted multiple words. A completed quote will trigger this. Unclosed quotes are ignored. """ - self.assertEqual(parse_tags('"one'), [u'one']) - self.assertEqual(parse_tags('"one two'), [u'one', u'two']) - self.assertEqual(parse_tags('"one two three'), [u'one', u'three', u'two']) - self.assertEqual(parse_tags('"one two"'), [u'one two']) + self.assertEqual(parse_tags('"one'), ['one']) + self.assertEqual(parse_tags('"one two'), ['one', 'two']) + self.assertEqual(parse_tags('"one two three'), ['one', 'three', 'two']) + self.assertEqual(parse_tags('"one two"'), ['one two']) 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): """ 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): """ 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): """ Double quotes can contain commas """ 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"'), - [u'one', u'two']) + ['one', 'two']) def test_with_naughty_input(self): """ @@ -467,16 +473,16 @@ def test_with_naughty_input(self): self.assertEqual(parse_tags('""'), []) self.assertEqual(parse_tags('"' * 7), []) self.assertEqual(parse_tags(',,,,,,'), []) - self.assertEqual(parse_tags('",",",",",",","'), [u',']) + self.assertEqual(parse_tags('",",",",",",","'), [',']) 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): plain = Tag.objects.create(name='plain') spaces = Tag.objects.create(name='spa ces') comma = Tag.objects.create(name='com,ma') - self.assertEqual(edit_string_for_tags([plain]), u'plain') - self.assertEqual(edit_string_for_tags([plain, spaces]), u'"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, comma]), u'"com,ma", plain') - self.assertEqual(edit_string_for_tags([comma, spaces]), u'"com,ma", "spa ces"') + self.assertEqual(edit_string_for_tags([plain]), 'plain') + self.assertEqual(edit_string_for_tags([plain, spaces]), '"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]), '"com,ma", plain') + self.assertEqual(edit_string_for_tags([comma, spaces]), '"com,ma", "spa ces"') diff --git a/taggit/utils.py b/taggit/utils.py index 1b5e5a7f..997c4f0a 100644 --- a/taggit/utils.py +++ b/taggit/utils.py @@ -1,5 +1,8 @@ -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 import six def parse_tags(tagstring): @@ -16,13 +19,13 @@ def parse_tags(tagstring): if not tagstring: return [] - tagstring = force_unicode(tagstring) + tagstring = force_text(tagstring) # 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 # split on spaces. - if u',' not in tagstring and u'"' not in tagstring: - words = list(set(split_strip(tagstring, u' '))) + if ',' not in tagstring and '"' not in tagstring: + words = list(set(split_strip(tagstring, ' '))) words.sort() return words @@ -36,39 +39,39 @@ def parse_tags(tagstring): i = iter(tagstring) try: while True: - c = i.next() - if c == u'"': + c = six.next(i) + if c == '"': if buffer: - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) buffer = [] # Find the matching quote open_quote = True - c = i.next() - while c != u'"': + c = six.next(i) + while c != '"': buffer.append(c) - c = i.next() + c = six.next(i) if buffer: - word = u''.join(buffer).strip() + word = ''.join(buffer).strip() if word: words.append(word) buffer = [] open_quote = False else: - if not saw_loose_comma and c == u',': + if not saw_loose_comma and c == ',': saw_loose_comma = True buffer.append(c) except StopIteration: # If we were parsing an open quote which was never closed treat # the buffer as unquoted. if buffer: - if open_quote and u',' in buffer: + if open_quote and ',' in buffer: saw_loose_comma = True - to_be_split.append(u''.join(buffer)) + to_be_split.append(''.join(buffer)) if to_be_split: if saw_loose_comma: - delimiter = u',' + delimiter = ',' else: - delimiter = u' ' + delimiter = ' ' for chunk in to_be_split: words.extend(split_strip(chunk, delimiter)) words = list(set(words)) @@ -76,7 +79,7 @@ def parse_tags(tagstring): return words -def split_strip(string, delimiter=u','): +def split_strip(string, delimiter=','): """ Splits ``string`` on ``delimiter``, stripping each resulting string and returning a list of non-empty strings. @@ -110,11 +113,11 @@ def edit_string_for_tags(tags): names = [] for tag in tags: name = tag.name - if u',' in name or u' ' in name: + if ',' in name or ' ' in name: names.append('"%s"' % name) else: names.append(name) - return u', '.join(sorted(names)) + return ', '.join(sorted(names)) def require_instance_manager(func): diff --git a/taggit/views.py b/taggit/views.py index 68e955b8..f27f6bb8 100644 --- a/taggit/views.py +++ b/taggit/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 from django.views.generic.list_detail import object_list From 828cbd26b0ce82d5741546cf60b26903080b94de Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 18:54:17 +0100 Subject: [PATCH 04/10] Doc for __lt__ --- taggit/managers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/taggit/managers.py b/taggit/managers.py index cc3dc746..2bbf3961 100644 --- a/taggit/managers.py +++ b/taggit/managers.py @@ -67,6 +67,11 @@ def resolve_related_class(field, model, 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): From e29fc41fd1fbd62638c2fbd51f8d40a8352bb5ca Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 18:57:10 +0100 Subject: [PATCH 05/10] Update docs for recent requirements --- docs/changelog.txt | 6 ++++++ docs/index.txt | 4 +--- docs/issues.txt | 9 --------- 3 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 docs/issues.txt diff --git a/docs/changelog.txt b/docs/changelog.txt index 54054481..ba262568 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,6 +1,12 @@ Changelog ========= +unreleased +~~~~~~~~~~ + + * Python3 support + * Works only with Django 1.4 and Django 1.5 + 0.9.2 ~~~~~ diff --git a/docs/index.txt b/docs/index.txt index dbc37b8b..36f8b69b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -4,8 +4,7 @@ Welcome to django-taggit's documentation! ``django-taggit`` is a reusable Django application designed to making adding tagging to your project easy and fun. -``django-taggit`` works with Django 1.1 and 1.2 (see :doc:`issues` for known -issues with older versions of Django), and Python 2.4-2.X. +``django-taggit`` works with Django 1.4+ and Python 2.7-3.X. .. toctree:: :maxdepth: 2 @@ -15,7 +14,6 @@ issues with older versions of Django), and Python 2.4-2.X. admin api custom_tagging - issues external_apps changelog diff --git a/docs/issues.txt b/docs/issues.txt deleted file mode 100644 index 0cba64a0..00000000 --- a/docs/issues.txt +++ /dev/null @@ -1,9 +0,0 @@ -Known Issues -============ - -Currently there is 1 known issue: - - * When run under Django 1.1, doing ``Model.objects.all().delete()`` (or any - bulk deletion operation) on a model with a ``TaggableManager`` will result - in losing the tags for items beyond just those assosciated with the deleted - objects. This issue is not present in Django 1.2. From 102c62ea5b67a47f1281c2b3d7fa61bc1386a7af Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 19:08:36 +0100 Subject: [PATCH 06/10] Add requirements files --- requirements/docs.txt | 1 + requirements/test.txt | 2 ++ requirements/travis-ci.txt | 1 + 3 files changed, 4 insertions(+) create mode 100644 requirements/docs.txt create mode 100644 requirements/test.txt create mode 100644 requirements/travis-ci.txt diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 00000000..2806c164 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1 @@ +Sphinx diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..75e67d66 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,2 @@ +Django +coverage diff --git a/requirements/travis-ci.txt b/requirements/travis-ci.txt new file mode 100644 index 00000000..4ebc8aea --- /dev/null +++ b/requirements/travis-ci.txt @@ -0,0 +1 @@ +coverage From 06f22a375e4cc518e15e8cdc04837d7b8c4e9711 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 19:09:55 +0100 Subject: [PATCH 07/10] Update travis config to test py3k and django master --- .travis.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6cb13337..ffa612cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,28 @@ language: python python: - "2.6" - "2.7" + - "3.2" + - "3.3" env: + - DJANGO=https://github.com/django/django/archive/master.tar.gz - DJANGO=django==1.5 --use-mirrors - DJANGO=django==1.4.5 --use-mirrors - - DJANGO=django==1.3.7 --use-mirrors install: - pip install $DJANGO + - pip install -r requirements/travis-ci.txt --use-mirrors script: - - python setup.py test + - coverage run --source django_taggit runtests.py + - coverage report notifications: 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 From a0b5da6e5f906c7bdcb125e372f77003b5b67701 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 19:19:52 +0100 Subject: [PATCH 08/10] Fixed Django version requirement --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 36f8b69b..059df549 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -4,7 +4,7 @@ Welcome to django-taggit's documentation! ``django-taggit`` is a reusable Django application designed to making adding tagging to your project easy and fun. -``django-taggit`` works with Django 1.4+ and Python 2.7-3.X. +``django-taggit`` works with Django 1.4.5+ and Python 2.7-3.X. .. toctree:: :maxdepth: 2 From a7bf54f417576bfc355e1851258e711dadd73ad3 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 19:22:14 +0100 Subject: [PATCH 09/10] Add python trove classifiers --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index f9fd0975..4390f6ef 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,11 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', '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', ], test_suite='runtests.runtests', From d9fb9b16cb3cb8d6ff96bbfc7d9fa7b3092a98c2 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sat, 23 Mar 2013 19:23:40 +0100 Subject: [PATCH 10/10] Clarify TagBase and ItemBase as python2 unicode compatible --- taggit/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taggit/models.py b/taggit/models.py index 61d6a751..f45dcf45 100644 --- a/taggit/models.py +++ b/taggit/models.py @@ -6,8 +6,10 @@ from django.db import models, IntegrityError, transaction 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 +@python_2_unicode_compatible class TagBase(models.Model): name = models.CharField(verbose_name=_('Name'), unique=True, max_length=100) slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100) @@ -59,6 +61,7 @@ class Meta: verbose_name_plural = _("Tags") +@python_2_unicode_compatible class ItemBase(models.Model): def __str__(self): return ugettext("%(object)s tagged with %(tag)s") % {