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
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
Changelog
=========
0.11.0 (unreleased)
~~~~~~~~~~~~~~~~~~~
* Python3 support
* *Backwards incompatible* Dropped support for Django < 1.4.5.
0.10.0
~~~~~~
......
......@@ -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.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::
:maxdepth: 2
......
......@@ -27,9 +27,14 @@ setup(
'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='taggit.tests.runtests.runtests',
test_suite='runtests.runtests',
include_package_data=True,
zip_safe=False,
)
from __future__ import unicode_literals
from django.contrib import admin
from taggit.models import Tag, TaggedItem
......
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)
......
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.fields.related import ManyToManyRel, RelatedField, add_laz
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 @@ class TaggableManager(RelatedField):
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,14 @@ class TaggableManager(RelatedField):
else:
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):
self.use_gfk = (
self.through is None or issubclass(self.through, GenericTaggedItemBase)
......@@ -132,7 +128,8 @@ class TaggableManager(RelatedField):
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 +194,7 @@ class _TaggableManager(models.Manager):
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 +217,7 @@ class _TaggableManager(models.Manager):
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 +240,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
# -*- 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']
# -*- 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']
from __future__ import unicode_literals
import django
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey
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)
def __unicode__(self):
def __str__(self):
return self.name
class Meta:
......@@ -57,9 +61,9 @@ class Tag(TagBase):
verbose_name_plural = _("Tags")
@python_2_unicode_compatible
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 +94,7 @@ class ItemBase(models.Model):
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
......
from __future__ import unicode_literals
from django import forms
from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood
......
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 @@ class ArticleTag(Tag):
slug += "-%d" % i
return slug
class ArticleTaggedItem(TaggedItem):
class Meta:
proxy = True
......@@ -137,7 +157,8 @@ class ArticleTaggedItem(TaggedItem):
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
from __future__ import unicode_literals
from unittest import TestCase as UnitTestCase
import django
......@@ -5,6 +7,8 @@ from django.conf import settings
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 @@ from taggit.utils import parse_tags, edit_string_for_tags
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,15 +56,13 @@ class BaseTaggingTest(object):
return form_str
def assert_form_renders(self, form, html):
try:
self.assertHTMLEqual(str(form), self._get_form_str(html))
except AttributeError:
self.assertEqual(str(form), self._get_form_str(html))
self.assertHTMLEqual(str(form), self._get_form_str(html))
class BaseTaggingTestCase(TestCase, BaseTaggingTest):
pass
class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
pass
......@@ -214,10 +216,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
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")
......@@ -228,10 +232,12 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
guava = self.food_model.objects.create(name="guava")
self.assertEqual(
sorted(map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"]))),
sorted([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"""
......@@ -246,7 +252,8 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
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")
......@@ -272,7 +279,7 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
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"
)
......@@ -332,10 +339,7 @@ class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
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):
......@@ -343,7 +347,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, """<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):
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(""), [])
......@@ -411,54 +415,54 @@ class TagStringParseTestCase(UnitTestCase):
"""
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):
"""
......@@ -471,16 +475,16 @@ class TagStringParseTestCase(UnitTestCase):
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"')
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):
......
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 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