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

On add new nested polymorphic inline it presents all child inlines #138

Closed
pod2metra opened this issue Apr 15, 2019 · 11 comments
Closed

On add new nested polymorphic inline it presents all child inlines #138

pod2metra opened this issue Apr 15, 2019 · 11 comments

Comments

@pod2metra
Copy link

So,
When I try to add any type of child model it will present all nested inlines that presented to any child. It looks like that this is JS/render template problem cause after save on new rendered models it presents only that inline that is really needed.

@pod2metra
Copy link
Author

@fdintino
Copy link
Member

Could you provide an example admin.py and models.py that exhibits the issue?

@arthurio
Copy link

We have the same issue:

models.py

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


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


class Question(PolymorphicModel):
    survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
    position = models.PositiveSmallIntegerField("Position", null=True)

    class Meta:
        ordering = ["position"]


class FreeText(Question):
    pass


class Poll(Question):
    pass


class QuestionComponent(PolymorphicModel):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    position = models.PositiveSmallIntegerField("Position", null=True)

    class Meta:
        ordering = ["position"]


class Text(QuestionComponent):
    value = models.TextField()


class Textarea(QuestionComponent):
    pass


class RadioGroup(QuestionComponent):
    pass


class Radio(models.Model):
    radio_group = models.ForeignKey(RadioGroup, on_delete=models.CASCADE)
    label = models.CharField(max_length=255)
    value = models.CharField(max_length=255)
    position = models.PositiveSmallIntegerField("Position", null=True)

admin.py

from django.contrib import admin

import nested_admin

from .models import FreeText
from .models import Poll
from .models import Question
from .models import Radio
from .models import RadioGroup
from .models import Survey
from .models import Text
from .models import Textarea


class RadioInline(nested_admin.NestedTabularInline):
    model = Radio
    sortable_field_name = "position"
    extra = 0
    min_num = 1
    max_num = 8


class TextInline(nested_admin.NestedTabularInline):
    model = Text
    extra = 1
    min_num = 1
    max_num = 1


class TextareaInline(nested_admin.NestedTabularInline):
    model = Textarea
    extra = 1
    min_num = 1
    max_num = 1


class RadioGroupInline(nested_admin.NestedTabularInline):
    model = RadioGroup
    inlines = (RadioInline,)
    extra = 0
    min_num = 1
    max_num = 1


class QuestionInline(nested_admin.NestedStackedPolymorphicInline):
    class FreeTextInline(nested_admin.NestedStackedPolymorphicInline.Child):
        model = FreeText
        inlines = (TextInline, TextareaInline,)

    class PollInline(nested_admin.NestedStackedPolymorphicInline.Child):
        model = Poll
        inlines = (TextInline, RadioGroupInline,)

    model = Question
    extra = 0
    sortable_field_name = "position"
    child_inlines = (FreeTextInline, PollInline,)


@admin.register(Survey)
class SurveyAdmin(nested_admin.NestedPolymorphicModelAdmin):
    inlines = (QuestionInline,)

Expected:
The radiogroup should show only for poll questions and textarea only for freetext

Actual:
Everything show up

fydlfT2jey

@fdintino
Copy link
Member

Thank you for that example. I see the issue now, it might take me a bit to figure out the best way to resolve it.

@fdintino
Copy link
Member

@arthurio @pod2metra could you try the branch polymorphic-fixes locally and see if it resolves your issues?

@arthurio
Copy link

@fdintino Thanks for the quick turn around! It works in term of UI (i.e. only the proper elements are displayed), but there seems to be a bug with the order in which components are created/deleted. Or it could be something where you need to create/delete the PolymorphicModel using specific queryset methods so that the related parent model is created/deleted properly.

Here are two different scenarii:

Delete

Deleting the QuestionComponent first, then the Question works, but deleting the Question directly fails with as you can see below (video):

text

