Search code examples
pythonajaxdjangobootstrap-modalinline-formset

Inline formset causing error: 'NoneType' object has no attribute 'is_ajax'


I am using bootstrap-modal-forms to show a user a formset with some inline forms. It is possible for the user to save the form if data is only entered into the original form, but if the inline formset has data then I get the following error:

'NoneType' object has no attribute 'is_ajax'

The inline formset was working correctly before I tried to implement them in the modal form. The problem seems to arise only when the inline formset (projectimages) is saved it is a NoneType.

My views.py

class ProjectCreate(BSModalCreateView):
    form_class = ProjectForm
    template_name = 'project_form.html'
    success_message = 'Success: %(project_name)s was created.'

    def get_success_url(self):
        return reverse_lazy('project-detail', kwargs={'project': self.object.slug})

    def get_context_data(self, **kwargs):
        data = super(ProjectCreate, self).get_context_data(**kwargs)
        if self.request.POST:
            data['projectimages'] = ProjectFormSet(self.request.POST, self.request.FILES,)
        else:
            data['projectimages'] = ProjectFormSet()
        return data

    def form_valid(self, form):
        form.instance.date_created = timezone.now()
        context = self.get_context_data()
        projectimages = context['projectimages']
        with transaction.atomic():
            self.object = form.save()

            if projectimages.is_valid():
                projectimages.instance = self.object
                projectimages.save()

        return super(ProjectCreate, self).form_valid(form)

My forms.py

class ProjectForm(BSModalForm):
    class Meta:
        model = Project
        exclude = ['date_created', 'slug']

ProjectFormSet = inlineformset_factory(
    Project,
    ProjectImage,
    can_delete=True,
    form=ProjectForm,
    extra=1,
)

My models.py

class Project(models.Model):
    project_name = models.CharField(max_length=100)
    date_created = models.DateTimeField('Created on')
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.project_name

    def save(self, *args, **kwargs):
        self.slug = slugify(str(self))
        super(Project, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('project-list')

class ProjectImage(models.Model):
    image =  models.ImageField(verbose_name='Additional Images', upload_to=project_directory_path)
    project = models.ForeignKey(Project, on_delete=models.PROTECT)
    annotation = models.CharField(max_length=200, blank=True)

I expect the user to be able to add as many images to the modal formset as they like.


Solution

  • The BSModalForm expects you to initialise it with the request. This happens in your BSModalCreateView for the main form but not for your formset, because you initialise it manually.

    So when initialising, just add the form_kwargs attribute:

     if self.request.POST:
         data['projectimages'] = ProjectFormSet(
             self.request.POST, self.request.FILES,
             form_kwargs={'request': self.request})
     else:
         data['projectimages'] = ProjectFormSet(form_kwargs={'request': self.request})
    

    Note that I think the form you set in ProjectFormSet is wrong, because it should be a form for a ProjectImage model, not a Project. It should actually be called ProjectImageFormSet to better reflect what it is.

    You probably want to remove form=ProjectForm as it probably doesn't need to be a BSModalForm (not sure about that). In that case you should not pass the request in form_kwargs. If not, you just need to create another ProjectImageForm class.

    Finally, you should not return super().form_valid() because that will save the main form a second time (you already did). Do the redirect yourself.