Skip to content
Merged
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
25 changes: 25 additions & 0 deletions src/polymorphic/tests/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
InlineParent,
NoChildren,
ModelWithPolyFK,
M2MAdminTest,
M2MAdminTestChildA,
M2MAdminTestChildB,
M2MAdminTestChildC,
)


Expand Down Expand Up @@ -79,3 +83,24 @@ class NoChildrenAdmin(PolymorphicParentModelAdmin):
@register(ModelWithPolyFK)
class ModelWithPolyFKAdmin(ModelAdmin):
fields = ["name", "poly_fk"]


@register(M2MAdminTest)
class M2MAdminTestAdmin(PolymorphicParentModelAdmin):
list_filter = (PolymorphicChildModelFilter,)
child_models = (M2MAdminTestChildA, M2MAdminTestChildB, M2MAdminTestChildC)


@register(M2MAdminTestChildA)
class M2MAdminTestChildA(PolymorphicChildModelAdmin):
raw_id_fields = ("child_bs",)


@register(M2MAdminTestChildB)
class M2MAdminTestChildB(PolymorphicChildModelAdmin):
raw_id_fields = ("child_as",)


@register(M2MAdminTestChildC)
class M2MAdminTestChildC(PolymorphicChildModelAdmin):
raw_id_fields = ("child_as",)
2 changes: 1 addition & 1 deletion src/polymorphic/tests/deletion/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-06 23:40
# Generated by Django 4.2 on 2026-01-07 11:43

from decimal import Decimal
from django.conf import settings
Expand Down
53 changes: 52 additions & 1 deletion src/polymorphic/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-06 23:40
# Generated by Django 4.2 on 2026-01-07 11:43

