From 8573632a33584e639a1b46cb5e9a1faf112e3361 Mon Sep 17 00:00:00 2001 From: Trent Holliday Date: Tue, 27 Sep 2022 10:25:48 -0400 Subject: [PATCH 1/3] Updating logic to pull correct child models --- nested_admin/polymorphic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nested_admin/polymorphic.py b/nested_admin/polymorphic.py index 8eb8b2e..5724453 100644 --- a/nested_admin/polymorphic.py +++ b/nested_admin/polymorphic.py @@ -79,10 +79,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_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] From fa7890c2b4e348af2187f2874ae298f5735bb717 Mon Sep 17 00:00:00 2001 From: Trent Holliday Date: Tue, 27 Sep 2022 11:21:43 -0400 Subject: [PATCH 2/3] Created new function to find all polymorphic child classes regardless of depth --- nested_admin/polymorphic.py | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/nested_admin/polymorphic.py b/nested_admin/polymorphic.py index 5724453..1cd6703 100644 --- a/nested_admin/polymorphic.py +++ b/nested_admin/polymorphic.py @@ -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): @@ -79,7 +117,7 @@ def inline_formset_data(self): formset_fk_model = '' parent_models = [] compatible_parents = get_compatible_parents(self.formset.model) - sub_models = get_child_polymorphic_models(self.formset.model) + 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], From 4a886da819be4855f5399931d483666faee16536 Mon Sep 17 00:00:00 2001 From: Trent Holliday Date: Tue, 27 Sep 2022 16:34:00 -0400 Subject: [PATCH 3/3] Added new tests to verify that abstract child models are handled correctly --- .../__init__.py | 0 .../admin.py | 44 +++++++++++ .../models.py | 47 ++++++++++++ .../tests.py | 76 +++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/__init__.py create mode 100644 nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/admin.py create mode 100644 nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/models.py create mode 100644 nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/tests.py diff --git a/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/__init__.py b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/admin.py b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/admin.py new file mode 100644 index 0000000..aabf9e1 --- /dev/null +++ b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/admin.py @@ -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,) diff --git a/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/models.py b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/models.py new file mode 100644 index 0000000..968bd3f --- /dev/null +++ b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/models.py @@ -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 diff --git a/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/tests.py b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/tests.py new file mode 100644 index 0000000..8a30903 --- /dev/null +++ b/nested_admin/tests/nested_polymorphic/test_polymorphic_abstract_classes/tests.py @@ -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)