smolloy
smolloy

Reputation: 368

Django: Setting initial vals of multiplechoicefield only works first time

I'm dynamically generating a form in Django.

forms.py

class ActiveSignalForm(forms.Form):
    choices = forms.MultipleChoiceField(
        label = 'Alter Archiver Registry',
        choices = ((sig.id, sig.signal)
            for sig in registry.objects.order_by('first_registered')),
        widget = forms.CheckboxSelectMultiple,
        initial = [
            sig.id for sig in registry.objects.order_by('first_registered')
            if sig.archival_active],
    )

views.py

def index(request):
    latest_signal_list = registry.objects.order_by('first_registered')
    form = ActiveSignalForm()
    context = {'latest_signal_list': latest_signal_list, 'form': form}
    return render(request, 'archiver/index.html', context)

When the form is submitted, the view that handles it uses HttpResponseRedirect to send the user back to the page hosting the form.

The form does the job that I want it to (that is, it makes the appropriate changes in the database), but the initial values remain stuck at whatever values were grabbed from the database when the server started.

Could you please advise on a technique to make sure the initial values on the form match those in the database every time the page is loaded?

Thanks.

Upvotes: 1

Views: 1491

Answers (2)

BillyBBone
BillyBBone

Reputation: 3324

To understand what's going on, consider the nature of the objects at play:

  1. ActiveSignalForm is a class. It is defined within a module, which means that when the module is loaded into the interpreter, the class object is constructed.

  2. ActiveSignalForm.choices is a class attribute. As such, it is available to any code that loads the ActiveSignalForm class, before even an instance of that class is created. In other words, you can access it as ActiveSignalForm.choices without first having to create an instance with ActiveSignalForm().

  3. In this particular case, the definition involves an assignment -- ActiveSignalForm.choices will be set to the return value provided by calling forms.MultipleChoiceField().

  4. And finally, two of the calling arguments to ModelChoiceField involve comprehension expressions.

With all of this information, you start getting a picture of the chain of events:

  1. The module containing the class is loaded. This kicks-off the construction of the ActiveSignalForm class.

  2. The interpreter encounters an assignment expression to define ActiveSignalForm.choices.

  3. The right-hand side of the expression is evaluated, which involves calling the MultipleChoiceField() callable.

  4. In order to do this, the choices and initial calling arguments must be resolved, and this involves evaluating two comprehension expressions.

So you see, steps 1 through 4 are nested, which results in the behaviour you are seeing -- the server is started, the module is loaded, the values are computed once and then never again.

The Django documentation contains this piece of important information about both initial and choices fields:

Instead of a constant, you can also pass any callable. The callable will be evaluated only when the unbound form is displayed, not when it is defined.

So it seems that in order to have these fields re-calculated every time a different ActiveSignalForm instance is created, we will need to define them as callables.

To accomplish this, I believe all you need to do is prefix your comprehension expressions with lambda:, in order to create a simple callable:

choices = lambda: ((sig.id, sig.signal)
                   for sig
                   in registry.objects.order_by('first_registered')),
initial = lambda: [sig.id
                   for sig
                   in registry.objects.order_by('first_registered')
                   if sig.archival_active],

What this does, is it splits step (4), above, into two parts.

4a. Now, when the class is defined, the only thing that needs to be evaluated when resolving the initial= and choices= arguments to MultipleChoiceField is a lambda statement. This creates a tiny function that, when called will evaluate the comprehension expressions within. This allows the nesting to resolve (i.e. step 4a returns a callable for step 3, which returns a result for assignment as a class property in step 2, which then allows the rest of the class definition to proceed in step 1).

4b. (Much) later, when Django is rendering the ActiveSignalForm.choices field, it encounters the callables we created in (4a) and executes them in order to get the appropriate iterables.

Upvotes: 3

Daniel Roseman
Daniel Roseman

Reputation: 599490

There's no reason to have these list comprehensions. You are filtering on values from querysets, so you should use ModelMultipleChoiceField along with an actual queryset.

choices = forms.ModelMultipleChoiceField(
    label='Alter Archiver Registry',
    queryset=registry.objects.order_by('first_registered')),
    initial=registry.objects.order_by('first_registered').filter(archival_active=True)
)

Upvotes: 0

Related Questions