Is it possible to prepopulate Django FormSets with FileFields?

Question:

I am creating attachments on a form. Multiple. All is well and good. Here’s the problem…I want to "GET" those attachments on an update form so that they are displayed and can be deleted if the form is approved. This is proving challenging. I am prepopulating forms in some cases by using a dictionary to get the data that I need as initial data. All is working as expected except for FileFields or FieldFile as Django references it. I have read a few similar articles on SO…but nothing is helping. I understand the security issues and I am not trying to "FORCE" uploads..I simply want to grab the attachment name and copy it to another model essentially. My form submits, but the attachments are not being processed.

Here’s my code….

HTML…

<form method="POST" enctype="multipart/form-data" id="forms">

    {{ procedure_attachment_form.management_form }}

    {{ procedure_attachment_form.non_form_errors }}

    {% for fileform in procedure_attachment_form.forms %}

    {{ fileform.id }}

      <div class="inline {{ procedure_attachment_form.prefix }}">

          {{ fileform.attachments }}

            {% if procedure_attachment_form.non_form_errors %}

              <h3 class="spacer174">
                {{ procedure_attachment_form.non_form_errors }}
              </h3>

            {% endif %}

            {% if fileform.attachments.errors %}

              <h3 class="spacer174">
                {{ fileform.attachments.errors }}
              </h3>

            {% endif %}

          {{ fileform.procedure.as_hidden }}

          </div>

    {% endfor %}

My FORM…

class UpdateProcedureFilesForm(forms.ModelForm):

class Meta:
    model = UpdateProcedureFiles
    fields = ['attachments']
    widgets = {
        'attachments': ClearableFileInput(attrs={'multiple': True}),
    }

My View( CreateView )

class UpdateProcedureView(LoginRequiredMixin,CreateView):
    model = UpdateProcedure
    form_class = UpdateProcedureForm
    template_name = 'update_procedure.html'

def get(self, request, *args, **kwargs):
    self.object = self.get_object()
    context = self.get_context_data()
    form_class = self.get_form_class()
    form = self.get_form(form_class)

    dropdown = self.kwargs["pk"]
    attachments = ProcedureFiles.objects.filter(procedure_id=dropdown)

    attachment_listofdicts = []
    for attachment in attachments:
        attachment_dict = model_to_dict(attachment)
        del attachment_dict['id']
        del attachment_dict['procedure']
        del attachment_dict['archive_procedure']
        del attachment_dict['new_procedure']
        del attachment_dict['update_procedure']
        print(attachment_dict)
        attachment_listofdicts.append(attachment_dict)

    UpdateProcedureFileFormSet = inlineformset_factory(UpdateProcedure,
                                                       UpdateProcedureFiles,
                                                       form=UpdateProcedureFilesForm,
                                                       extra=len(attachment_listofdicts),
                                                       can_order=True,
                                                       min_num=0,
                                                       validate_min=True)

    procedure_attachment_form = UpdateProcedureFileFormSet(initial=attachment_listofdicts)
    # print(procedure_attachment_form)

    return self.render_to_response(
        self.get_context_data(
            form=form,
            procedure_attachment_form=procedure_attachment_form,
        )
    )


def get_object(self, queryset=None):
    return get_object_or_404(Procedure, id=self.kwargs['pk'])

def get_initial(self):
    initial = super(UpdateProcedureView, self).get_initial()
    procedure = Procedure.objects.get(pk=self.kwargs["pk"])
    initial = procedure.__dict__.copy()
    department = self.request.user.userprofile.department_access.all()

    initial.update({
                      "name": procedure.name,  
    })

    if procedure.department in self.request.user.userprofile.department_access.all() and procedure.access_level == "Default" :
        return initial
    else:
        raise Http404

def get_context_data(self, **kwargs):
    context = super(UpdateProcedureView, self).get_context_data(**kwargs)
    pk=self.kwargs["pk"]
    if self.request.POST:
        context["attachments"] = UpdateProcedureFileFormSet(self.request.POST,self.request.FILES)
    else:
        context["attachments"] = UpdateProcedureFileFormSet()
    return context

def form_valid(self, form, procedure_attachment_form):
    self.object = form.save()
    procedure_attachment_form.instance = self.object

    instance = form.save()
    return super(UpdateProcedureView, self).form_valid(form)

def form_invalid(self, form, procedure_attachment_form):
    return self.render_to_response(
        self.get_context_data(form=form,
                              procedure_attachment_form=procedure_attachment_form,
                              ))

def post(self, request, *args, **kwargs):
    print(request.POST)
    if "cancel" in request.POST:
        return HttpResponseRedirect(reverse('Procedures:procedure_main_menu'))
    else:
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        user = request.user
        userprofile = request.user
        procedure_attachment_form = UpdateProcedureFileFormSet(self.request.POST,self.request.FILES)
        files = request.FILES.getlist('attachments') #field name in model
        if (form.is_valid() and procedure_attachment_form.is_valid()):
            procedure_instance = form.save(commit=False)
            procedure_instance.user = user
            procedure_instance.save()
            for f in files:
                file_instance = UpdateProcedureFiles(attachments=f, update_procedure=procedure_instance)
                file_instance.save()
            return self.form_valid(form, procedure_attachment_form)
        else:
            return self.form_invalid(form, procedure_attachment_form)

Again, this all works. The only exception is when FileFields are involved…then nada.

Asked By: Steve Smith

||

Answers:

A kind gentleman from a Facebook group named Matt Hoskins provided me with this explanation and after 3 days of research I’m inclined to believe him. Closing this out with the basic premise that it’s not easily possible if possible at all. Moving on to a different approach. Here is his more eloquent summary….Ah, I think I understand the issue. It’s not an issue with form sets at all, as it happens – the key is that you’re trying to set an initial value for a file field on a form that’s not bound to an instance.
HTML file inputs cannot take an initial value (this is not a django thing, this is just how HTML/browsers work), so when django renders a file field widget there is never an initial value even if the underlying data has one.
How django works with editing model instances with file fields is that if the user picks a file on the form and submits then the browser will submit that file as a value for the field and django will update the field on the instance, however if the user doesn’t pick a file on the form then the browser won’t submit that field at all (it’s not that it will submit an empty value for the field, there’ll just be no entry for the field in request.FILES) and when that happens django won’t update the field on the instance (i.e. it will retain its existing value).
The ClearableFileInput widget adds to the plain HTML file input an extra HTML checkbox field to allow for clearing of the existing value to be requested and will display the name of any existing value, but the file input itself still cannot have any initial value stored on it. So when the user submits a form with a ClearableFileInput widget without picking a new file then nothing turns up in request.FILES for that field (the value of the associated clear checkbox field will get submitted, but that’s purely telling django whether to clear the file field on the instance).
So if your inline form set was being prepopulated with actual instances rather than just initial then it would work, but because you’re trying to create a new set of instances based on existing data and that existing data is only being brought across by the HTML form and because html input file fields can’t have an initial value then you’re ending up with nothing for fields the user is not touching (the fact you can see file values displayed by ClearableFileInput widgets picking that up from initial is misleading – those values aren’t submitted by the form).
Hope that makes sense… I’ll write some more musings shortly

Answered By: Steve Smith