Issue
Creating an attachment to a form. Many. All is going well. Here’s the problem… I want to be able to “GET” these attachments in my update form, view them and remove them if the form is approved. This has proven challenging. In some cases, forms are prefilled by using dictionaries to get the required data as initial data. Everything is working as expected except for FileFields or FieldFiles as Django is referencing them. I’ve read some similar articles on SO, but nothing helps. I understand the security issues and I’m not trying to “force” an upload.I just want to get the attachment name and essentially copy it to another model.The form will submit but , the attachment is not processed.
Here is my code….
HTML…
My form…
class UpdateProcedureFilesForm(forms.ModelForm):
class meta:
Model = UpdateProcedureFiles
field = ['Attachments']
widget = {
'Attachment': 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_list of dicts = []
For attachments 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_Dictionary)
attachment_list of dicts.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,
}))
For procedure.department and procedure.access_level == "Default" in self.request.user.userprofile.department_access.all() :
return the initial value
other than that:
Raise Http404
def get_context_data(self, **kwargs):
context = super(UpdateProcedureView, self).get_context_data(**kwargs)
pk=self.kwargs["pk"]
For self.request.POST:
context["attachments"] = UpdateProcedureFileFormSet(self.request.POST,self.request.FILES)
other than that:
context["attachment"] = UpdateProcedureFileFormSet()
return the 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 you "cancel" in request.POST :
return HttpResponseRedirect(reverse('Procedures:procedure_main_menu'))
other than that:
self.object = None
form_class = self.get_form_class()
form = self. get_form(form_class)
user = request.user
User profile = request.user
procedure_attachment_form = UpdateProcedureFileFormSet(self.request.POST,self.request.FILES)
files = request.FILES.getlist('attachments') #model field name
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 the file:
file_instance = UpdateProcedureFiles(attachment=f, update_procedure=procedure_instance)
file_instance.save()
return self.form_valid(form, procedure_attachment_form)
other than that:
return self.form_invalid(form, procedure_attachment_form)
Again, this all works. The only exception is when FileFields are involved…then nada.
Solution
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
Answer Checked By – Willingham (Easybugfix Volunteer)