diff --git a/docs/advanced.rst b/docs/advanced.rst index 8da7de58..88aa1cae 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -124,8 +124,6 @@ including the complexity around related fields and multi-table inheritance. :pypi:`django-polymorphic` offers a utility function :func:`~polymorphic.utils.prepare_for_copy` that resets all necessary fields on a model instance to prepare it for copying: -.. code-block:: python - from polymorphic.utils import prepare_for_copy obj = ModelB.objects.first() @@ -133,6 +131,124 @@ that resets all necessary fields on a model instance to prepare it for copying: obj.save() # obj is now a copy of the original ModelB instance + +Working with Signals and Fixtures +---------------------------------- + +When using Django's :django-admin:`loaddata` command with polymorphic models, you may notice that +``post_save`` signal handlers receive instances that appear incomplete – parent class attributes +may be empty and ``pk``/``id`` fields may not match. **This is expected Django behavior** for +multi-table inheritance during deserialization, not a bug in :pypi:`django-polymorphic`. + +Understanding the Issue +~~~~~~~~~~~~~~~~~~~~~~~~ + +During fixture loading, Django deserializes parent and child table rows separately +(see :Django:`fixture loading `). + +When a child model's ``post_save`` signal fires, Django passes a ``raw=True`` parameter to indicate +the data is being loaded from a fixture. At this point, parent attributes may not yet be fully +accessible. + +Django’s :Django:`post_save ` signal is called with ``raw=True`` during +fixture loading, and signal handlers should not rely on related or inherited model data at this +stage. + +For example, with this model hierarchy: + +.. code-block:: python + + class Endpoint(PolymorphicModel): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + name = models.CharField(max_length=250) + + class Switch(Endpoint): + ip_address = models.GenericIPAddressField() + + @receiver(post_save, sender=Switch) + def switch_saved(sender, instance, created, **kwargs): + # During loaddata: instance.name may be empty! + print(f"Switch: {instance.name}") + +During ``loaddata``, the signal may fire before ``instance.name`` is populated, even though the +fixture contains the correct data. + +Recommended Solution: Check for ``raw=True`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The standard Django pattern is to check the ``raw`` parameter and skip custom logic during +fixture loading, as recommended in Django’s signal documentation +(:Django:`post_save `): + +.. code-block:: python + + from django.db.models.signals import post_save + from django.dispatch import receiver + + @receiver(post_save, sender=Switch) + def switch_saved(sender, instance, created, raw, **kwargs): + # Skip signal logic during fixture loading + if raw: + return + + if created: + # This logic only runs during normal saves, not loaddata + print(f"New switch created: {instance.name}") + setup_monitoring(instance) + +This approach prevents issues caused by incomplete data during fixture deserialization. + +Alternative: Use ``post_migrate`` Signal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to perform setup tasks after loading fixtures, use the ``post_migrate`` signal instead, +which runs after migrations and fixture loading have completed: + +.. code-block:: python + + from django.db.models.signals import post_migrate + from django.dispatch import receiver + + @receiver(post_migrate) + def setup_switches(sender, **kwargs): + """Run after migrations and fixtures are loaded""" + from myapp.models import Switch + + for switch in Switch.objects.filter(monitoring_configured=False): + # Now all attributes are fully loaded + setup_monitoring(switch) + switch.monitoring_configured = True + switch.save() + +Best Practices for Fixtures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When working with fixtures and polymorphic models: + +1. **Always use natural keys** when creating fixtures with :django-admin:`dumpdata`: + + .. code-block:: bash + + python manage.py dumpdata myapp --natural-foreign --natural-primary > fixture.json + + This ensures ``polymorphic_ctype`` references are portable across databases. + +2. **Check for raw=True** in signal handlers to avoid accessing incomplete data. + +3. **Use post_migrate** for post-fixture setup tasks rather than ``post_save``. + +4. **Verify polymorphic_ctype** after loading fixtures if needed: + + .. code-block:: python + + from polymorphic.utils import reset_polymorphic_ctype + from myapp.models import Endpoint, Switch + + # After loaddata, ensure ctype is correct + reset_polymorphic_ctype(Endpoint, Switch) + +For more details, see `issue #502 `_. + Using Third Party Models (without modifying them) ------------------------------------------------- @@ -329,6 +445,8 @@ Restrictions & Caveats (or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`), include the :option:`--natural-primary ` and :option:`--natural-foreign ` flags in the arguments. + See :ref:`Working with Signals and Fixtures` for more details on using fixtures with + polymorphic models. * If the ``polymorphic_ctype_id`` on the base table points to the wrong :class:`~django.contrib.contenttypes.models.ContentType` (this can happen if you delete child diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 893e0e21..536a0a53 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -105,6 +105,9 @@ works fully automatic and just delivers the expected results. :option:`--natural-foreign ` flag in the arguments. This makes sure the :class:`~django.contrib.contenttypes.models.ContentType` models will be referenced by name instead of their primary key as that changes between Django instances. + + If you're using ``post_save`` signals with polymorphic models, see + :ref:`advanced-features` for important information about handling signals during fixture loading. .. note::