Traceback (most recent call last):
  File "***/lib/python3.7/site-packages/django/contrib/staticfiles/handlers.py", line 65, in __call__
    return self.application(environ, start_response)
  File "***/lib/python3.7/site-packages/django/core/handlers/wsgi.py", line 142, in __call__
    response = self.get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 78, in get_response
    response = self._middleware_chain(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 36, in inner
    response = response_for_exception(request, exc)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 90, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 125, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "***/lib/python3.7/site-packages/django_extensions/management/technical_response.py", line 37, in null_technical_500_response
    six.reraise(exc_type, exc_value, tb)
  File "***/lib/python3.7/site-packages/six.py", line 692, in reraise
    raise value.with_traceback(tb)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/sites.py", line 223, in inner
    return view(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1650, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1536, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1576, in _changeform_view
    self.save_related(request, form, formsets, not add)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1122, in save_related
    self.save_formset(request, form, formset, change=change)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1110, in save_formset
    formset.save()
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 143, in save
    instance = self.get_saved_instance_for_form(form, commit, form_instances)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 229, in get_saved_instance_for_form
    instances = self.save_existing_objects([form], commit)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 324, in save_existing_objects
    self.delete_existing(obj, commit=commit)
  File "***/lib/python3.7/site-packages/django/forms/models.py", line 655, in delete_existing
    obj.delete()
  File "***/lib/python3.7/site-packages/django/db/models/base.py", line 878, in delete
    collector.collect([self], keep_parents=keep_parents)
  File "***/lib/python3.7/site-packages/django/db/models/deletion.py", line 201, in collect
    parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
  File "***/lib/python3.7/site-packages/django/db/models/deletion.py", line 201, in <listcomp>
    parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
  File "***/lib/python3.7/site-packages/polymorphic/models.py", line 175, in accessor_function
    attr = model._base_objects.get(pk=self.pk)
  File "***/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/db/models/query.py", line 399, in get
    self.model._meta.object_name
nested_polymorphic.models.QuestionComponent.DoesNotExist: QuestionComponent matching query does not exist.

Create QuestionComponent with related objects (Radio)

Creating a Poll (Question having a RadioGroup as QuestionComponent) fails. It seems that the Radio elements (ForeignKey to RadioGroup) are being created before the QuestionComponent is created

Video: http://recordit.co/8kGnjBTqc2
poll

Traceback (most recent call last):
  File "***/lib/python3.7/site-packages/django/contrib/staticfiles/handlers.py", line 65, in __call__
    return self.application(environ, start_response)
  File "***/lib/python3.7/site-packages/django/core/handlers/wsgi.py", line 142, in __call__
    response = self.get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 78, in get_response
    response = self._middleware_chain(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 36, in inner
    response = response_for_exception(request, exc)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 90, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 125, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "***/lib/python3.7/site-packages/django_extensions/management/technical_response.py", line 37, in null_technical_500_response
    six.reraise(exc_type, exc_value, tb)
  File "***/lib/python3.7/site-packages/six.py", line 692, in reraise
    raise value.with_traceback(tb)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/sites.py", line 223, in inner
    return view(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1650, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1536, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1576, in _changeform_view
    self.save_related(request, form, formsets, not add)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1122, in save_related
    self.save_formset(request, form, formset, change=change)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1110, in save_formset
    formset.save()
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 143, in save
    instance = self.get_saved_instance_for_form(form, commit, form_instances)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 231, in get_saved_instance_for_form
    instances = self.save_new_objects([form], commit)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 367, in save_new_objects
    new_objects.append(self.save_new(form, commit=commit))
  File "***/lib/python3.7/site-packages/django/forms/models.py", line 946, in save_new
    pk_value = getattr(self.instance, self.fk.remote_field.field_name)
  File "***/lib/python3.7/site-packages/polymorphic/models.py", line 175, in accessor_function
    attr = model._base_objects.get(pk=self.pk)
  File "***/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/db/models/query.py", line 399, in get
    self.model._meta.object_name
nested_polymorphic.models.QuestionComponent.DoesNotExist: QuestionComponent matching query does not exist.

I can put what I have locally for testing in a public repo if you want? Or what else could help?
Thanks again for your help!

@pod2metra
Copy link
Author

@fdintino You changes fixed that issue. Thank you.

@fdintino
Copy link
Member

@arthurio thank you again for the detailed example! I was able to use them to create failing test cases, which made fixing them much easier. Could you pull down the latest commits on the polymorphic-fixes branch and verify that it is working now?

One note: if you get an IntegrityError when you test deleting a nested polymorphic inline, that means you're encountering jazzband/django-polymorphic#229. The workaround in this comment appears to address the issue: jazzband/django-polymorphic#229 (comment)

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


class MyModel(PolymorphicModel):
    fk_field = models.ForeignKey(on_delete=NON_POLYMORPHIC_CASCADE)

@arthurio
Copy link

arthurio commented Apr 26, 2019

@fdintino The free text question works now but the choice one still doesn't.

I get:

Traceback (most recent call last):
  File "***/lib/python3.7/site-packages/django/contrib/staticfiles/handlers.py", line 65, in __call__
    return self.application(environ, start_response)
  File "***/lib/python3.7/site-packages/django/core/handlers/wsgi.py", line 142, in __call__
    response = self.get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 78, in get_response
    response = self._middleware_chain(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 36, in inner
    response = response_for_exception(request, exc)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 90, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 125, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "***/lib/python3.7/site-packages/django_extensions/management/technical_response.py", line 37, in null_technical_500_response
    six.reraise(exc_type, exc_value, tb)
  File "***/lib/python3.7/site-packages/six.py", line 692, in reraise
    raise value.with_traceback(tb)
  File "***/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "***/lib/python3.7/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 607, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/sites.py", line 223, in inner
    return view(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1647, in add_view
    return self.changeform_view(request, None, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "***/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1536, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1576, in _changeform_view
    self.save_related(request, form, formsets, not add)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1122, in save_related
    self.save_formset(request, form, formset, change=change)
  File "***/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1110, in save_formset
    formset.save()
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 149, in save
    instance = self.get_saved_instance_for_form(form, commit, form_instances)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 237, in get_saved_instance_for_form
    instances = self.save_new_objects([form], commit)
  File "***/lib/python3.7/site-packages/nested_admin/formsets.py", line 387, in save_new_objects
    new_objects.append(self.save_new(form, commit=commit))
  File "***/lib/python3.7/site-packages/django/forms/models.py", line 946, in save_new
    pk_value = getattr(self.instance, self.fk.remote_field.field_name)
  File "***/lib/python3.7/site-packages/polymorphic/models.py", line 175, in accessor_function
    attr = model._base_objects.get(pk=self.pk)
  File "***/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)

nested_polymorphic_models_QuestionComponent_DoesNotExist__QuestionComponent_matching_query_does_not_exist_____Werkzeug_Debugger

Edit: I got a different error after my initial post...

@arthurio
Copy link

arthurio commented Apr 26, 2019

Ha! Found the issue, because my radio group doesn't have any field in it, when saving, form.has_changed() is False, which means that my RadioGroup instance was not saved, hence the DoesNotExist error! I think it's an edge case that is not related to what we are trying to do here but you should probably update save_new_objects so that a form with no change that has children should be saved!

I think it's good to ship 🎉 Thanks so much for looking into it!

@fdintino
Copy link
Member

Glad to hear! We have some handling for situations where the parent form has_changed() is False, but there are changes to children, but it must be missing your scenario. Could you open a separate issue so that the fix can be tracked?

Thanks again for your helpful feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants