Search code examples
djangodjango-generic-viewsinline-formsetimagefield

UpdateView clean deleted form but does not actually delete it


I have formset of modelforms contains ImageField. After click on delete checkbox near initial form, and submit button - I have same number of forms in formset, and same number of initial forms, but those which have been "deleted" now are without value and without delete checkbox (checkbox is enable in admin panel, I assume it's widget render). What I am doing wrong?

Mixin to combine create and update views:

class HumanAddEditMixin(object):
    u"""
    """
    form_class = HumanForm
    model = Human
    template_name = 'human/add_edit.html'
    object = None

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(HumanAddEditMixin, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        if self.object:
            form = self.form_class(request.POST, instance=self.object)
            sign_formset = SignFormSet(request.POST, request.FILES, instance=self.object)
            description_formset = DescriptionFormSet(request.POST, request.FILES, instance=self.object)
            doc_formset = DocFormSet(request.POST, instance=self.object)
        else:
            form = self.form_class(request.POST)
            sign_formset = SignFormSet(request.POST, request.FILES)
            description_formset = DescriptionFormSet(request.POST, request.FILES)
            doc_formset = DocFormSet(request.POST)

        if form.is_valid() and sign_formset.is_valid() and description_formset.is_valid() and doc_formset.is_valid():
            return self.form_valid(form, sign_formset, description_formset, doc_formset)
        else:
            return self.form_invalid(form, sign_formset, description_formset, doc_formset)

    def form_valid(self, form, sign_formset, description_formset, doc_formset):
        if not self.object:
            self.object = form.save()
            sign_formset.instance = self.object
            description_formset.instance = self.object
            doc_formset.instance = self.object

        sign_formset.save()
        description_formset.save()
        doc_formset.save()

        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, sign_formset, description_formset, doc_formset):
        return self.render_to_response(
            self.get_context_data(
                form=form,
                sign_formset=sign_formset,
                description_formset=description_formset,
                doc_formset=doc_formset
            )
        )

    def get_success_url(self):
        return reverse('human:list')

CreateView looks like it works fine:

class HumanAddView(HumanAddEditMixin, CreateView):
    def get_context_data(self, **kwargs):
        context = super(HumanAddView, self).get_context_data(**kwargs)

        context['sign_formset'] = SignFormSet(instance=self.model())
        context['description_formset'] = DescriptionFormSet(instance=self.model())
        context['doc_formset'] = DocFormSet(instance=self.model())

        return context

UpdateView:

class HumanUpdateView(HumanAddEditMixin, UpdateView):
    pk_url_kwarg = 'human_id'

    def dispatch(self, request, *args, **kwargs):
        self.object = get_object_or_404(self.model, pk=kwargs[self.pk_url_kwarg])

        return super(HumanUpdateView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(HumanUpdateView, self).get_context_data(**kwargs)

        context['sign_formset'] = SignFormSet(instance=self.object)
        context['description_formset'] = DescriptionFormSet(instance=self.object)
        context['doc_formset'] = DocFormSet(instance=self.object)

        return context

Formsets declaration:

SignFormSet = inlineformset_factory(Human, HumanSignImage, form=HumanSignImageForm, extra=1)
DescriptionFormSet = inlineformset_factory(Human, HumanDescriptionImage, form=HumanSignImageForm, extra=1)
DocFormSet = inlineformset_factory(Human, HumanDocImage, form=HumanDocImageForm, extra=1)

One of models with image (that in SignFormSet) and it's modelform (just with crispy-forms helper):

class HumanSignImage(models.Model):

    human = models.ForeignKey('Human', related_name='sign_images')
    image = models.ImageField(upload_to=get_file_path, verbose_name=u'', blank=True, null=True)

    def __unicode__(self):
        return self.image.name

class HumanSignImageForm(CommonImageModelFormWithHelper):
    class Meta:
        model = HumanSignImage

Solution

  • Finally I've figured out my problem was that there are two checkbox types on Admin panel: clear and delete. And for some reason (with can_delete=True crispy-forms doesn't render delete checkbox here is an answer) I mistook that clear was delete.

    Or (without crispy-forms) override ClearableFileInput widget (without clear but with delete), something like that:

    class ImageFileInput(ClearableFileInput):
        template_with_initial = '{initial} {input} {delete}'
    
        def delete_checkbox_name(self, name):
            """
            Given the name of the file input, return the name of the DELETE checkbox
            input.
            """
            tmp_list = name.split('-')
            del tmp_list[-1]
            delete_checkbox_name = '-'.join(tmp_list)
    
            return delete_checkbox_name + '-DELETE'
    
        def delete_checkbox_id(self, name):
            """
            Given the name of the delete checkbox input, return the HTML id for it.
            """
            return 'id_' + name
    
        def render(self, name, value, attrs=None):
            substitutions = {}
            template = '{input}'
            substitutions['input'] = FileInput().render(name, value, attrs)
    
            if value and hasattr(value, "url"):
                template = self.template_with_initial
                substitutions['initial'] = format_html(
                    self.url_markup_template,
                    value.url,
                    force_text(value)
                )
    
                checkbox_name = self.delete_checkbox_name(name)
                checkbox_id = self.delete_checkbox_id(checkbox_name)
                substitutions['delete'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
    
            return mark_safe(template.format(**substitutions))