Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/admin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,83 @@ Child model types can be filtered by adding a
:attr:`~django.contrib.admin.ModelAdmin.list_filter` attribute. See the example above.


Using Raw ID Fields with Polymorphic Models
--------------------------------------------

When using :attr:`~django.contrib.admin.ModelAdmin.raw_id_fields` for foreign keys or many-to-many
fields that point to polymorphic models, the popup change list normally shows **all** child types.
This can be confusing when a field on a specific child model should only reference instances of
another specific child type.

The :class:`~polymorphic.admin.PolymorphicForeignKeyRawIdWidget` automatically filters the popup
change list to show only instances of the correct child model type.

Example
~~~~~~~

Consider polymorphic models where ``ChildA`` has a many-to-many field to ``ChildB``:

.. code-block:: python

from django.db import models
from polymorphic.models import PolymorphicModel

class ParentModel(PolymorphicModel):
name = models.CharField(max_length=100)

class ChildA(ParentModel):
related_children = models.ManyToManyField('ChildB', related_name='related_to_a')

class ChildB(ParentModel):
description = models.TextField()

To use the filtered raw ID widget in the admin:

.. code-block:: python

from django.contrib import admin
from polymorphic.admin import (
PolymorphicParentModelAdmin,
PolymorphicChildModelAdmin,
PolymorphicForeignKeyRawIdWidget,
)
from .models import ParentModel, ChildA, ChildB

@admin.register(ChildA)
class ChildAAdmin(PolymorphicChildModelAdmin):
base_model = ParentModel
raw_id_fields = ['related_children']

def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name in self.raw_id_fields:
kwargs['widget'] = PolymorphicForeignKeyRawIdWidget(
db_field.remote_field, self.admin_site
)
return super().formfield_for_manytomany(db_field, request, **kwargs)

@admin.register(ChildB)
class ChildBAdmin(PolymorphicChildModelAdmin):
base_model = ParentModel

@admin.register(ParentModel)
class ParentModelAdmin(PolymorphicParentModelAdmin):
base_model = ParentModel
child_models = (ChildA, ChildB)

Now when editing a ``ChildA`` instance, clicking the magnifying glass icon next to the
``related_children`` field will open a popup showing only ``ChildB`` instances, not all child types.

How it works
~~~~~~~~~~~~

The :class:`~polymorphic.admin.PolymorphicForeignKeyRawIdWidget` adds a ``polymorphic_ctype``
parameter to the popup URL, which is automatically detected by
:class:`~polymorphic.admin.PolymorphicParentModelAdmin` to filter the queryset by content type.

This feature is backward compatible - existing raw ID fields without the custom widget will continue
to show all child types as before.


Inline models
-------------

Expand Down
2 changes: 2 additions & 0 deletions src/polymorphic/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Utils
from .forms import PolymorphicModelChoiceForm
from .widgets import PolymorphicForeignKeyRawIdWidget

# Expose generic admin features too. There is no need to split those
# as the admin already relies on contenttypes.
Expand All @@ -31,6 +32,7 @@
"PolymorphicChildModelAdmin",
"PolymorphicModelChoiceForm",
"PolymorphicChildModelFilter",
"PolymorphicForeignKeyRawIdWidget",
"PolymorphicInlineAdminForm",
"PolymorphicInlineAdminFormSet",
"PolymorphicInlineSupportMixin",
Expand Down
10 changes: 10 additions & 0 deletions src/polymorphic/admin/parentadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ def get_queryset(self, request):
qs = super().get_queryset(request)
if not self.polymorphic_list:
qs = qs.non_polymorphic()

# Filter by content type if specified (for raw ID widget popups)
ctype_id = request.GET.get("polymorphic_ctype")
if ctype_id:
try:
qs = qs.filter(polymorphic_ctype_id=int(ctype_id))
except (ValueError, TypeError):
# Invalid ctype_id, ignore the filter
pass

return qs

def add_view(self, request, form_url="", extra_context=None):
Expand Down
72 changes: 72 additions & 0 deletions src/polymorphic/admin/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Widgets for polymorphic admin.
"""

from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.contrib.contenttypes.models import ContentType


class PolymorphicForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
"""
A raw ID widget that automatically filters the popup change list by content type.

When used with polymorphic models, this widget adds a 'polymorphic_ctype' parameter
to the popup URL, which filters the queryset to show only instances of the specific
child model type.

Example usage in a child admin::

from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicForeignKeyRawIdWidget

class ChildAAdmin(PolymorphicChildModelAdmin):
base_model = ParentModel
raw_id_fields = ['related_child_b']

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name in self.raw_id_fields:
kwargs['widget'] = PolymorphicForeignKeyRawIdWidget(
db_field.remote_field, self.admin_site
)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
"""

def url_parameters(self):
"""
Add polymorphic_ctype parameter to the popup URL if the related model is polymorphic.
"""
# Handle None rel case before calling super
if not self.rel:
return {}

params = super().url_parameters()

# Get the content type for the related model
ctype_id = self._get_polymorphic_ctype()
if ctype_id:
params["polymorphic_ctype"] = ctype_id

return params

def _get_polymorphic_ctype(self):
"""
Get the content type ID for the related model if it's polymorphic.

Returns:
int or None: The content type ID if the model is polymorphic, None otherwise.
"""
if not self.rel:
return None

related_model = self.rel.model

# Check if the model has polymorphic_ctype field (indicating it's polymorphic)
if not hasattr(related_model, "polymorphic_ctype"):
return None

# Get the content type for this specific model
try:
ctype = ContentType.objects.get_for_model(related_model, for_concrete_model=False)
return ctype.id
except Exception:
# If anything goes wrong, just don't add the parameter
return None
Loading
Loading