from django.conf import settings
from django.db import migrations, models
Expand Down Expand Up @@ -193,6 +193,18 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=30)),
],
),
migrations.CreateModel(
name='M2MAdminTest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30)),
('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='Model2A',
fields=[
Expand Down Expand Up @@ -579,6 +591,29 @@ class Migration(migrations.Migration):
},
bases=('tests.inittestmodel',),
),
migrations.CreateModel(
name='M2MAdminTestChildA',
fields=[
('m2madmintest_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintest')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2madmintest',),
),
migrations.CreateModel(
name='M2MAdminTestChildB',
fields=[
('m2madmintest_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintest')),
('child_as', models.ManyToManyField(blank=True, related_name='related_bs', to='tests.m2madmintestchilda')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2madmintest',),
),
migrations.CreateModel(
name='Middle',
fields=[
Expand Down Expand Up @@ -1418,6 +1453,17 @@ class Migration(migrations.Migration):
},
bases=('tests.disparatekeyschild2',),
),
migrations.CreateModel(
name='M2MAdminTestChildC',
fields=[
('m2madmintestchildb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2madmintestchildb')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2madmintestchildb',),
),
migrations.CreateModel(
name='Model2C',
fields=[
Expand Down Expand Up @@ -1561,6 +1607,11 @@ class Migration(migrations.Migration):
('user_profiles', models.ManyToManyField(related_name='user_teams', to='tests.userprofile')),
],
),
migrations.AddField(
model_name='m2madmintestchilda',
name='child_bs',
field=models.ManyToManyField(blank=True, related_name='related_as', to='tests.m2madmintestchildb'),
),
migrations.CreateModel(
name='InlineModelB',
fields=[
Expand Down
19 changes: 19 additions & 0 deletions src/polymorphic/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,3 +888,22 @@ class DisparateKeysGrandChild2(DisparateKeysChild2):

class DisparateKeysGrandChild(DisparateKeysChild1):
text_grand_child = models.CharField(max_length=30)


class M2MAdminTest(PolymorphicModel):
name = models.CharField(max_length=30)

def __str__(self):
return self.name


class M2MAdminTestChildA(M2MAdminTest):
child_bs = models.ManyToManyField("M2MAdminTestChildB", related_name="related_as", blank=True)


class M2MAdminTestChildB(M2MAdminTest):
child_as = models.ManyToManyField("M2MAdminTestChildA", related_name="related_bs", blank=True)


class M2MAdminTestChildC(M2MAdminTestChildB):
pass
144 changes: 144 additions & 0 deletions src/polymorphic/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,3 +898,147 @@ def test_changelist_filter_persists_after_edit(self):
]
# Only obj_b should be displayed (obj_c has a different content type)
assert displayed_ids == [obj_b.pk]


class M2MAdminTests(_GenericAdminFormTest):
def test_m2m_admin_raw_id_fields(self):
"""
Test M2M relationships in polymorphic admin using raw_id_fields.

This test verifies that:
1. M2M relationships can be created between polymorphic child models
2. Raw ID field lookups display the correct polymorphic instances
3. M2M relationships are properly saved and displayed
"""
from polymorphic.tests.models import (
M2MAdminTestChildA,
M2MAdminTestChildB,
M2MAdminTestChildC,
)

# Create test instances
a1 = M2MAdminTestChildA.objects.create(name="A1")
b1 = M2MAdminTestChildB.objects.create(name="B1")
c1 = M2MAdminTestChildC.objects.create(name="C1")

# Navigate to A1's change page
self.page.goto(self.change_url(M2MAdminTestChildA, a1.pk))

# Verify the page loaded correctly
assert self.page.locator("input[name='name']").input_value() == "A1"

# Test adding B1 to A1's child_bs field using the raw ID lookup
# Click the lookup button (magnifying glass icon) for child_bs
with self.page.expect_popup(timeout=10000) as popup_info:
self.page.click("a#lookup_id_child_bs")

popup = popup_info.value
popup.wait_for_load_state("networkidle")

# In the popup, we should see both B1 and C1 (since C1 is a subclass of B)
# Verify B1 is present in the list
b1_link = popup.locator("table#result_list a:has-text('B1')")
expect(b1_link).to_be_visible()

# Verify C1 is present in the list
c1_link = popup.locator("table#result_list a:has-text('C1')")
expect(c1_link).to_be_visible()

# Verify that A1 is not present
expect(popup.locator("table#result_list a:has-text('A1')")).to_have_count(0)

# Click B1 to select it
with popup.expect_event("close", timeout=10000):
b1_link.click()

# Wait a moment for the popup to close and value to be set
self.page.wait_for_timeout(500)

# Verify B1's ID was added to the raw ID field
child_bs_value = self.page.locator("input[name='child_bs']").input_value()
assert str(b1.pk) in child_bs_value

# Now add C1 as well by clicking the lookup again
with self.page.expect_popup(timeout=10000) as popup_info:
self.page.click("a#lookup_id_child_bs")

popup = popup_info.value
popup.wait_for_load_state("networkidle")

# Click C1 to add it
c1_link = popup.locator("table#result_list a:has-text('C1')")
with popup.expect_event("close", timeout=10000):
c1_link.click()

self.page.wait_for_timeout(500)

# Verify both B1 and C1 are in the raw ID field (comma-separated)
child_bs_value = self.page.locator("input[name='child_bs']").input_value()
assert str(b1.pk) in child_bs_value
assert str(c1.pk) in child_bs_value

# Save the changes to A1
with self.page.expect_navigation(timeout=10000) as nav_info:
self.page.click("input[name='_save']")

response = nav_info.value
assert response.status < 400

# Verify the relationships were saved
a1.refresh_from_db()
child_bs_ids = set(a1.child_bs.values_list("pk", flat=True))
assert b1.pk in child_bs_ids
assert c1.pk in child_bs_ids
assert len(child_bs_ids) == 2

# Now test the reverse relationship: add A1 to B1's child_as
self.page.goto(self.change_url(M2MAdminTestChildB, b1.pk))

# Verify the page loaded correctly
assert self.page.locator("input[name='name']").input_value() == "B1"

# Click the lookup button for child_as
with self.page.expect_popup(timeout=10000) as popup_info:
self.page.click("a#lookup_id_child_as")

popup = popup_info.value
popup.wait_for_load_state("networkidle")

# In the popup, we should see A1
a1_link = popup.locator("table#result_list a:has-text('A1')")
expect(a1_link).to_be_visible()

# Verify that AB is not present
expect(popup.locator("table#result_list a:has-text('B1')")).to_have_count(0)
expect(popup.locator("table#result_list a:has-text('C1')")).to_have_count(0)

# Click A1 to select it
with popup.expect_event("close", timeout=10000):
a1_link.click()

self.page.wait_for_timeout(500)

# Verify A1's ID was added to the raw ID field
child_as_value = self.page.locator("input[name='child_as']").input_value()
assert str(a1.pk) in child_as_value

# Save the changes to B1
with self.page.expect_navigation(timeout=10000) as nav_info:
self.page.click("input[name='_save']")

response = nav_info.value
assert response.status < 400

# Verify the relationship was saved
b1.refresh_from_db()
child_as_ids = set(b1.child_as.values_list("pk", flat=True))
assert a1.pk in child_as_ids
assert len(child_as_ids) == 1

# Verify the relationships display correctly when we go back to the change page
self.page.goto(self.change_url(M2MAdminTestChildA, a1.pk))

# The raw ID field should show both B1 and C1
child_bs_value = self.page.locator("input[name='child_bs']").input_value()
assert str(b1.pk) in child_bs_value
assert str(c1.pk) in child_bs_value
Loading