Generic Create View, ForeignKey, and Clean()

I am having a problem with trying to use CreateView, and then calling methods on the model, because the instance attribute is None until too late in the form process. Suppose this setup:

urls.py:

urlpatterns = [ path('<int:pk>/create/', ChildCreate.as_view()) ]

models.py:

class Parent(models.Model):
    pass

class Child(models.Model):
    parent = models.ForeignKey(Parent, on_delete=models.CASCADE)

    def clean(self):
        if self.parent.pk == 1:
            raise ValidationError("Parent 1 can't have children, because reasons")

views.py:

class ChildCreate(CreateView)
    model = child
    fields = []

The problem is that “parent” is not set anywhere in this workflow. As a result when the form is ready to save() it throws a “Parent is None” error.

I can fix this by overriding form_valid() in views.py:

class ChildCreate(CreateView)
    model = child
    fields = []

    def form_valid(self, form):
        # copy url PK to the form so it has the value attached
        form.instance.parent_id = self.kwargs["pk"]

        return super().form_valid(form);

…but this does not fix the problem with the clean() method in models.py, because that is getting called before form_valid.

Where is the proper place to set the parent ID for the new child, so it’s available for clean and save methods?

Hi Greg.

Assuming you know the parent early, I’d probably override get_form() to set it when the form is first created.

I’m pretty certain that won’t work, as instance isn’t created until _post_clean():

Right when the form is instantiated, it creates the instance if one is not provided:

        # in BaseModelForm.__init__()
        if instance is None:
            # if we didn't get an instance, instantiate a new one
            self.instance = opts.model()

So then you can set values on the instance, and as long as they’re not overridden by your form data in construct_instance() they’ll be available.

So something like:

def get_form(...):
     form = super().get_form(...)
     form.instance.parent = parent_obj   # Generally try to not use the `_id` attributes. 
     return form

If you can get_form_kwargs() is perhaps nicer, but sometimes these views have too many methods to remember them all. :slightly_smiling_face:

I hope that makes sense.

Kind Regards,

Carlton

1 Like

Passing the instance of the parent like this should work.

class ChildCreate(CreateView):
      model = Child
      fields = '__all__'
  
      def form_valid(self, form):
          form.instance.parent = Parent.objects.get(id=self.kwargs.get('pk'))
          return super(ChildCreate).form_valid(form);