Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle subclasses of PolymorphicModel being abstract #228

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
58 changes: 48 additions & 10 deletions nested_admin/polymorphic.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,52 @@ def get_base_polymorphic_models(child_model):
return models


def get_child_polymorphic_models(model):
models = []
for m in model.__subclasses__():
if (isinstance(model, PolymorphicModelBase)
and model is not PolymorphicModel
and not model._meta.abstract):
models.append(model)
return models
def get_all_subclasses(python_class):
"""
Helper function to get all the subclasses of a class.

Taken from: https://gist.github.com/pzrq/460424c9382dd50d02b8
:param python_class: Any Python class that implements __subclasses__()
"""
python_class.__subclasses__()
subclasses = set()
check_these = [python_class]
while check_these:
parent = check_these.pop()
for child in parent.__subclasses__():
if child not in subclasses:
subclasses.add(child)
check_these.append(child)
return subclasses


def get_child_concrete_polymorphic_models(base_model):
"""
Helper function to get all concrete models
that are subclasses of base_model
in sorted order by name.

Taken and modified from: https://gist.github.com/pzrq/460424c9382dd50d02b8

:param base_model: A Django models.Model instance.
"""
found = get_all_subclasses(base_model)
def filter_func(model):
meta = getattr(model, '_meta', '')
if getattr(meta, 'abstract', True):
# Skip meta classes
return False
if not isinstance(model, PolymorphicModelBase):
return False
if model is PolymorphicModel:
return False
if '_Deferred_' in model.__name__:
# See deferred_class_factory() in django.db.models.query_utils
# Catches when you do .only('attr') on a queryset
return False
return True
subclasses = list(filter(filter_func, found))
return subclasses


