52

I have an application that uses the following form:

class ConfirmForm(forms.Form):
    account_name = forms.CharField(widget=forms.HiddenInput)
    up_to_date = forms.BooleanField(initial=True)

I use the form in the following template exerpt:

<form class="confirmform" action="/foo/" enctype="multipart/form-data" method="post">
{{ confirm_form.up_to_date }} Check if this data brings the account up to date.<br>
{{ confirm_form.account_name }} <input type="submit" name="confirm" value="Confirm" />
</form>

My view uses the following basic code structure:

if request.method == 'POST':
    #check for 'confirm' because I actually have multiple forms in this page
    if 'confirm' in request.POST:
        confirm_form = ConfirmForm(request.POST)
        if confirm_form.is_valid():
            #do stuff
        else:
            c['confirm_form'] = confirm_form
else:
    c['confirm_form'] = ConfirmForm({'account_name':'provided_value'})

Two things are wrong:

1) Even though I have initial=True, the checkbox is unchecked when the page loads

2) The form is always invalid unless I check the checkbox. It gives errors for up_to_date only: "This field is required."

I have read this similar question but his solution doesn't apply to my project.

So... what's going on?

Edit:

I updated the code above to be more faithful to my actual code.

Problem #1 was my fault because I was overriding the initial value by binding data when the form was instantiated.

Problem #2 I still consider an issue. Using required=False on up_to_date will fix the problem, however it doesn't seem correct that using the default widget, a BooleanField can be either NULL (causes validity check to fail) or True but never False.

Community
  • 1
  • 1
dhowland
  • 643
  • 1
  • 5
  • 12

6 Answers6

46

In Django forms, Boolean fields must be created with required=False.

When the checkbox isn't checked, browsers do not send the field in the POST parameters of requests. Without specifying that the field is optional Django will treat it as a missing field when not in the POST parameters.

Imho it would be nice for Django to have this behavior by default for boolean form fields..

(this was already answered in comments by Yuji but it would be useful as an answer)

yairchu
  • 23,680
  • 7
  • 69
  • 109
40

initial=True should render the widget with checked="checked" so I don't see the problem there.. but the reason the form is invalid is because all fields are required by default unless you specify otherwise.

Field.required¶ By default, each Field class assumes the value is required, so if you pass an empty value -- either None or the empty string ("") -- then clean() will raise a ValidationError exception:

If you'd like a checkbox or any other value to be optional, you need to pass in required=False into the form field constructor.

up_to_date = forms.BooleanField(initial=True, required=False) 
# no longer required..
Yuji 'Tomita' Tomita
  • 115,817
  • 29
  • 282
  • 245
  • 7
    None the less, initial=True has no effect. Also, why would the solution be to make the field optional? Can't the BooleanField return false? – dhowland Dec 17 '11 at 06:08
  • Does the HTML render with "checked"? Have you restarted the server? Can you fire up the debugger (`import pdb; pdb.set_trace()`) at that point to double check that the initial attribute is correctly set? `assert form.fields['up_to_date'].initial == True` These will narrow down our problem.. – Yuji 'Tomita' Tomita Dec 17 '11 at 06:52
  • 2
    As for why make the field optional? 1: the docs say so. But 2: checkboxes are HTML elements that don't submit data upon being "unchecked". A `False` value can only be determined by the lack of input. – Yuji 'Tomita' Tomita Dec 17 '11 at 07:00
  • 2
    Yuji: (1) They do? [the docs for BooleanField](https://docs.djangoproject.com/en/dev/ref/models/fields/#booleanfield) say no such thing. (2) Exactly! I now understand what I need to do to make it work, but I have no idea why the designers would allow is_valid() to fail when we would want a false result. May be the docs should tell us to always let 'required=False' unless the widget or validator is changed. – dhowland Dec 17 '11 at 14:13
  • @dhawland, yes, the relevant docs do. You are looking at the Model documentation. Models have no built in validation. If you look at the `forms` documentation (which is what you are using) you will find the relevant warnings. As for your second point, I disagree. Because forms generally perform specific functions and not _optional functions_ they expect specific input from the user. A majority of the time (for me) that means the fields are required. It may be debatable but that's my opinion : ) – Yuji 'Tomita' Tomita Dec 17 '11 at 17:21
  • 1
    Why would the designers allow `is_valid()` to fail when you want a False result? I don't understand the problem here. In your current code sample, it simply doesn't allow a `False` result which throws a validation error. – Yuji 'Tomita' Tomita Dec 17 '11 at 17:26
  • The latest [BooleanField docs](https://docs.djangoproject.com/en/1.9/ref/forms/fields/#booleanfield) have a note about this. – phoenix Jun 24 '16 at 13:49
17

According to official docs:

If you want to include a boolean in your form that can be either True or False (e.g. a checked or unchecked checkbox), you must remember to pass in required=False when creating the BooleanField.

user
  • 17,781
  • 20
  • 98
  • 124
1

Just a hunch, but this could be about the basics - lost a few hours until realising something similar was just about basic variable types.

In my case, after creating the FORM (very basic, see below)

class para_selection(forms.Form):
    first=forms.BooleanField(initial=False, required=False)
    second=forms.BooleanField(initial=False, required=False)
    third=forms.BooleanField(initial=False, required=False)
    fourth=forms.BooleanField(initial=False, required=False)

... I was using if selection == 'True': .. in my view. But since the form is using Boolean values, the correct code would be: if selection is True:

The first line will never work, because "==" is not used with Boolean type. Hopefully this was not the case, but wanted to flag it here since I kept trying and searching all over the Internet, without paying attention to the type of variable I was using.

0

Make sure you are not overwriting the initial form the first time the page loads.

I originally wasn't checking if there was actually anything in request.GET. The first time the page loads request.GET is None, and calling SomeForm(request.GET) would actually wipe out the initial values.

def some_view(request):

    form = SomeForm()

    if request.method == 'GET':
        form = SomeForm(request.GET)

Adding a check to make sure there is something in request.GET fixed this.

def some_view(request):

    form = SomeForm()

    if request.method == 'GET' and request.GET:
        form = SomeForm(request.GET)
SuperFunkyMonkey
  • 495
  • 4
  • 13
0

If you do as suggested, your up_to_date field will likely always be False. And you will never know if that is because the user wants it that way, or because the user skipped over the field!

I have the same problem with a legally required OPT_IN question. Where on signup, I need to get the user to make a conscience decision to allow my system to send them unsolicited e-mails (or not). I'm working with Django-Allauth and a custom user database.

I'll share my solution below in the hope you can adapt it to your situation - I adapted this from the DOCS.

forms.py:

# ExtraFieldsSignup as per https://stackoverflow.com/a/12308807
class SignupFormExtraFields(forms.Form):
  CHOICES = (('1', 'YES, keep in touch',), ('2', 'No',))
  first_name = forms.CharField(max_length=30, label='Voornaam')
  last_name = forms.CharField(max_length=30, label='Achternaam')
  opt_in = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES)

  def signup(self, request, user):
    user.first_name = self.cleaned_data['first_name']
    user.last_name = self.cleaned_data['last_name']
    # Now we figure out what the user chose:
    if self.cleaned_data['opt_in'] == '1':
        user.opt_in = True
    else:
        user.opt_in = False
    user.save()
Richard Cooke
  • 713
  • 8
  • 15