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
95 changes: 94 additions & 1 deletion src/polymorphic/tests/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from inspect import isclass
from django.contrib.admin import register, ModelAdmin, site as admin_site
from django.contrib.admin import register, ModelAdmin, TabularInline, site as admin_site
from django.db.models.query import QuerySet
from django.http import HttpRequest
from polymorphic.admin import (
Expand All @@ -25,6 +25,15 @@
M2MAdminTestChildA,
M2MAdminTestChildB,
M2MAdminTestChildC,
M2MThroughBase,
M2MThroughProject,
M2MThroughPerson,
M2MThroughMembership,
M2MThroughMembershipWithPerson,
M2MThroughMembershipWithSpecialPerson,
M2MThroughProjectWithTeam,
M2MThroughSpecialPerson,
DirectM2MContainer,
)


Expand Down Expand Up @@ -104,3 +113,87 @@ class M2MAdminTestChildB(PolymorphicChildModelAdmin):
@register(M2MAdminTestChildC)
class M2MAdminTestChildC(PolymorphicChildModelAdmin):
raw_id_fields = ("child_as",)


# Issue #182: M2M field in model admin
# Register models to test M2M field to polymorphic model
@register(M2MThroughBase)
class M2MThroughBaseAdmin(PolymorphicParentModelAdmin):
"""Base admin for polymorphic M2M test models."""

child_models = (
M2MThroughProject,
M2MThroughPerson,
M2MThroughProjectWithTeam,
M2MThroughSpecialPerson,
)


@register(M2MThroughProject)
class M2MThroughProjectAdmin(PolymorphicChildModelAdmin):
"""Admin for M2MThroughProject polymorphic child."""

pass


@register(M2MThroughPerson)
class M2MThroughPersonAdmin(PolymorphicChildModelAdmin):
"""Admin for M2MThroughPerson polymorphic child."""

pass


@register(M2MThroughSpecialPerson)
class M2MThroughSpecialPersonAdmin(PolymorphicChildModelAdmin):
"""Admin for M2MThroughSpecialPerson polymorphic child."""

pass


@register(DirectM2MContainer)
class DirectM2MContainerAdmin(ModelAdmin):
"""
Test case for Issue #182: M2M field in model admin.
DirectM2MContainer has a direct M2M field to polymorphic M2MThroughBase model.
This should work without AttributeError: 'int' object has no attribute 'pk'.
"""

filter_horizontal = ("items",)


# Issue #375: Admin with M2M through table on polymorphic model
class M2MThroughMembershipInline(StackedPolymorphicInline):
"""
Polymorphic inline for Issue #375: M2M through table with polymorphic membership types.
This tests creating different membership types inline based on person type.
"""

model = M2MThroughMembership
extra = 1

class MembershipWithPersonChild(StackedPolymorphicInline.Child):
"""Inline for regular Person membership."""

model = M2MThroughMembershipWithPerson

class MembershipWithSpecialPersonChild(StackedPolymorphicInline.Child):
"""Inline for SpecialPerson membership with special notes."""

model = M2MThroughMembershipWithSpecialPerson

child_inlines = (
MembershipWithPersonChild,
MembershipWithSpecialPersonChild,
)


@register(M2MThroughProjectWithTeam)
class M2MThroughProjectWithTeamAdmin(PolymorphicInlineSupportMixin, PolymorphicChildModelAdmin):
"""
Test case for Issue #375: Admin with M2M through table on polymorphic model.
M2MThroughProjectWithTeam (polymorphic) has M2M to M2MThroughPerson (polymorphic)
with custom through model M2MThroughMembership (now polymorphic).
Uses polymorphic inlines to support different membership types.
"""

inlines = (M2MThroughMembershipInline,)
4 changes: 2 additions & 2 deletions 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-07 11:43
# Generated by Django 4.2 on 2026-01-07 13:29

from decimal import Decimal
from django.conf import settings
Expand All @@ -12,8 +12,8 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
Expand Down
116 changes: 115 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-07 11:43
# Generated by Django 4.2 on 2026-01-07 13:29

from django.conf import settings
from django.db import migrations, models
Expand Down Expand Up @@ -205,6 +205,31 @@ class Migration(migrations.Migration):
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='M2MThroughBase',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('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='M2MThroughMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(max_length=50)),
('joined_date', models.DateField(auto_now_add=True)),
('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 @@ -614,6 +639,53 @@ class Migration(migrations.Migration):
},
bases=('tests.m2madmintest',),
),
migrations.CreateModel(
name='M2MThroughMembershipWithPerson',
fields=[
('m2mthroughmembership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughmembership')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughmembership',),
),
migrations.CreateModel(
name='M2MThroughMembershipWithSpecialPerson',
fields=[
('m2mthroughmembership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughmembership')),
('special_notes', models.TextField(blank=True, default='')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughmembership',),
),
migrations.CreateModel(
name='M2MThroughPerson',
fields=[
('m2mthroughbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughbase')),
('email', models.EmailField(blank=True, max_length=254)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughbase',),
),
migrations.CreateModel(
name='M2MThroughProject',
fields=[
('m2mthroughbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughbase')),
('description', models.TextField(blank=True)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughbase',),
),
migrations.CreateModel(
name='Middle',
fields=[
Expand Down Expand Up @@ -1267,6 +1339,14 @@ class Migration(migrations.Migration):
name='lake',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.lakewiththrough'),
),
migrations.CreateModel(
name='DirectM2MContainer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('items', models.ManyToManyField(blank=True, related_name='containers', to='tests.m2mthroughbase')),
],
),
migrations.AddField(
model_name='derivedmanagertest',
name='related_test',
Expand Down Expand Up @@ -1464,6 +1544,18 @@ class Migration(migrations.Migration):
},
bases=('tests.m2madmintestchildb',),
),
migrations.CreateModel(
name='M2MThroughSpecialPerson',
fields=[
('m2mthroughperson_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughperson')),
('special_code', models.CharField(blank=True, max_length=20)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughperson',),
),
migrations.CreateModel(
name='Model2C',
fields=[
Expand Down Expand Up @@ -1607,6 +1699,11 @@ class Migration(migrations.Migration):
('user_profiles', models.ManyToManyField(related_name='user_teams', to='tests.userprofile')),
],
),
migrations.AddField(
model_name='m2mthroughmembership',
name='person',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.m2mthroughperson'),
),
migrations.AddField(
model_name='m2madmintestchilda',
name='child_bs',
Expand Down Expand Up @@ -1686,6 +1783,23 @@ class Migration(migrations.Migration):
},
bases=('tests.uuidartprojecta',),
),
migrations.CreateModel(
name='M2MThroughProjectWithTeam',
fields=[
('m2mthroughproject_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tests.m2mthroughproject')),
('team', models.ManyToManyField(blank=True, related_name='projects', through='tests.M2MThroughMembership', to='tests.m2mthroughperson')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('tests.m2mthroughproject',),
),
migrations.AddField(
model_name='m2mthroughmembership',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.m2mthroughprojectwithteam'),
),
migrations.CreateModel(
name='UUIDArtProjectC',
fields=[
Expand Down
81 changes: 81 additions & 0 deletions src/polymorphic/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,3 +907,84 @@ class M2MAdminTestChildB(M2MAdminTest):

class M2MAdminTestChildC(M2MAdminTestChildB):
pass


# Models for testing Issue #182 and #375: M2M with through tables to/from polymorphic models
class M2MThroughBase(PolymorphicModel):
"""Base polymorphic model for M2M through table tests."""

name = models.CharField(max_length=50)

def __str__(self):
return self.name


class M2MThroughPerson(M2MThroughBase):
"""Polymorphic child representing a person who can be on teams."""

email = models.EmailField(blank=True)


class M2MThroughSpecialPerson(M2MThroughPerson):
"""Polymorphic child representing a special person."""

special_code = models.CharField(max_length=20, blank=True)


class M2MThroughProject(M2MThroughBase):
"""Polymorphic child representing a project."""

description = models.TextField(blank=True)


class M2MThroughProjectWithTeam(M2MThroughProject):
"""
Polymorphic child with M2M to Person through Membership.
Tests Issue #375: M2M with through table on polymorphic model.
"""

pass


class M2MThroughMembership(PolymorphicModel):
"""Polymorphic through model for M2M relationship between ProjectWithTeam and Person."""

project = models.ForeignKey("M2MThroughProjectWithTeam", on_delete=models.CASCADE)
person = models.ForeignKey(M2MThroughPerson, on_delete=models.CASCADE)
role = models.CharField(max_length=50)
joined_date = models.DateField(auto_now_add=True)

def __str__(self):
return f"{self.person.name} - {self.role} on {self.project.name}"


class M2MThroughMembershipWithPerson(M2MThroughMembership):
"""Membership for regular Person instances."""

pass


class M2MThroughMembershipWithSpecialPerson(M2MThroughMembership):
"""Membership for SpecialPerson instances with additional tracking."""

special_notes = models.TextField(blank=True, default="")


# Add the M2M field after the through model is defined
M2MThroughProjectWithTeam.add_to_class(
"team",
models.ManyToManyField(
M2MThroughPerson, through=M2MThroughMembership, related_name="projects", blank=True
),
)


# Additional models for Issue #182: Direct M2M to polymorphic model
class DirectM2MContainer(models.Model):
"""Non-polymorphic model with direct M2M to polymorphic model."""

name = models.CharField(max_length=50)
items = models.ManyToManyField(M2MThroughBase, related_name="containers", blank=True)

def __str__(self):
return self.name
Loading
Loading