Search code examples
djangodjango-formsdjango-validation

Django forms dynamic generation and validation


Let's say I have the following model

class Foo(models.Model):
    title = models.CharField(default='untitled')
    # Bar is a MPTT class, so I'm building a tree here
    # Should not matter for my question...
    root = models.ForeignKey(Bar)
    leaf = models.ForeignKey(Bar)

To create new Foo objects I want to make use of a ModelForm like this:

class FooForm(ModelForm):
    # possibly custom validation functions needed here...
    class Meta:
        model = Foo
        fields = '__all__'

My view looks like this:

def create(request, leaf_id=None):
    form = FooForm(data=request.POST or None)

    if form.is_valid():
        new = form.save()
        return redirect('show.html', root_id=new.root.id)

    return render('create_foo.html', 
                  { 'form': form })

As you can see, the view function should be used to handle two different use-cases:

  1. /foo/create/
  2. /foo/create/4where 4 is a leaf-ID.

If the leaf-ID is given, the form obviously isn't required to show a form field for this. Furthermore, root can be determined from leaf, so it isn't required aswell.

I know that I can dynamically change the used widgets, so I can switch them to HiddenInput, but I would like to not even show them as hidden to the user. But if I dynamically exclude them, they are not available for form validation and the whole process will fail during the validation process.

What I would like to achieve is: Show only form fields to the user, that are not yet pre-filled. Is there any best-practice available for this case?


Solution

  • You can do that by overriding the __init__() method of FooForm.

    We override the __init__() method and check if instance argument was passed to the form. If instance was passed, we disable the root and leaf form fields so that it is not displayed in the template.

    We will pass instance argument to the form when the request is of type foo/create/4 i.e. leaf_id is not None.

    forms.py

    class FooForm(ModelForm):    
    
        def __init__(self, *args, **kwargs):
            super(FooForm, self).__init__(*args, **kwargs) # call the 'super()' init method
            instance = getattr(self, 'instance', None) # get the `instance` form attribute 
            if instance and instance.id: # check if form has 'instance' attribute set and 'instance' has an id
                self.fields['root'].widget.attrs['disabled'] = 'disabled' # disable the 'root' form field
                self.fields['leaf'].widget.attrs['disabled'] = 'disabled' # disable the 'leaf' form field
    
        # custom validation functions here
        ....
    
        class Meta:
            model = Foo
            fields = '__all__'
    

    In our view, we first check if leaf_id argument was passed to this view. If leaf_id was passed,we retrieve the Foo object having leaf id as the leaf_id. This instance is then passed when initializing a form and is updated when form.save() is called. We will use the instance to populate the form with values as the attributes set on the instance.

    If leaf_id is not passed, then we initialize FooForm with data argument.

    views.py

    def create(request, leaf_id=None):
        # Get the instance if any
        instance = None
         if leaf_id:
            instance = Foo.objects.get(leaf_id=leaf_id) # get the 'Foo' instance from leaf_id
    
        # POST request handling
        if request.method=='POST':    
            if instance:      
                form = FooForm(data=request.POST, instance=instance) # Populate the form with initial data and supply the 'instance' to be used in 'form.save()'
            else:
                form = FooForm(data=request.POST)
    
            if form.is_valid():
                new = form.save()
                return redirect('show.html', root_id=new.root.id)
    
            return render('create_foo.html', 
                      { 'form': form })
    
        # GET request handling    
        if instance:
            form = FooForm(initial=instance._data, instance=instance) # form will be populated with instance data
        else:
            form = FooForm() # blank form is initialized        
        return render('create_foo.html', 
                      { 'form': form })