[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