Why is my formset not initialised correctly? formset.is_valid() fails!

Hi all,

I have created a formset:

ActivityStepFormSet = inlineformset_factory(
    form=ActivityStepForm, # this is ModelForm
    extra=0,            # number of extra empty forms to display
    min_num=1,          # number of minimum filled forms
    can_delete=False,   # show a checkbox in each form to delete the row

The form is shown, I added a new row (form in the formset through javascript), updated TOTAL-FORMS to 2. When the form is posted, I receive the POST data, and I provide the following kwargs to initialise an ActivityStepFormSet ():

formset_kwargs: {'data': <QueryDict: {'csrfmiddlewaretoken': ['zFzWtsyT5IynU4auVsLE1jhuvc2BrvoFknLEQSHpzactEZ28rfGQcCkWPi0sZ1Cq'], 'activity_digitisation_wizard_view-current_step': ['add_activity'], 'add_activity-name': ['Sample Activity 1'], 'add_activity-description': [''], 'add_activity-name_local': [''], 'add_activity-description_local': [''], 'add_activity-language_local': [''], 'add_activity-created_by': ['2'], 'add_activity-TOTAL_FORMS': ['2'], 'add_activity-INITIAL_FORMS': ['0'], 'add_activity-MIN_NUM_FORMS': ['1'], 'add_activity-MAX_NUM_FORMS': ['1000'], 'add_activity-__prefix__-created_by': [''], 'add_activity-0-created_by': ['2'], 'add_activity-1-created_by': [''], 'add_activity-__prefix__-name': [''], 'add_activity-__prefix__-description': [''], 'add_activity-__prefix__-name_local': [''], 'add_activity-__prefix__-description_local': [''], 'add_activity-__prefix__-language_local': [''], 'add_activity-__prefix__-activity': [''], 'add_activity-__prefix__-stimulus': [''], 'add_activity-__prefix__-id': [''], 'add_activity-0-name': ['help'], 'add_activity-0-description': [''], 'add_activity-0-name_local': [''], 'add_activity-0-description_local': [''], 'add_activity-0-language_local': [''], 'add_activity-0-activity': [''], 'add_activity-0-stimulus': ['1'], 'add_activity-0-id': [''], 'add_activity-1-name': ['help2'], 'add_activity-1-description': [''], 'add_activity-1-name_local': [''], 'add_activity-1-description_local': [''], 'add_activity-1-language_local': [''], 'add_activity-1-activity': [''], 'add_activity-1-stimulus': ['1'], 'add_activity-1-id': [''], '_next': ['Next']}>, 'files': <MultiValueDict: {}>, 'prefix': 'add_activity', 'initial': [{'created_by': <SimpleLazyObject: <User: petasis>>}, {'created_by': <SimpleLazyObject: <User: petasis>>}]}

However, calling is_valid() on the form object, returns false:

False [{}, {'created_by': ['This field is required.']}]

I cannot understand why the second form is invalid, since I have provided a list of two “created_by” in the argument ‘initial’.

What can be wrong?

Please post your ActivityStep model and your ActivityStepForm form, along with the view.

class VASTForm(ModelForm):
    class Meta:
        exclude = ('uuid', 'created', 'updated', 'name_md5', 'id')
        widgets = {
            "date":       AdminSplitDateTime(),
            "date_from":  AdminSplitDateTime(),
            "date_to":    AdminSplitDateTime(),
            "created_by": forms.HiddenInput(),

class ActivityStepForm(VASTForm):
    class Meta(VASTForm.Meta):
        model = ActivityStep

class VASTObject(Model):
    uuid              = models.UUIDField(default = uuid.uuid4, editable = False)
    name              = models.CharField(max_length=255, default=None)
    name_md5          = models.CharField(max_length=64,  default=None, null=True, blank=True, editable=False)
    description       = models.CharField(max_length=255, default=None, null=True, blank=True)
    name_local        = models.CharField(max_length=255, default=None, null=True, blank=True)
    description_local = models.CharField(max_length=255, default=None, null=True, blank=True)
    language_local    = models.ForeignKey('Language', on_delete=models.CASCADE, default=None, null=True, blank=True)
    created           = models.DateTimeField(auto_now_add=True, null=True)
    updated           = models.DateTimeField(auto_now=True, null=True)
    created_by        = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, on_delete=models.CASCADE)
    class Meta(AutoUpdateTimeFields.Meta):
        abstract = True

class VASTObject_NameUserGroupUnique(VASTObject):
    class Meta(VASTObject.Meta):
        abstract = True
        # Enforce a policy for the name to be unique (for each user)
        unique_together = [["name", "created_by"]]

class ActivityStep(VASTObject_NameUserGroupUnique):
    activity          = models.ForeignKey('Activity',     on_delete=models.CASCADE, default=None, null=False, blank=False)
    stimulus          = models.ForeignKey('Stimulus',     on_delete=models.CASCADE, default=None, null=False, blank=False)

The view is a NamedUrlSessionWizardView from formtools package.

I have restarted django, and reseted session data. Starting with a single form in formset, and with everything unfilled, is_valid() returns:

False [{'name': ['This field is required.'], 'stimulus': ['This field is required.'], 'id': ['This field is required.']}]

According to my ActivityStepForm (which inherits Meta from VASTForm), ‘id’ should not be there.

Unless your form is “insert only”, I don’t think you can validly exclude the id field in a ModelForm. On any “edit” operation, Django needs the ID to identify which entity is being edited. (See Creating forms from models | Django documentation | Django)

I think the problem originates from the way I add the new rows/forms.

In javascript, I copy the empty form, which has created_by empty. So, this ends also empty in the data, and validation fails.

I removed id by explicitly set exclude=(‘id’,) in the call to inlineformset_factory. I shouldn’t need to do that, but…

Is there any way to set default values to the empty form?

Yes, my forms are “insert only”. How can I specify this?
(Its a wizard designed to add new objects)

Upon second thought - I still think you need the id field. It can remain hidden, but I believe it’s necessary. (My current guess would be that Django uses an empty ID field to identify that this is an insert vs an update.) You need an ID field, it would be None on a new row.

When you copy the empty forms, are you also changing the name fields to reflect their new instance number?

If you haven’t done so before, I suggest you take the simplest case for a formset and render it with two or three rows along with one or two extras, and visually examine what gets rendered to help visualize how a formset works. (I know that helped me a great deal when I started working with them.) It’ll also help you know what to look for when checking your JavaScript for adding new rows.

Yes, I change the ids (I have posted an output in the first post). Now I am trying to change the data passed when initialising the formset…

This seems to work:

class ActivityForm(VASTForm):
    activity_steps = ActivityStepFormSet()
    def __init__(self, *args, **kwargs):
        formset_kwargs = copy.deepcopy(kwargs)
        # Get prefix
        prefix = kwargs.get('prefix')
        # Get number of forms
        data   = kwargs.get('data')
        if data:
            total_forms = int(data.get(f'{prefix}-TOTAL_FORMS')[0])
            # Set created_by...
            for key in kwargs.get('initial').keys():
                value = formset_kwargs['data'][f'{prefix}-0-{key}']
                for i in range(1, total_forms):
                    formset_kwargs['data'][f'{prefix}-{i}-{key}'] = value
            total_forms = 1
        # Give as many initial objects as forms...
        formset_kwargs['initial'] = [kwargs.get('initial')] * (total_forms)
        # TODO: Handle instances
        self.activity_steps = ActivityStepFormSet(*args, **formset_kwargs)
        super().__init__(*args, **kwargs)

    class Meta(VASTForm.Meta):
        model = Activity

(My formset is shown as a “field” in the parent model)