16

It seems like if a ModelForm is given an instance, it ignores any values you provide for initial and instead sets it to the value of the instance -- even if that instance is an empty model record.

Is there any way to create a form with an instance and have it set initial data?

I need it because I'm saving related records and they don't appear to save correctly unless the ModelForm is given an instance when created.

I'm sure the answer to this is straightforward and I'm just missing something obvious.

Here is the relevant code:

in the view:

form = form_class(person=person, conference=conference, initial=initial, instance=registration)

where form_class is RegistrationForm and then in the registration form:

class RegisterForm(forms.ModelForm):
    ... fields here ...

    def __init__(self, *args, **kwargs):
        ... other code ...
        self.person = kwargs.pop('person')
        super(RegisterForm, self).__init__(*args, **kwargs)
        for key, in self.fields.keys():
            if hasattr(self.person, key):
                self.fields[k].initial = getattr(self.person, key)

Then when I call the field, the related fields are empty.

Jordan Reiter
  • 20,467
  • 11
  • 95
  • 161
  • If somebody still comes across this question, look here: https://stackoverflow.com/a/26887842/11983534 – Whale May 16 '20 at 00:32
  • If somebody still comes across this question, look here: https://stackoverflow.com/a/26887842/11983534 – Whale May 16 '20 at 00:35

4 Answers4

9

Figured this out after a little bit of googling.

You have to set the initial value before calling super.

So instead of looping through self.fields.keys(), I had to type out the list of fields that I wanted and looped through that instead:

class RegisterForm(forms.ModelForm):
    ... fields here ...
    initial_fields = ['first_name', 'last_name', ... ]

    def __init__(self, *args, **kwargs):
        ... other code ...
        self.person = kwargs.pop('person')
        for key in self.initial_fields:
            if hasattr(self.person, key):
                self.fields[k].initial = getattr(self.person, key)
        super(RegisterForm, self).__init__(*args, **kwargs)

@Daria rightly points out that you don't have self.fields before calling super. I'm pretty sure this will work:

class RegisterForm(forms.ModelForm):
    ... fields here ...
    initial_fields = ['first_name', 'last_name', ... ]

    def __init__(self, *args, **kwargs):
        ... other code ...
        initial = kwargs.pop('initial', {})
        self.person = kwargs.pop('person')
        for key in self.initial_fields:
            if hasattr(self.person, key):
                initial[key] = initial.get(key) or getattr(self.person, key)
        kwargs['initial'] = initial
        super(RegisterForm, self).__init__(*args, **kwargs)

In this version, we use the initial argument to pass the values in. It's also written so that if we already have a value in initial for that field, we don't overwrite it.

Jordan Reiter
  • 20,467
  • 11
  • 95
  • 161
  • 1
    But before calling super constructor, you don't have self.fields – Daria Apr 21 '14 at 04:44
  • Huh, you're right. Not sure how I ended up making this work…nearly half a year ago now. I'll have to see if I can find the relevant code and how it ended up being implemented. – Jordan Reiter Apr 21 '14 at 05:45
  • 3
    `for key, in self.initial_fields:` is not a valid Python. If I am not wrong either remove `,` or add underscore `, _` – Grijesh Chauhan Nov 18 '14 at 16:00
2

Sounds to me that you may be looking for a bound form. Not entirely sure, I'm trying to unpick a similar issue:

Django forms can be instantiated with two arguments which control this kind of thing. As I understand it:

form = MyForm(initial={...}, data={...}, ...)

initial will set the possible values for the fields—like setting a queryset—data will set the actual (or selected) values of a form and create a bound form. Maybe that is what you want. Another, tangental, point you might find interesting is to consider a factory method rather than a constructor, I think the syntax is more natural:

class MyForm(forms.ModelForm):

    ...

    @staticmethod
    def makeBoundForm(user):
        myObjSet = MyObject.objects.filter(some_attr__user=user)
        if len(myObjSet) is not 0:
            data = {'myObject': myObjSet[0]}
        else:
            raise ValueError()
        initial = {'myObject': myObjSet}
        return MyForm(initial=initial, data=data)
Peter Shannon
  • 191
  • 1
  • 3
  • Not exactly. In this case it's more like I was trying to pre-fill some fields for one kind of model with fields from another model. For example, filling in the address fields for an order based on the existing address for that user. – Jordan Reiter Jun 19 '15 at 14:26
  • I see, well, I think you could still apply the basic point I was making. This [page](http://stackoverflow.com/questions/21925671/convert-django-model-object-to-dict-with-all-of-the-fields-intact) describes how you could convert a model into a dictionary in a general way (not really a hard problem but should save some time) once you have a dictionary you can remove any fields you don't want set and supply it as the _data_ argument. I suppose this is an alternative to _instance_ which grants more control over the form initialisation data used. – Peter Shannon Jun 21 '15 at 11:37
1

You can also pass extra variables to the class when initializing it. The values you pass can then override initial or POST data.

class RegisterForm(forms.ModelForm):
    ... fields here ...

    def __init__(self, person, conference, *args, **kwargs):
        ... other code ...
        super(RegisterForm, self).__init__(*args, **kwargs)
        self.fields['person'] = person
        self.fields['conference'] = conference

form = RegisterForm(person, conference, initial=initial, instance=registration)
Tom
  • 1,986
  • 2
  • 18
  • 29
  • 1
    Unless I'm reading this incorrectly, this replaces fields with the values from a model. I can't think of a use case where you'd actually want to do this. – Jordan Reiter Jun 19 '15 at 14:26
  • The initial and instance data will get passed to `super(RegisterForm, self).__init__(*args, **kwargs)` then the explicitly passed in values from the view (or wherever the form instance is being created) `person` and `conference` will override both `initial` and `instance`. – Tom Jun 19 '15 at 14:54
  • 4
    Read your code. You're setting `self.fields['person']` to the value contained in `person`. `self.fields['person']` was a field; `person` was not. Perhaps you meant `self.fields['person'].initial = person`? Because I can't think of any situation where you'd want to replace one of the fields of a form with a record. – Jordan Reiter Jun 19 '15 at 20:38
0

Use ModelAdmin.get_changeform_initial_data. For example, if you add initial data for form field "report_datetime"

    def get_changeform_initial_data(self, request):
        initial_data = super().get_changeform_initial_data(request)
        initial_data.update(report_datetime=<my_initial_datetime>)
        return initial_data

Works for 3.2+. I'm not sure about older versions.

See django docs

erikvw
  • 416
  • 3
  • 11