From 9fecb86a5221982ea753270b7d2b4c6487f79941 Mon Sep 17 00:00:00 2001
From: Claude Paroz <claude@2xlibre.net>
Date: Sun, 21 Dec 2014 14:24:15 +0100
Subject: [PATCH] Fixed #17946 -- Fixed deserialization of self-referencing M2M
 fields

Thanks Philip Mountifield for the report and excellent analysis, and
Simon Charette for the review.
---
 django/core/serializers/python.py              | 14 +++++++-------
 tests/fixtures_regress/fixtures/m2mtoself.json |  1 +
 tests/fixtures_regress/models.py               |  4 ++++
 tests/fixtures_regress/tests.py                | 10 ++++++++++
 4 files changed, 22 insertions(+), 7 deletions(-)
 create mode 100644 tests/fixtures_regress/fixtures/m2mtoself.json

diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py
index db4e11452e..b4712c76be 100644
--- a/django/core/serializers/python.py
+++ b/django/core/serializers/python.py
@@ -9,7 +9,7 @@ from django.apps import apps
 from django.conf import settings
 from django.core.serializers import base
 from django.db import models, DEFAULT_DB_ALIAS
-from django.utils.encoding import smart_text, is_protected_type
+from django.utils.encoding import force_text, is_protected_type
 from django.utils import six
 
 
@@ -36,11 +36,11 @@ class Serializer(base.Serializer):
 
     def get_dump_object(self, obj):
         data = {
-            "model": smart_text(obj._meta),
+            "model": force_text(obj._meta),
             "fields": self._current,
         }
         if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'):
-            data["pk"] = smart_text(obj._get_pk_val(), strings_only=True)
+            data["pk"] = force_text(obj._get_pk_val(), strings_only=True)
 
         return data
 
@@ -70,7 +70,7 @@ class Serializer(base.Serializer):
             if self.use_natural_foreign_keys and hasattr(field.rel.to, 'natural_key'):
                 m2m_value = lambda value: value.natural_key()
             else:
-                m2m_value = lambda value: smart_text(value._get_pk_val(), strings_only=True)
+                m2m_value = lambda value: force_text(value._get_pk_val(), strings_only=True)
             self._current[field.name] = [m2m_value(related)
                                for related in getattr(obj, field.name).iterator()]
 
@@ -111,7 +111,7 @@ def Deserializer(object_list, **options):
                 continue
 
             if isinstance(field_value, str):
-                field_value = smart_text(
+                field_value = force_text(
                     field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True
                 )
 
@@ -124,9 +124,9 @@ def Deserializer(object_list, **options):
                         if hasattr(value, '__iter__') and not isinstance(value, six.text_type):
                             return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk
                         else:
-                            return smart_text(field.rel.to._meta.pk.to_python(value))
+                            return force_text(field.rel.to._meta.pk.to_python(value), strings_only=True)
                 else:
-                    m2m_convert = lambda v: smart_text(field.rel.to._meta.pk.to_python(v))
+                    m2m_convert = lambda v: force_text(field.rel.to._meta.pk.to_python(v), strings_only=True)
                 m2m_data[field.name] = [m2m_convert(pk) for pk in field_value]
 
             # Handle FK fields
diff --git a/tests/fixtures_regress/fixtures/m2mtoself.json b/tests/fixtures_regress/fixtures/m2mtoself.json
new file mode 100644
index 0000000000..b904ba36e0
--- /dev/null
+++ b/tests/fixtures_regress/fixtures/m2mtoself.json
@@ -0,0 +1 @@
+[{"fields": {"parent": [1]}, "model": "fixtures_regress.m2mtoself", "pk": 1}]
diff --git a/tests/fixtures_regress/models.py b/tests/fixtures_regress/models.py
index f75b1e02d0..cd381e92bd 100644
--- a/tests/fixtures_regress/models.py
+++ b/tests/fixtures_regress/models.py
@@ -248,6 +248,10 @@ class Thingy(models.Model):
     name = models.CharField(max_length=255)
 
 
+class M2MToSelf(models.Model):
+    parent = models.ManyToManyField("self", blank=True, null=True)
+
+
 @python_2_unicode_compatible
 class BaseNKModel(models.Model):
     """
diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py
index 89b8fb17ef..a91935af8f 100644
--- a/tests/fixtures_regress/tests.py
+++ b/tests/fixtures_regress/tests.py
@@ -487,6 +487,16 @@ class TestFixtures(TestCase):
             verbosity=0,
         )
 
+    def test_loaddata_with_m2m_to_self(self):
+        """
+        Regression test for ticket #17946.
+        """
+        management.call_command(
+            'loaddata',
+            'm2mtoself.json',
+            verbosity=0,
+        )
+
     @override_settings(FIXTURE_DIRS=[os.path.join(_cur_dir, 'fixtures_1'),
                                      os.path.join(_cur_dir, 'fixtures_1')])
     def test_fixture_dirs_with_duplicates(self):