ionalchemist
ionalchemist

Reputation: 418

Django FormView with dynamic forms

I created the FormView below that will dynamically return a form class based on what step in the process that the user is in. I'm having trouble with the get_form method. It returns the correct form class in a get request, but the post request isn't working.

tournament_form_dict = {
    '1':TournamentCreationForm,
    '2':TournamentDateForm,
    '3':TournamentTimeForm,
    '4':TournamentLocationForm,
    '5':TournamentRestrictionForm,
    '6':TournamentSectionForm,
    '7':TournamentSectionRestrictionForm,
    '8':TournamentSectionRoundForm,}

class CreateTournament(FormView):
    template_name = 'events/create_tournament_step.html'

    def __init__(self, *args, **kwargs):
        form_class = self.get_form()
        success_url = self.get_success_url()
        super(CreateTournament, self).__init__(*args, **kwargs)

    def get_form(self, **kwargs):
        if 'step' not in kwargs:
             step = '1'
        else:
             step = kwargs['step']
        return tournament_form_dict[step]

    def get_success_url(self, **kwargs):
        if 'step' not in kwargs:
            step = 1
        else:
            step = int(kwargs['step'])
        step += 1
        if 'record_id' not in kwargs:
            record_id = 0
        else:
            record_id = int(kwargs['record_id'])
        return 'events/tournaments/create/%d/%d/' % (record_id, step)

The post request fails at the django\views\generic\edit.py at the get_form line, which I realize is because I've overwritten it in my FormView:

def post(self, request, *args, **kwargs):
    """
    Handle POST requests: instantiate a form instance with the passed
    POST variables and then check if it's valid.
    """
    form = self.get_form()
    if form.is_valid(): …
        return self.form_valid(form)
    else:
        return self.form_invalid(form)

However, when I change the name of my custom get_form method to say gen_form, like so:

def __init__(self, *args, **kwargs):
    form_class = self.gen_form()
    success_url = self.get_success_url()
    super(CreateTournament, self).__init__(*args, **kwargs)

def gen_form(self, **kwargs):
    if 'step' not in kwargs:
        step = '1'
    else:
        step = kwargs['step']
    return tournament_form_dict[step]

my form class doesn't get processed in the get request and evaluates to None. I'm scratching my head as to why when I override the get_form method, it works, but my own named method doesn't? Does anyone know what the flaw might be?

Upvotes: 1

Views: 2826

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476614

Django's FormMixin [Django-doc] defines a get_form function [Django-doc]. You here thus basically subclassed the FormView and "patched" the get_form method.

Your attempt with the gen_form does not work, since you only defined local variables, and thus do not make much difference anyway, only the super(..) call will have some side effects. The other commands will keep the CPU busy for some time, but at the end, will only assign a reference to a Form calls to the form_class variable, but since it is local, you will throw it away.

That being said, your function contains some errors. For example the **kwargs will usually contain at most one parameter: form_class. So the steps will not do much. You can access the URL parameters through self.args and self.kwargs, and the querystring parameters through self.request.GET. Furthermore you probably want to patch the get_form_class function anyway, since you return a reference to a class, not, as far as I understand it, a reference to an initilized form.

Constructing URLs through string processing is probably not a good idea either, since if you would (slightly) change the URL pattern, then it is likely you will forget to replace the success_url, and hence you will refer to a path that no longer exists. Using the reverse function is a safer way, since you pass the name of the view, and parameters, and then this function will "calculate" the correct URL. This is basically the mechanism behind the {% url ... %} template tag in Django templates.

A better approach is thus:

from django.urls import reverse

class CreateTournament(FormView):
    template_name = 'events/create_tournament_step.html'

    def get_form_class(self):
        return tournament_form_dict[self.kwargs.get('step', '1')]

    def get_success_url(self):
        new_step = int(self.kwargs.get('step', 1)) + 1
        # use a reverse
        return reverse('name_of_view', kwargs={'step': new_step})

Upvotes: 3

Related Questions