[Python-modules-commits] [django-taggit] 01/05: Imported Upstream version 0.19.1
Michal Cihar
nijel at moszumanska.debian.org
Thu May 26 07:41:46 UTC 2016
This is an automated email from the git hooks/post-receive script.
nijel pushed a commit to branch master
in repository django-taggit.
commit 67224e193ee430e6b3e097c43bf8006f6832a947
Author: Michal Čihař <nijel at debian.org>
Date: Thu May 26 09:14:51 2016 +0200
Imported Upstream version 0.19.1
---
AUTHORS | 1 +
CHANGELOG.txt | 12 ++
PKG-INFO | 6 +-
README.rst | 4 +
django_taggit.egg-info/PKG-INFO | 6 +-
django_taggit.egg-info/SOURCES.txt | 4 +
docs/api.txt | 7 +-
docs/conf.py | 3 +-
runtests.py | 1 -
setup.cfg | 1 +
setup.py | 5 +-
taggit/__init__.py | 4 +-
taggit/admin.py | 5 +-
taggit/apps.py | 7 +
taggit/locale/zh_Hans/LC_MESSAGES/django.mo | Bin 0 -> 856 bytes
taggit/locale/zh_Hans/LC_MESSAGES/django.po | 68 ++++++++
taggit/managers.py | 147 ++++++++++++++---
taggit/models.py | 1 +
tests/custom_parser.py | 1 +
tests/forms.py | 6 +-
tests/models.py | 19 ++-
tests/settings.py | 16 ++
tests/tests.py | 243 +++++++++++++++++++++++++---
tox.ini | 135 ++--------------
24 files changed, 528 insertions(+), 174 deletions(-)
diff --git a/AUTHORS b/AUTHORS
index 2950197..b66b86b 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,3 +13,4 @@ Jonathan Buchanan
idle sign <idlesign at yandex.ru>
Charles Leifer
Florian Apolloner <apollo13 at apolloner.eu>
+Andrew Pryde <andrew at rocketpod.co.uk>
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index a3ab339..09faf62 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,18 @@
Changelog
=========
+0.19.1 (2016-05-25)
+~~~~~~~~~~~~~~~~~~~
+ * Add app config, add simplified Chinese translation file
+ * https://github.com/alex/django-taggit/pull/410
+
+0.19.0 (2016-05-23)
+~~~~~~~~~~~~~~~~~~~
+ * Implementation of m2m_changed signal sending
+ * https://github.com/alex/django-taggit/pull/409
+ * Code and tooling improvements
+ * https://github.com/alex/django-taggit/pull/408
+
0.18.3 (2016-05-12)
~~~~~~~~~~~~~~~~~~~
* Added Spanish and Turkish translations
diff --git a/PKG-INFO b/PKG-INFO
index 60f2866..3c98279 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: django-taggit
-Version: 0.18.3
+Version: 0.19.1
Summary: django-taggit is a reusable Django application for simple tagging.
Home-page: http://github.com/alex/django-taggit/tree/master
Author: Alex Gaynor
@@ -8,6 +8,10 @@ Author-email: alex.gaynor at gmail.com
License: BSD
Description: django-taggit
=============
+ .. image:: https://travis-ci.org/alex/django-taggit.svg?branch=master
+ :target: https://travis-ci.org/alex/django-taggit
+ .. image:: https://codecov.io/gh/alex/django-taggit/coverage.svg?branch=master
+ :target: https://codecov.io/gh/alex/django-taggit?branch=master
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
diff --git a/README.rst b/README.rst
index c70f655..32e5c95 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,9 @@
django-taggit
=============
+.. image:: https://travis-ci.org/alex/django-taggit.svg?branch=master
+ :target: https://travis-ci.org/alex/django-taggit
+.. image:: https://codecov.io/gh/alex/django-taggit/coverage.svg?branch=master
+ :target: https://codecov.io/gh/alex/django-taggit?branch=master
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
diff --git a/django_taggit.egg-info/PKG-INFO b/django_taggit.egg-info/PKG-INFO
index 60f2866..3c98279 100644
--- a/django_taggit.egg-info/PKG-INFO
+++ b/django_taggit.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: django-taggit
-Version: 0.18.3
+Version: 0.19.1
Summary: django-taggit is a reusable Django application for simple tagging.
Home-page: http://github.com/alex/django-taggit/tree/master
Author: Alex Gaynor
@@ -8,6 +8,10 @@ Author-email: alex.gaynor at gmail.com
License: BSD
Description: django-taggit
=============
+ .. image:: https://travis-ci.org/alex/django-taggit.svg?branch=master
+ :target: https://travis-ci.org/alex/django-taggit
+ .. image:: https://codecov.io/gh/alex/django-taggit/coverage.svg?branch=master
+ :target: https://codecov.io/gh/alex/django-taggit?branch=master
``django-taggit`` a simpler approach to tagging with Django. Add ``"taggit"`` to your
``INSTALLED_APPS`` then just add a TaggableManager to your model and go:
diff --git a/django_taggit.egg-info/SOURCES.txt b/django_taggit.egg-info/SOURCES.txt
index 309c69e..263689b 100644
--- a/django_taggit.egg-info/SOURCES.txt
+++ b/django_taggit.egg-info/SOURCES.txt
@@ -24,6 +24,7 @@ docs/getting_started.txt
docs/index.txt
taggit/__init__.py
taggit/admin.py
+taggit/apps.py
taggit/forms.py
taggit/managers.py
taggit/models.py
@@ -54,6 +55,8 @@ taggit/locale/ru/LC_MESSAGES/django.mo
taggit/locale/ru/LC_MESSAGES/django.po
taggit/locale/tr/LC_MESSAGES/django.mo
taggit/locale/tr/LC_MESSAGES/django.po
+taggit/locale/zh_Hans/LC_MESSAGES/django.mo
+taggit/locale/zh_Hans/LC_MESSAGES/django.po
taggit/migrations/0001_initial.py
taggit/migrations/0002_auto_20150616_2121.py
taggit/migrations/__init__.py
@@ -64,6 +67,7 @@ tests/__init__.py
tests/custom_parser.py
tests/forms.py
tests/models.py
+tests/settings.py
tests/tests.py
tests/migrations/0001_initial.py
tests/migrations/__init__.py
\ No newline at end of file
diff --git a/docs/api.txt b/docs/api.txt
index 26dd453..a69948f 100644
--- a/docs/api.txt
+++ b/docs/api.txt
@@ -30,10 +30,11 @@ playing around with the API.
Removes all tags from an object.
- .. method:: set(*tags)
+ .. method:: set(*tags, clear=False)
- Removes all the current tags and then adds the specified tags to the
- object.
+ If ``clear = True`` removes all the current tags and then adds the
+ specified tags to the object. Otherwise sets the object's tags to those
+ specified, removing only the missing tags and adding only the new tags.
.. method: most_common()
diff --git a/docs/conf.py b/docs/conf.py
index 53763ce..e83b040 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -11,7 +11,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import os
+import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
diff --git a/runtests.py b/runtests.py
index dedd55a..36a7d4e 100755
--- a/runtests.py
+++ b/runtests.py
@@ -5,7 +5,6 @@ import warnings
from django.conf import settings
from django.core.management import execute_from_command_line
-
if not settings.configured:
settings.configure(
DATABASES={
diff --git a/setup.cfg b/setup.cfg
index dd960e1..8f3c6ea 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,6 +10,7 @@ exclude = south_migrations,migrations
[isort]
forced_separate = tests,taggit
+skip = migrations,.tox,south_migrations,docs
[egg_info]
tag_build =
diff --git a/setup.py b/setup.py
index da115ff..bfd7452 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-from setuptools import setup, find_packages
+from setuptools import find_packages, setup
import taggit
@@ -37,4 +37,7 @@ setup(
],
include_package_data=True,
zip_safe=False,
+ setup_requires=[
+ 'isort'
+ ],
)
diff --git a/taggit/__init__.py b/taggit/__init__.py
index 109f6b2..f60adf0 100644
--- a/taggit/__init__.py
+++ b/taggit/__init__.py
@@ -1 +1,3 @@
-VERSION = (0, 18, 3)
+VERSION = (0, 19, 1)
+
+default_app_config = 'taggit.apps.TaggitAppConfig'
diff --git a/taggit/admin.py b/taggit/admin.py
index 0498c9d..df40e38 100644
--- a/taggit/admin.py
+++ b/taggit/admin.py
@@ -8,10 +8,9 @@ from taggit.models import Tag, TaggedItem
class TaggedItemInline(admin.StackedInline):
model = TaggedItem
+
class TagAdmin(admin.ModelAdmin):
- inlines = [
- TaggedItemInline
- ]
+ inlines = [TaggedItemInline]
list_display = ["name", "slug"]
ordering = ["name", "slug"]
search_fields = ["name"]
diff --git a/taggit/apps.py b/taggit/apps.py
new file mode 100644
index 0000000..b73a79d
--- /dev/null
+++ b/taggit/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig as BaseConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class TaggitAppConfig(BaseConfig):
+ name = 'taggit'
+ verbose_name = _('Taggit')
diff --git a/taggit/locale/zh_Hans/LC_MESSAGES/django.mo b/taggit/locale/zh_Hans/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..b67ac4c
Binary files /dev/null and b/taggit/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/taggit/locale/zh_Hans/LC_MESSAGES/django.po b/taggit/locale/zh_Hans/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e7bdfcb
--- /dev/null
+++ b/taggit/locale/zh_Hans/LC_MESSAGES/django.po
@@ -0,0 +1,68 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-05-23 17:26+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: taggit/apps.py:7
+msgid "Taggit"
+msgstr "标签项"
+
+#: taggit/forms.py:27
+msgid "Please provide a comma-separated list of tags."
+msgstr ""
+
+#: taggit/managers.py:299 taggit/models.py:102
+msgid "Tags"
+msgstr "标签"
+
+#: taggit/managers.py:300
+msgid "A comma-separated list of tags."
+msgstr ""
+
+#: taggit/models.py:47
+msgid "Name"
+msgstr "名称"
+
+#: taggit/models.py:48
+msgid "Slug"
+msgstr "唯一标识"
+
+#: taggit/models.py:101
+msgid "Tag"
+msgstr "标签"
+
+#: taggit/models.py:108
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s 使用了标签 %(tag)s"
+
+#: taggit/models.py:163
+msgid "Content type"
+msgstr "内容类型"
+
+#: taggit/models.py:207 taggit/models.py:216
+msgid "Object id"
+msgstr "对象ID"
+
+#: taggit/models.py:224
+msgid "Tagged Item"
+msgstr "标签项"
+
+#: taggit/models.py:225
+msgid "Tagged Items"
+msgstr "标签项"
diff --git a/taggit/managers.py b/taggit/managers.py
index 6dec89a..550e60d 100644
--- a/taggit/managers.py
+++ b/taggit/managers.py
@@ -6,15 +6,18 @@ from django import VERSION
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models, router
+from django.db.models import signals
from django.db.models.fields import Field
-from django.db.models.fields.related import ManyToManyRel, OneToOneRel, RelatedField
+from django.db.models.fields.related import (ManyToManyRel, OneToOneRel,
+ RelatedField)
from django.utils import six
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from taggit.forms import TagField
from taggit.models import CommonGenericTaggedItemBase, TaggedItem
-from taggit.utils import _get_field, _related_model, _remote_field, require_instance_manager
+from taggit.utils import (_get_field, _related_model, _remote_field,
+ require_instance_manager)
if VERSION < (1, 8):
# related.py was removed in Django 1.8
@@ -164,16 +167,54 @@ class _TaggableManager(models.Manager):
@require_instance_manager
def add(self, *tags):
+ db = router.db_for_write(self.through, instance=self.instance)
+
+ tag_objs = self._to_tag_model_instances(tags)
+ new_ids = set(t.pk for t in tag_objs)
+
+ # NOTE: can we hardcode 'tag_id' here or should the column name be got
+ # dynamically from somewhere?
+ vals = (self.through._default_manager.using(db)
+ .values_list('tag_id', flat=True)
+ .filter(**self._lookup_kwargs()))
+
+ new_ids = new_ids - set(vals)
+
+ signals.m2m_changed.send(
+ sender=self.through, action="pre_add",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=new_ids, using=db,
+ )
+
+ for tag in tag_objs:
+ self.through._default_manager.using(db).get_or_create(
+ tag=tag, **self._lookup_kwargs())
+
+ signals.m2m_changed.send(
+ sender=self.through, action="post_add",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=new_ids, using=db,
+ )
+
+ def _to_tag_model_instances(self, tags):
+ """
+ Takes an iterable containing either strings, tag objects, or a mixture
+ of both and returns set of tag objects.
+ """
+ db = router.db_for_write(self.through, instance=self.instance)
+
str_tags = set()
tag_objs = set()
+
for t in tags:
if isinstance(t, self.through.tag_model()):
tag_objs.add(t)
elif isinstance(t, six.string_types):
str_tags.add(t)
else:
- raise ValueError("Cannot add {0} ({1}). Expected {2} or str.".format(
- t, type(t), type(self.through.tag_model())))
+ raise ValueError(
+ "Cannot add {0} ({1}). Expected {2} or str.".format(
+ t, type(t), type(self.through.tag_model())))
if getattr(settings, 'TAGGIT_CASE_INSENSITIVE', False):
# Some databases can do case-insensitive comparison with IN, which
@@ -183,26 +224,30 @@ class _TaggableManager(models.Manager):
for name in str_tags:
try:
- tag = self.through.tag_model().objects.get(name__iexact=name)
+ tag = (self.through.tag_model()._default_manager
+ .using(db)
+ .get(name__iexact=name))
existing.append(tag)
except self.through.tag_model().DoesNotExist:
tags_to_create.append(name)
else:
- # If str_tags has 0 elements Django actually optimizes that to not do a
- # query. Malcolm is very smart.
- existing = self.through.tag_model().objects.filter(
- name__in=str_tags
- )
+ # If str_tags has 0 elements Django actually optimizes that to not
+ # do a query. Malcolm is very smart.
+ existing = (self.through.tag_model()._default_manager
+ .using(db)
+ .filter(name__in=str_tags))
tags_to_create = str_tags - set(t.name for t in existing)
tag_objs.update(existing)
for new_tag in tags_to_create:
- tag_objs.add(self.through.tag_model().objects.create(name=new_tag))
+ tag_objs.add(
+ self.through.tag_model()._default_manager
+ .using(db)
+ .create(name=new_tag))
- for tag in tag_objs:
- self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs())
+ return tag_objs
@require_instance_manager
def names(self):
@@ -213,18 +258,82 @@ class _TaggableManager(models.Manager):
return self.get_queryset().values_list('slug', flat=True)
@require_instance_manager
- def set(self, *tags):
- self.clear()
- self.add(*tags)
+ def set(self, *tags, **kwargs):
+ """
+ Set the object's tags to the given n tags. If the clear kwarg is True
+ then all existing tags are removed (using `.clear()`) and the new tags
+ added. Otherwise, only those tags that are not present in the args are
+ removed and any new tags added.
+ """
+ db = router.db_for_write(self.through, instance=self.instance)
+ clear = kwargs.pop('clear', False)
+
+ if clear:
+ self.clear()
+ self.add(*tags)
+ else:
+ # make sure we're working with a collection of a uniform type
+ objs = self._to_tag_model_instances(tags)
+
+ # get the existing tag strings
+ old_tag_strs = set(self.through._default_manager
+ .using(db)
+ .filter(**self._lookup_kwargs())
+ .values_list('tag__name', flat=True))
+
+ new_objs = []
+ for obj in objs:
+ if obj.name in old_tag_strs:
+ old_tag_strs.remove(obj.name)
+ else:
+ new_objs.append(obj)
+
+ self.remove(*old_tag_strs)
+ self.add(*new_objs)
@require_instance_manager
def remove(self, *tags):
- self.through.objects.filter(**self._lookup_kwargs()).filter(
- tag__name__in=tags).delete()
+ if not tags:
+ return
+
+ db = router.db_for_write(self.through, instance=self.instance)
+
+ qs = (self.through._default_manager.using(db)
+ .filter(**self._lookup_kwargs())
+ .filter(tag__name__in=tags))
+
+ old_ids = set(qs.values_list('tag_id', flat=True))
+
+ signals.m2m_changed.send(
+ sender=self.through, action="pre_remove",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=old_ids, using=db,
+ )
+ qs.delete()
+ signals.m2m_changed.send(
+ sender=self.through, action="post_remove",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=old_ids, using=db,
+ )
@require_instance_manager
def clear(self):
- self.through.objects.filter(**self._lookup_kwargs()).delete()
+ db = router.db_for_write(self.through, instance=self.instance)
+
+ signals.m2m_changed.send(
+ sender=self.through, action="pre_clear",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=None, using=db,
+ )
+
+ self.through._default_manager.using(db).filter(
+ **self._lookup_kwargs()).delete()
+
+ signals.m2m_changed.send(
+ sender=self.through, action="post_clear",
+ instance=self.instance, reverse=False,
+ model=self.through.tag_model(), pk_set=None, using=db,
+ )
def most_common(self, min_count=None):
queryset = self.get_queryset().annotate(
diff --git a/taggit/models.py b/taggit/models.py
index cd0d147..2189aac 100644
--- a/taggit/models.py
+++ b/taggit/models.py
@@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from taggit.utils import _get_field
+
try:
from unidecode import unidecode
except ImportError:
diff --git a/tests/custom_parser.py b/tests/custom_parser.py
index 5b2e95b..b03f10d 100644
--- a/tests/custom_parser.py
+++ b/tests/custom_parser.py
@@ -1,5 +1,6 @@
def comma_splitter(tag_string):
return [t.strip() for t in tag_string.split(',') if t.strip()]
+
def comma_joiner(tags):
return ', '.join(t.name for t in tags)
diff --git a/tests/forms.py b/tests/forms.py
index 4513389..8a4a453 100644
--- a/tests/forms.py
+++ b/tests/forms.py
@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
-from django import forms, VERSION
+from django import VERSION, forms
from .models import (CustomPKFood, DirectCustomPKFood, DirectFood, Food,
OfficialFood)
@@ -15,21 +15,25 @@ class FoodForm(forms.ModelForm):
model = Food
fields = fields
+
class DirectFoodForm(forms.ModelForm):
class Meta:
model = DirectFood
fields = fields
+
class DirectCustomPKFoodForm(forms.ModelForm):
class Meta:
model = DirectCustomPKFood
fields = fields
+
class CustomPKFoodForm(forms.ModelForm):
class Meta:
model = CustomPKFood
fields = fields
+
class OfficialFoodForm(forms.ModelForm):
class Meta:
model = OfficialFood
diff --git a/tests/models.py b/tests/models.py
index 71a92fd..a8ddf8a 100644
--- a/tests/models.py
+++ b/tests/models.py
@@ -21,10 +21,12 @@ class MultipleTags(models.Model):
tags1 = TaggableManager(through=Through1, related_name='tags1')
tags2 = TaggableManager(through=Through2, related_name='tags2')
+
# Ensure that two TaggableManagers with GFK via different through models are allowed.
class ThroughGFK(GenericTaggedItemBase):
tag = models.ForeignKey(Tag, related_name='tagged_items', on_delete=models.CASCADE)
+
class MultipleTagsGFK(models.Model):
tags1 = TaggableManager(related_name='tagsgfk1')
tags2 = TaggableManager(through=ThroughGFK, related_name='tagsgfk2')
@@ -39,6 +41,7 @@ class Food(models.Model):
def __str__(self):
return self.name
+
@python_2_unicode_compatible
class Pet(models.Model):
name = models.CharField(max_length=50)
@@ -92,9 +95,11 @@ class DirectHousePet(DirectPet):
class TaggedCustomPKFood(TaggedItemBase):
content_object = models.ForeignKey('DirectCustomPKFood', on_delete=models.CASCADE)
+
class TaggedCustomPKPet(TaggedItemBase):
content_object = models.ForeignKey('DirectCustomPKPet', on_delete=models.CASCADE)
+
@python_2_unicode_compatible
class DirectCustomPKFood(models.Model):
name = models.CharField(max_length=50, primary_key=True)
@@ -104,6 +109,7 @@ class DirectCustomPKFood(models.Model):
def __str__(self):
return self.name
+
@python_2_unicode_compatible
class DirectCustomPKPet(models.Model):
name = models.CharField(max_length=50, primary_key=True)
@@ -113,14 +119,16 @@ class DirectCustomPKPet(models.Model):
def __str__(self):
return self.name
+
class DirectCustomPKHousePet(DirectCustomPKPet):
trained = models.BooleanField(default=False)
-# Test custom through model to model with custom PK using GenericForeignKey
+# Test custom through model to model with custom PK using GenericForeignKey
class TaggedCustomPK(CommonGenericTaggedItemBase, TaggedItemBase):
object_id = models.CharField(max_length=50, verbose_name='Object id', db_index=True)
+
@python_2_unicode_compatible
class CustomPKFood(models.Model):
name = models.CharField(max_length=50, primary_key=True)
@@ -130,6 +138,7 @@ class CustomPKFood(models.Model):
def __str__(self):
return self.name
+
@python_2_unicode_compatible
class CustomPKPet(models.Model):
name = models.CharField(max_length=50, primary_key=True)
@@ -139,17 +148,21 @@ class CustomPKPet(models.Model):
def __str__(self):
return self.name
+
class CustomPKHousePet(CustomPKPet):
trained = models.BooleanField(default=False)
# Test custom through model to a custom tag model
+
class OfficialTag(TagBase):
official = models.BooleanField(default=False)
+
class OfficialThroughModel(GenericTaggedItemBase):
tag = models.ForeignKey(OfficialTag, related_name="tagged_items", on_delete=models.CASCADE)
+
@python_2_unicode_compatible
class OfficialFood(models.Model):
name = models.CharField(max_length=50)
@@ -159,6 +172,7 @@ class OfficialFood(models.Model):
def __str__(self):
return self.name
+
@python_2_unicode_compatible
class OfficialPet(models.Model):
name = models.CharField(max_length=50)
@@ -168,6 +182,7 @@ class OfficialPet(models.Model):
def __str__(self):
return self.name
+
class OfficialHousePet(OfficialPet):
trained = models.BooleanField(default=False)
@@ -178,9 +193,11 @@ class Media(models.Model):
class Meta:
abstract = True
+
class Photo(Media):
pass
+
class Movie(Media):
pass
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..68fdfe3
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,16 @@
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:'
+ }
+}
+
+INSTALLED_APPS = [
+ 'django.contrib.contenttypes',
+ 'taggit',
+ 'tests',
+]
+
+MIDDLEWARE_CLASSES = []
+
+SECRET_KEY = 'secretkey'
diff --git a/tests/tests.py b/tests/tests.py
index 9e4d2c3..fc83b41 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -5,6 +5,7 @@ import warnings
from unittest import TestCase as UnitTestCase
import django
+import mock
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.core.exceptions import ImproperlyConfigured, ValidationError
@@ -20,14 +21,14 @@ from .models import (Article, Child, CustomManager, CustomPKFood,
CustomPKHousePet, CustomPKPet, DirectCustomPKFood,
DirectCustomPKHousePet, DirectCustomPKPet, DirectFood,
DirectHousePet, DirectPet, Food, HousePet, Movie,
- OfficialFood, OfficialHousePet, OfficialPet,
- OfficialTag, OfficialThroughModel, Pet, Photo,
- TaggedCustomPK, TaggedCustomPKFood, TaggedFood)
+ OfficialFood, OfficialHousePet, OfficialPet, OfficialTag,
+ OfficialThroughModel, Pet, Photo, TaggedCustomPK,
+ TaggedCustomPKFood, TaggedFood)
-from taggit.managers import _model_name, _TaggableManager, TaggableManager
+from taggit.managers import TaggableManager, _model_name, _TaggableManager
from taggit.models import Tag, TaggedItem
-
-from taggit.utils import edit_string_for_tags, parse_tags, _remote_field, _related_model
+from taggit.utils import (_related_model, _remote_field, edit_string_for_tags,
+ parse_tags)
try:
from unittest import skipIf, skipUnless
@@ -56,7 +57,7 @@ class BaseTaggingTest(object):
}
return form_str
- def assert_form_renders(self, form, html):
+ def assertFormRenders(self, form, html):
# Django causes a DeprecationWarning on Python 3.3, 3.4
if (3, 3) <= sys.version_info < (3, 5):
with warnings.catch_warnings(record=True):
@@ -117,22 +118,27 @@ class TagModelTestCase(BaseTaggingTransactionTestCase):
r"Expected <class 'django.db.models.base.ModelBase'> or str.")):
apple.tags.add(1)
+
class TagModelDirectTestCase(TagModelTestCase):
food_model = DirectFood
tag_model = Tag
+
class TagModelDirectCustomPKTestCase(TagModelTestCase):
food_model = DirectCustomPKFood
tag_model = Tag
+
class TagModelCustomPKTestCase(TagModelTestCase):
food_model = CustomPKFood
tag_model = Tag
+
class TagModelOfficialTestCase(TagModelTestCase):
food_model = OfficialFood
tag_model = OfficialTag
+
class TaggableManagerTestCase(BaseTaggingTestCase):
food_model = Food
pet_model = Pet
@@ -180,31 +186,217 @@ class TaggableManagerTestCase(BaseTaggingTestCase):
apple.delete()
self.assert_tags_equal(self.food_model.tags.all(), ["green"])
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_add_new_tag_sends_m2m_changed_signals(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('green')
+ green_pk = self.tag_model.objects.get(name='green').pk
+
+ self.assertEqual(send_mock.call_count, 2)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_add_existing_tag_sends_m2m_changed_signals(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ green = self.tag_model.objects.create(name='green')
+ apple.tags.add('green')
+
+ self.assertEqual(send_mock.call_count, 2)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green.pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green.pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_add_second_tag_sends_m2m_changed_signals_with_correct_new_pks(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ green = self.tag_model.objects.create(name='green')
+ apple.tags.add('red')
+ send_mock.reset_mock()
+ apple.tags.add('green', 'red')
+
+ self.assertEqual(send_mock.call_count, 2)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green.pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green.pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_remove_tag_sends_m2m_changed_signals(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('green')
+ green_pk = self.tag_model.objects.get(name='green').pk
+ send_mock.reset_mock()
+
+ apple.tags.remove('green')
+
+ self.assertEqual(send_mock.call_count, 2)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_remove',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_remove',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_clear_sends_m2m_changed_signal(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('red')
+ send_mock.reset_mock()
+ apple.tags.clear()
+
+ self.assertEqual(send_mock.call_count, 2)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_clear',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=None,
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_clear',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=None,
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
+ @mock.patch('django.db.models.signals.m2m_changed.send')
+ def test_set_sends_m2m_changed_signal(self, send_mock):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add('green')
+ send_mock.reset_mock()
+
+ apple.tags.set('red')
+
+ green_pk = self.tag_model.objects.get(name='green').pk
+ red_pk = self.tag_model.objects.get(name='red').pk
+
+ self.assertEqual(send_mock.call_count, 4)
+ send_mock.assert_has_calls([
+ mock.call(
+ action=u'pre_remove',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_remove',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([green_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'pre_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([red_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default'),
+ mock.call(
+ action=u'post_add',
+ instance=apple,
+ model=self.tag_model,
+ pk_set=set([red_pk]),
+ reverse=False,
+ sender=self.taggeditem_model,
+ using='default')]
+ )
+
def test_add_queries(self):
# Prefill content type cache:
ContentType.objects.get_for_model(self.food_model)
apple = self.food_model.objects.create(name="apple")
# 1 query to see which tags exist
+ # 1 query to check existing ids for sending m2m_changed signal
# + 3 queries to create the tags.
# + 6 queries to create the intermediary things (including SELECTs, to
# make sure we don't double create.
- # + 12 on Django 1.6 for save points.
- queries = 22
+ # + 12 on Django 1.6+ for save points.
+ queries = 23
if django.VERSION < (1, 6):
queries -= 12
self.assertNumQueries(queries, apple.tags.add, "red", "delicious", "green")
pear = self.food_model.objects.create(name="pear")
# 1 query to see which tags exist
+ # 1 query to check existing ids for sending m2m_changed signal
# + 4 queries to create the intermeidary things (including SELECTs, to
# make sure we dont't double create.
- # + 4 on Django 1.6 for save points.
- queries = 9
+ # + 4 on Django 1.6+ for save points.
+ queries = 10
if django.VERSION < (1, 6):
queries -= 4
self.assertNumQueries(queries, pear.tags.add, "green", "delicious")
- self.assertNumQueries(0, pear.tags.add)
+ # 1 query to check existing ids for sending m2m_changed signal
+ self.assertNumQueries(1, pear.tags.add)
def test_require_pk(self):
food_instance = self.food_model()
@@ -442,6 +634,7 @@ class TaggableManagerDirectTestCase(TaggableManagerTestCase):
housepet_model = DirectHousePet
taggeditem_model = TaggedFood
+
class TaggableManagerDirectCustomPKTestCase(TaggableManagerTestCase):
food_model = DirectCustomPKFood
pet_model = DirectCustomPKPet
@@ -453,6 +646,7 @@ class TaggableManagerDirectCustomPKTestCase(TaggableManagerTestCase):
# tell if the instance is saved or not
pass
+
class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
food_model = CustomPKFood
pet_model = CustomPKPet
@@ -464,6 +658,7 @@ class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
# tell if the instance is saved or not
pass
+
class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
food_model = OfficialFood
pet_model = OfficialPet
@@ -491,6 +686,7 @@ class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
tag_info = self.tag_model.objects.filter(officialfood__in=[apple.id, pear.id], name='green').annotate(models.Count('name'))
self.assertEqual(tag_info[0].name__count, 2)
+
class TaggableManagerInitializationTestCase(TaggableManagerTestCase):
"""Make sure manager override defaults and sets correctly."""
food_model = Food
@@ -502,6 +698,7 @@ class TaggableManagerInitializationTestCase(TaggableManagerTestCase):
def test_custom_manager(self):
self.assertEqual(self.custom_manager_model.tags.__class__, CustomManager.Foo)
+
class TaggableFormTestCase(BaseTaggingTestCase):
form_class = FoodForm
food_model = Food
@@ -510,7 +707,7 @@ class TaggableFormTestCase(BaseTaggingTestCase):
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>
+ self.assertFormRenders(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>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
f.save()
apple = self.food_model.objects.get(name='apple')
@@ -526,17 +723,17 @@ class TaggableFormTestCase(BaseTaggingTestCase):
self.assertFalse(f.is_valid())
f = self.form_class(instance=apple)
- 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.assertFormRenders(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>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
apple.tags.add('has,comma')
f = self.form_class(instance=apple)
- 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.assertFormRenders(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>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value=""has,comma", delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
apple.tags.add('has space')
f = self.form_class(instance=apple)
- 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.assertFormRenders(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>
<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value=""has space", "has,comma", delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
def test_formfield(self):
@@ -552,18 +749,22 @@ class TaggableFormTestCase(BaseTaggingTestCase):
ff = tm.formfield()
self.assertRaises(ValidationError, ff.clean, "")
+
class TaggableFormDirectTestCase(TaggableFormTestCase):
form_class = DirectFoodForm
food_model = DirectFood
+
class TaggableFormDirectCustomPKTestCase(TaggableFormTestCase):
form_class = DirectCustomPKFoodForm
food_model = DirectCustomPKFood
+
class TaggableFormCustomPKTestCase(TaggableFormTestCase):
form_class = CustomPKFoodForm
food_model = CustomPKFood
+
class TaggableFormOfficialTestCase(TaggableFormTestCase):
form_class = OfficialFoodForm
food_model = OfficialFood
@@ -644,9 +845,9 @@ class TagStringParseTestCase(UnitTestCase):
['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')
+ plain = Tag(name='plain')
+ spaces = Tag(name='spa ces')
+ comma = Tag(name='com,ma')
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')
@@ -663,8 +864,8 @@ class TagStringParseTestCase(UnitTestCase):
@override_settings(TAGGIT_STRING_FROM_TAGS='tests.custom_parser.comma_joiner')
def test_custom_comma_joiner(self):
- a = Tag.objects.create(name='Cued Speech')
- b = Tag.objects.create(name='transliterator')
+ a = Tag(name='Cued Speech')
+ b = Tag(name='transliterator')
self.assertEqual(edit_string_for_tags([a, b]), 'Cued Speech, transliterator')
diff --git a/tox.ini b/tox.ini
index 46f975f..a31a77d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,123 +1,18 @@
+[tox]
+envlist =
+ py27-1.4.x
+ {py27,py33,py34}-{1.5.x,1.6.x,1.7.x}
+ {py27,py33,py34,py35}-1.8.x
+ {py27,py34,py35}-1.9.x
+
[testenv]
-skipsdist = True
-usedevelop = True
deps =
- flake8
-deps14 =
- https://github.com/django/django/archive/stable/1.4.x.tar.gz#egg=django
-deps15 =
- https://github.com/django/django/archive/stable/1.5.x.tar.gz#egg=django
-deps16 =
- https://github.com/django/django/archive/stable/1.6.x.tar.gz#egg=django
-deps17 =
- https://github.com/django/django/archive/stable/1.7.x.tar.gz#egg=django
-deps18 =
- https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
-deps19 =
- https://github.com/django/django/archive/stable/1.9.x.tar.gz#egg=django
-
+ 1.4.x: https://github.com/django/django/archive/stable/1.4.x.tar.gz#egg=django
+ 1.5.x: https://github.com/django/django/archive/stable/1.5.x.tar.gz#egg=django
+ 1.6.x: https://github.com/django/django/archive/stable/1.6.x.tar.gz#egg=django
+ 1.7.x: https://github.com/django/django/archive/stable/1.7.x.tar.gz#egg=django
+ 1.8.x: https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
+ 1.9.x: https://github.com/django/django/archive/stable/1.9.x.tar.gz#egg=django
commands =
- python ./runtests.py {posargs}
-
-
-[testenv:py27-1.4.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps14}
-
-[testenv:py27-1.5.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps15}
-
-[testenv:py27-1.6.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps16}
-
-[testenv:py27-1.7.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps17}
-
-[testenv:py27-1.8.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps18}
-
-[testenv:py27-1.9.x]
-basepython = python2.7
-deps =
- {[testenv]deps}
- {[testenv]deps19}
-
-[testenv:py33-1.5.x]
-basepython = python3.3
-deps =
- {[testenv]deps}
- {[testenv]deps15}
-
-[testenv:py33-1.6.x]
-basepython = python3.3
-deps =
- {[testenv]deps}
- {[testenv]deps16}
-
-[testenv:py33-1.7.x]
-basepython = python3.3
-deps =
- {[testenv]deps}
- {[testenv]deps17}
-
-[testenv:py33-1.8.x]
-basepython = python3.3
-deps =
- {[testenv]deps}
- {[testenv]deps18}
-
-[testenv:py34-1.5.x]
-basepython = python3.4
-deps =
- {[testenv]deps}
- {[testenv]deps15}
-
-[testenv:py34-1.6.x]
-basepython = python3.4
-deps =
- {[testenv]deps}
- {[testenv]deps16}
-
-[testenv:py34-1.7.x]
-basepython = python3.4
-deps =
- {[testenv]deps}
- {[testenv]deps17}
-
-[testenv:py34-1.8.x]
-basepython = python3.4
-deps =
- {[testenv]deps}
- {[testenv]deps18}
-
-[testenv:py34-1.9.x]
-basepython = python3.4
-deps =
- {[testenv]deps}
- {[testenv]deps19}
-
-[testenv:py35-1.8.x]
-basepython = python3.5
-deps =
- {[testenv]deps}
- {[testenv]deps18}
-
-[testenv:py35-1.9.x]
-basepython = python3.5
-deps =
- {[testenv]deps}
- {[testenv]deps19}
+ make develop test
+whitelist_externals = make
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/collab-maint/django-taggit.git
More information about the Python-modules-commits
mailing list