11

With this form:

class Form(forms.Form):
    name = forms.CharField(required=False, initial='Hello world')

If I do something like this in the view:

form = Form(request.GET)
if form.is_valid():
    name = form.cleaned_data['name']

Then initial value of name is lost even if request.GET does not contain name as key. Is there any workaround? I would like initial values work to bound forms as "default values".

8 Answers 8

12

By slightly modifying Gonzalo's solution, this is the right way:

class Form(forms.Form):
    name = forms.CharField(required=False, initial='Hello world')

    def clean_name(self):
        if not self['name'].html_name in self.data:
            return self.fields['name'].initial
        return self.cleaned_data['name']

If you need this, you may have a look at django-filter app. I have discovered it quite recently.

Sign up to request clarification or add additional context in comments.

Comments

5

initial isn't really meant to be used to set default values for form fields. Instead, it's really more a placeholder utility when displaying forms to the user, and won't work well if the field isn't required (like in your example).

What you can do is define a clean_<fieldname> method that checks if there's an empty value for that field and return the default:

class Form(forms.Form):
    name = forms.CharField(required=False, initial='Hello world')

    def clean_name(self):
        name = self.cleaned_data['name']
        if name is None:
            return self.fields['name'].initial
        return name

3 Comments

This is an interesting option but there is a problem with that if not name:. What if name was set in request.GET but set to empty string. In this case the empty string would get overwritten.
I thought that's the behavior you wanted, I'm editing my answer to handle that case :-)
name will never be None. Even if name is not set in request.GET, cleaned_data['name'] will be an empty string because name is a CharField. And if name is sent as empty string in request.GET, it is anyways an empty string. So, you cannot differentiate between these two cases.
4

I use the following pattern for setting default values as initial values given for the form-

class InitialDefaultForm(forms.Form):
    def clean(self):
        cleaned_data = super(InitialDefaultForm, self).clean()
        # if data is not provided for some fields and those fields have an
        # initial value, then set the values to initial value
        for name in self.fields:
            if not self[name].html_name in self.data and self.fields[name].initial is not None:
                cleaned_data[name] = self.fields[name].initial
        return cleaned_data

This ensures that all fields which have an initial value and do not get values from user get populated by their initial value.

Comments

2

request.GET is a dictionary like object.

initial only works in case of unbound form.

Forms have an attribute named data. This attribute is provided as first positional argument or as a data keyword argument during form initialization.

Bound forms are those in which you provide some data as first argument to the form and unbound form has data attribute set as None.

Here in your initialization of form form=Form(request.GET), you are providing the first positional argument, so data attribute is being set on the form and it becomes a bound form. This happens even if request.GET is an empty dictionary. And since your form becomes a bound form so initial of name field has no effect on it.

So, In you GET request you should either do:

form = Form()

and your initial of name field would be honoured.

Or, if you want to read name from request.GET and if its there then want to use it instead of field's initial then have following in your view.

name = request.GET.get(name)
form_level_initial = {}
if name:
    form_level_initial['name'] = name
form = Form(initial=form_level_initial)

2 Comments

Sorry, I did not mention I need the default/initial value to be present in form.cleaned_data after validation if it has not been overwritten by request.GET
cleaned_data and validation should come into picture on POST request, not on GET. Can you explain a little more about what you want?
2

Will this work:

initial_form_data = {'name': 'Hello World'}   #put all the initial for fields in this dict
initial_form_data.update(request.GET)  #any field available in request.GET will override that field in initial_form_data
form = Form(initial_form_data)
if form.is_valid():
    name = form.cleaned_data['name']

1 Comment

This seems like an elegant solution, but in my case it put things like u'' into the form, which then triggered validation errors. :( I came up with an alternate answer below that gets around this issue.
2

The proposed solutions either didn't work for me or just seemed not very elegant. The documentation specifies that initial does not work for a bound form, which seems to be the original questioners (and my) use case:

This is why initial values are only displayed for unbound forms. For bound forms, the HTML output will use the bound data.

https://docs.djangoproject.com/en/1.10/ref/forms/fields/#initial

My solution is to see if the form should be bound or not:

initial = {'status': [Listing.ACTIVE], 'min_price': 123}     # Create default options

if request.method == 'GET':
    # create a form instance and populate it with data from the request:
    if len(request.GET):
        form = ListingSearchForm(request.GET)       # bind the form
    else:
        form = ListingSearchForm(initial=initial)   # if GET is empty, use default form

You could also use the other ways of initializing the form (mentioned above).

Comments

0

None of the answers actually does exactly what clime asked for. So here is my solution for the same problem:

class LeadsFiltersForm(forms.Form):
    TYPE_CHOICES = Lead.TYPES
    SITE_CHOICES = [(site.id, site.name) for site in Site.objects.all()]

    type = forms.MultipleChoiceField(
        choices=TYPE_CHOICES, widget=forms.CheckboxSelectMultiple(),
        required=False
    )
    site = forms.MultipleChoiceField(
        widget=forms.CheckboxSelectMultiple(), required=False,
        choices=SITE_CHOICES
    )
    date_from = forms.DateField(input_formats=['%m-%d-%Y',], required=False,
                                widget=forms.TextInput(attrs={'placeholder': 'Date From'}),
                                initial=timezone.now() - datetime.timedelta(days=30))
    date_to = forms.DateField(input_formats=['%m-%d-%Y',], required=False,
                                widget=forms.TextInput(attrs={'placeholder': 'Date To'}))

    defaults = {
        'type': [val[0] for val in TYPE_CHOICES],
        'site': [val[0] for val in SITE_CHOICES],
        'date_from': (timezone.now() - datetime.timedelta(days=30)).strftime('%m-%d-%Y'),
        'date_to': timezone.now().strftime('%m-%d-%Y')
    }

    def __init__(self, data, *args, **kwargs):
        super(LeadsFiltersForm, self).__init__(data, *args, **kwargs)
        self.data = self.defaults.copy()
        for key, val in data.iteritems():
            if not data.get(key):
                continue
            field = self.fields.get(key)
            if field and getattr(field.widget, 'allow_multiple_selected', False):
                self.data[key] = data.getlist(key)
            else:
                self.data[key] = data.get(key)

Comments

0

My approach to the problem:

class SomeForm(forms.Form):
    # ...
    def __init__(self, data, *args, **kwargs):
        # get a mutable version of data
        super().__init__(data.copy(), *args, **kwargs)
        for name, field in self.fields.items():
            if field.initial:
                self.data.setdefault(name, field.initial)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.