diff --git a/docs/managers.rst b/docs/managers.rst index 5886f65e..dc2dc4de 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -107,4 +107,39 @@ shortcut: ... For further discussion see `this topic on the Q&A page -`_. \ No newline at end of file +`_. + + +Natural Key Serialization +------------------------- + +When using Django's natural key serialization with :django-admin:`dumpdata` and +:django-admin:`loaddata`, polymorphic models require special handling. + +.. important:: + + Always use :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` in + ``get_by_natural_key()`` for polymorphic models. Without this, deserialization fails + when loading new objects because polymorphic queries try to fetch incomplete objects. + +Example implementation: + +.. literalinclude:: ../src/polymorphic/tests/examples/managers/models.py + :language: python + :linenos: + +Usage: + +.. code-block:: bash + + # Dump with natural keys + $ python manage.py dumpdata myapp --natural-primary --natural-foreign > fixtures.json + + # Load into another database + $ python manage.py loaddata fixtures.json + +.. note:: + + * Child models inherit ``natural_key()`` from the parent - no need to override + * Always use both ``--natural-primary`` and ``--natural-foreign`` flags with polymorphic models + * See `issue #517 `_ for details \ No newline at end of file diff --git a/src/polymorphic/tests/deletion/migrations/0001_initial.py b/src/polymorphic/tests/deletion/migrations/0001_initial.py index b1698f1b..fc12feaa 100644 --- a/src/polymorphic/tests/deletion/migrations/0001_initial.py +++ b/src/polymorphic/tests/deletion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2 on 2026-01-08 00:17 +# Generated by Django 4.2 on 2026-01-08 16:36 from decimal import Decimal from django.conf import settings diff --git a/src/polymorphic/tests/examples/managers/__init__.py b/src/polymorphic/tests/examples/managers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/polymorphic/tests/examples/managers/migrations/0001_initial.py b/src/polymorphic/tests/examples/managers/migrations/0001_initial.py new file mode 100644 index 00000000..99e90e8f --- /dev/null +++ b/src/polymorphic/tests/examples/managers/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.9 on 2026-01-08 16:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(unique=True)), + ('title', models.CharField(max_length=200)), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='BlogPost', + fields=[ + ('article_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='managers.article')), + ('author', models.CharField(max_length=100)), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('managers.article',), + ), + ] diff --git a/src/polymorphic/tests/examples/managers/migrations/__init__.py b/src/polymorphic/tests/examples/managers/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/polymorphic/tests/examples/managers/models.py b/src/polymorphic/tests/examples/managers/models.py new file mode 100644 index 00000000..f39fc7c1 --- /dev/null +++ b/src/polymorphic/tests/examples/managers/models.py @@ -0,0 +1,21 @@ +from django.db import models +from polymorphic.models import PolymorphicModel +from polymorphic.managers import PolymorphicManager + + +class ArticleManager(PolymorphicManager): + def get_by_natural_key(self, slug): + return self.non_polymorphic().get(slug=slug) + + +class Article(PolymorphicModel): + slug = models.SlugField(unique=True) + title = models.CharField(max_length=200) + objects = ArticleManager() + + def natural_key(self): + return (self.slug,) + + +class BlogPost(Article): + author = models.CharField(max_length=100) diff --git a/src/polymorphic/tests/examples/managers/tests.py b/src/polymorphic/tests/examples/managers/tests.py new file mode 100644 index 00000000..187e0d09 --- /dev/null +++ b/src/polymorphic/tests/examples/managers/tests.py @@ -0,0 +1,37 @@ +from django.test import TestCase +from django.core import serializers +from .models import Article, BlogPost + + +class NaturalKeyTests(TestCase): + def test_natural_key_serialization(self): + # Create objects + Article.objects.create(slug="article-1", title="First Article") + BlogPost.objects.create(slug="blog-post-1", title="First Blog Post", author="John Doe") + + # Serialize using natural keys + data = serializers.serialize( + "json", + Article.objects.all(), + use_natural_foreign_keys=True, + use_natural_primary_keys=True, + ) + + # Verify serialization contains natural keys (slugs) + self.assertIn("article-1", data) + self.assertIn("blog-post-1", data) + + # Deserialize + for obj in serializers.deserialize("json", data): + obj.save() + + # Verify objects still exist and are correct types + self.assertEqual(Article.objects.count(), 2) + self.assertEqual(BlogPost.objects.count(), 1) + + a1 = Article.objects.get(slug="article-1") + self.assertIsInstance(a1, Article) + self.assertNotIsInstance(a1, BlogPost) + + b1 = Article.objects.get(slug="blog-post-1") + self.assertIsInstance(b1, BlogPost) diff --git a/src/polymorphic/tests/migrations/0001_initial.py b/src/polymorphic/tests/migrations/0001_initial.py index 1321f060..a7d1e68c 100644 --- a/src/polymorphic/tests/migrations/0001_initial.py +++ b/src/polymorphic/tests/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2 on 2026-01-08 00:17 +# Generated by Django 4.2 on 2026-01-08 16:36 from django.conf import settings from django.db import migrations, models diff --git a/src/polymorphic/tests/settings.py b/src/polymorphic/tests/settings.py index 89ba8c79..e5e2d5e0 100644 --- a/src/polymorphic/tests/settings.py +++ b/src/polymorphic/tests/settings.py @@ -103,6 +103,7 @@ "polymorphic.tests.deletion", "polymorphic.tests.test_migrations", "polymorphic.tests.examples.views", + "polymorphic.tests.examples.managers", ) MIDDLEWARE = (