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

Issue with polymorphic_ctype when populating polymorphic inline formsets. #549

Open
michaelbaisch opened this issue Sep 6, 2023 · 2 comments

Comments

@michaelbaisch
Copy link

When trying to populate a polymorphic inline formset using the django-polymorphic package, I'm encountering a problem with the polymorphic_ctype field.

Context

In the get_context_data method, I populate the formset's initial data using:

ct = ContentType.objects.get_for_model(constraint)
initial[i]['polymorphic_ctype'] = ct

This works for an initial GET request to display the form. However, upon a POST, the formset becomes populated with the POST data where polymorphic_ctype is a string (eg "scheduler | sun separation constraint") and not the expected id, leading to an error:

["Formset row constraint_set-0 has no 'polymorphic_ctype' defined!"]

Expected Behavior

The polymorphic_ctype should be correctly recognized.

Current Behavior

The code expects an instance of ContentType:

model = defaults["initial"]["polymorphic_ctype"].model_class()

However, it also tries to retrieve an id from the POST data, which causes the inconsistency:

ct_id = int(self.data[f"{prefix}-polymorphic_ctype"])

Django version: 4.2.4
django-polymorphic version: 3.1.0
Python version: 3.11.5

Is there a better method to populate the formset? I'm attempting this approach to implement a "Duplication" feature. The intention is for the form to display data from the original Job and its associated constraints. This data has not been saved yet, allowing the user to make any necessary corrections.

 class JobDuplicateView(JobCreateView):
    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        cloned_constraints = self.duplicated_constraints
        
        # Prepopulate the formset with the cloned constraints
        initial = [{} for _ in cloned_constraints]
        for i, constraint in enumerate(cloned_constraints):
            constraint_form = data['constraint_formset'].get_form_class(constraint.__class__)()

            ct = ContentType.objects.get_for_model(constraint)
            initial[i]['polymorphic_ctype'] = ct

            for name, field in constraint_form.fields.items():
                if name != "id" and name != "job":
                    initial[i][name] = getattr(constraint, name)

        data['constraint_formset'].initial = initial
        data['constraint_formset'].extra = len(cloned_constraints)

        return data
@joshbaran
Copy link

I've run into this as well, and I used this as a workaround:

  1. Create a wrapper class for ContentType
class CTWrap:
    ''' Wrapper for ContentType to work around a bug in polymorphic inlineformsets'''
    def __init__(self,*args,**kwargs):
        self.ct = args[0]
        self.id = self.ct.id
        
    def __str__(self):
        return str(self.id)
    
    def model_class(self):
        return self.ct.model_class()
  1. Use the wrapper for initial. Using your snippet above:
ct = ContentType.objects.get_for_model(constraint)
ctw = CTWrap(ct)
initial[i]['polymorphic_ctype'] = ctw

The logic being: whatever causes this bug is using the str of the ContentType instead of the id when generating the form. The wrapper overrides str to return the id, which then gets into the form instead, and then the POST processing works. The wrapper also has the id directly on it and the model_class method which are the only other parts of ContentType being used here.

@michaelbaisch
Copy link
Author

@joshbaran looks interesting. My workaround is a custom subclass for BasePolymorphicInlineFormSet:

class CustomBasePolymorphicInlineFormSet(BasePolymorphicInlineFormSet):
    def _construct_form(self, i, **kwargs):
        """
        Workaround:
        For 'initial' data 'polymorphic_ctype' is expected
        to be a ContentType object. However, in the form, it must be the id as int.
        This method ensures that the ContentType object is converted to id after is was used.
        """
        form = super()._construct_form(i, **kwargs)

        ctype = form.initial.get("polymorphic_ctype")
        if isinstance(ctype, ContentType):
            form.initial["polymorphic_ctype"] = ctype.id

        return form

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

2 participants