def get_polymorphic_related_models(model):
Expand Down Expand Up @@ -79,10 +117,10 @@ def inline_formset_data(self):
formset_fk_model = ''
parent_models = []
compatible_parents = get_compatible_parents(self.formset.model)
sub_models = self.formset.model()._get_inheritance_relation_fields_and_models()
sub_models = get_child_concrete_polymorphic_models(self.formset.model)
data['nestedOptions'].update({
'parentModel': get_model_id(formset_fk_model),
'childModels': [get_model_id(m) for m in sub_models.values()],
'childModels': [get_model_id(m) for m in sub_models],
'parentModels': [get_model_id(m) for m in parent_models],
'compatibleParents': {
get_model_id(k): [get_model_id(m) for m in v]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.contrib import admin
from django.db import models
from django import forms

import nested_admin

from .models import (
FreeTextBlock, PollBlock, QuestionBlock, Survey, SurveyBlock, )


class SurveyBlockInline(nested_admin.NestedStackedPolymorphicInline):
class QuestionInline(nested_admin.NestedStackedPolymorphicInline.Child):
model = QuestionBlock
sortable_field_name = "position"
formfield_overrides = {
models.PositiveSmallIntegerField: {'widget': forms.HiddenInput},
}

class FreeTextInline(nested_admin.NestedStackedPolymorphicInline.Child):
model = FreeTextBlock
sortable_field_name = "position"
formfield_overrides = {
models.PositiveSmallIntegerField: {'widget': forms.HiddenInput},
}

class PollInline(nested_admin.NestedStackedPolymorphicInline.Child):
model = PollBlock
sortable_field_name = "position"
formfield_overrides = {
models.PositiveSmallIntegerField: {'widget': forms.HiddenInput},
}

model = SurveyBlock
extra = 0
sortable_field_name = "position"
child_inlines = (FreeTextInline, PollInline, QuestionInline)
formfield_overrides = {
models.PositiveSmallIntegerField: {'widget': forms.HiddenInput},
}


@admin.register(Survey)
class SurveyAdmin(nested_admin.NestedPolymorphicModelAdmin):
inlines = (SurveyBlockInline,)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import django
from django.db import models
from django.db.models import ForeignKey, CASCADE

try:
from polymorphic.models import PolymorphicModel
except:
# Temporary until django-polymorphic supports django 3.1
if django.VERSION < (3, 1):
raise
else:
PolymorphicModel = models.Model


def NON_POLYMORPHIC_CASCADE(collector, field, sub_objs, using):
return CASCADE(collector, field, sub_objs.non_polymorphic(), using)


class Survey(models.Model):
title = models.CharField(max_length=255)


class SurveyBlock(PolymorphicModel):
survey = ForeignKey(Survey, related_name='blocks', on_delete=NON_POLYMORPHIC_CASCADE)
position = models.PositiveSmallIntegerField(null=True)

class Meta:
ordering = ["position"]


class BaseQuestionBlock(SurveyBlock):
label = models.CharField(max_length=255)

class Meta:
abstract = True


class QuestionBlock(BaseQuestionBlock):
is_required = models.BooleanField()


class FreeTextBlock(BaseQuestionBlock):
pass


class PollBlock(BaseQuestionBlock):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from unittest import SkipTest
from django.test import TestCase

from .models import (
FreeTextBlock, PollBlock, QuestionBlock, Survey, SurveyBlock, )


try:
from nested_admin.tests.nested_polymorphic.base import BaseNestedPolymorphicTestCase
except ImportError:
BaseNestedPolymorphicTestCase = TestCase
has_polymorphic = False
else:
has_polymorphic = True


class PolymorphicAbstractClassesTestCase(BaseNestedPolymorphicTestCase):
root_model = Survey
nested_models = (SurveyBlock, )

@classmethod
def setUpClass(cls):
if not has_polymorphic:
raise SkipTest('django-polymorphic unavailable')
super(PolymorphicAbstractClassesTestCase, cls).setUpClass()

def test_add_level_one_to_empty(self):
obj = self.root_model.objects.create(title='test')
self.load_admin(obj)
self.add_inline(model=QuestionBlock, label='x')
self.save_form()

blocks = obj.blocks.all()
self.assertEqual(len(blocks), 1)
self.assertIsInstance(blocks[0], QuestionBlock)
self.assertEqual(blocks[0].label, 'x')
self.assertEqual(blocks[0].is_required, False)

def test_can_move_children_of_abstract(self):
obj = self.root_model.objects.create(title='test')
self.load_admin(obj)
self.add_inline(model=PollBlock, label='poll')
self.add_inline(model=QuestionBlock, label='question')
self.add_inline(model=FreeTextBlock, label='free')
self.save_form()

blocks = obj.blocks.all()
self.assertEqual(len(blocks), 3)
self.assertIsInstance(blocks[0], PollBlock)
self.assertEqual(blocks[0].label, 'poll')
self.assertEqual(blocks[0].position, 0)
self.assertIsInstance(blocks[1], QuestionBlock)
self.assertEqual(blocks[1].label, 'question')
self.assertEqual(blocks[1].position, 1)
self.assertEqual(blocks[1].is_required, False)
self.assertIsInstance(blocks[2], FreeTextBlock)
self.assertEqual(blocks[2].label, 'free')
self.assertEqual(blocks[2].position, 2)

# Move question block to top
self.drag_and_drop_item([1], [0], screenshot_hack=True)

self.save_form()

blocks = obj.blocks.all()
self.assertEqual(len(blocks), 3)
self.assertIsInstance(blocks[0], QuestionBlock)
self.assertEqual(blocks[0].label, 'question')
self.assertEqual(blocks[0].position, 0)
self.assertEqual(blocks[0].is_required, False)
self.assertIsInstance(blocks[1], PollBlock)
self.assertEqual(blocks[1].label, 'poll')
self.assertEqual(blocks[1].position, 1)
self.assertIsInstance(blocks[2], FreeTextBlock)
self.assertEqual(blocks[2].label, 'free')
self.assertEqual(blocks[2].position, 2)