Django ClearableFileInput - how to detect whether to delete the file - django

I'm using Django Crispy Forms for my form with an option to upload an image (ImageField in my Model)
The forms renders as I'd expect, with the checkbox to clear an existing file. However when processing the form submission the 'image-clear' checkbox always gives me a 'None' value.
image_clear = form.cleaned_data.get("image-clear")
print image_clear
In the HTML of the page I notice that the input checkbox doesn't have a value attribute, see:
<input id="image-clear_id" type="checkbox" name="image-clear">
So I wondered if this was the issue, but when I look at the rendering in the Django admin site, the corresponding HTML input field doesn't have a value either - yet it still identifies that the image should be removed.
I should note that if I upload a new image, then this works, it's only the case where I'm removing/clearing the image (and it works in Django admin pages, so assume that means my model definition is ok to allow no image to be attached to the model)
So... in my form processing, how do I detect whether or not the image should be removed or not?
I'm sure I'm missing something simple here - and any help much appreciated.

You shouldn't check the checkbox, but check the value of the file input field. If it is False, then you can delete the file. Otherwise it is the uploaded file. See: https://github.com/django/django/blob/339c01fb7552feb8df125ef7e5420dae04fd913f/django/forms/widgets.py#L434
# False signals to clear any existing value, as opposed to just None
return False
return upload

Let me add here my code that solved the problem - I decided to put this logic to ModelForm.clean():
class Document(models.Model):
upload = models.FileField(upload_to=document_name, blank=True)
class DocumentForm(ModelForm):
def clean(self):
cleaned_data = super(DocumentForm, self).clean()
upload = cleaned_data['upload']
if (upload == False) and self.instance.upload:
if os.path.isfile(self.instance.upload.path):
self.instance.upload.delete(False)

Related

Deal with django ImageField current value displayed in forms

I'm trying to remove the clear checkbox in Django's ImageField and remove the displayed current value of the file. Tha approach I tried is to replace the widget as proposed here How to not render django image field currently and clear stuff? but the result is that I get ValidationError :
"Upload a valid image. The file you uploaded was either not an image or a corrupted image.". Though the validation error the image get's uploaded, but I do not get redirected to the success page and an error is being rendered in the form.
What's the recommended way to remove the current value in the ImageField?
A solution that works for me, though I'm not sure how good is it.
profile_picture = forms.FileField(label='Upload profile picture', widget=forms.FileInput,validators=[validators.validate_image_file_extension], required=False)
I replaced ImageField with FileField and then added the ImageField default validator with FileInput widget and then added 'accept': 'image/*' to that widget, so the select shows only images. In this way I mimic the ImageField, but no error occurs.

Binding Multiple Files to Django ImageField Form Widget

Previously my Django form had a field that supported uploading a single file:
image = forms.ImageField(required=False, help_text=_('Optional image (2.5 MB or less)'), allow_empty_file=True)
If the user tried to edit the form after submitting it, I displayed the previously uploaded file by binding it to the form like so:
if request.method == 'POST':
...
else:
data = {'title': model.title}
file = {'image': model.file}
form = MyForm(data, file, instance=model)
To enable uploading multiple files, I've now changed the widget to:
image = forms.ImageField(required=False, help_text=_('Optional image (2.5 MB or less)'), allow_empty_file=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
This works fine when uploading the files, but I can't find a way to bind more than one file to the form when it comes to editing it.
Is there a solution using the current widget, or would it involve using a custom or multiple widgets?
Thanks.
This isn't possible with the standard widget.
A ClearableFileInput with multiple enabled provides access to the selected files via a MultiValueDict.
You can bind a MultiValueDict to an ImageField when initialising the form, but it will only ever bind the last image. This is because the widget extends FileInput, and its function value_from_datadict(self, data, files, name) returns files.get(name), rather than files.getlist(name).
So, in short, you have to come up with a custom solution. This answer helped me to see this.

How to tell if user changed file in django ModelForm

I have a django ModelForm with an image field. I want to resize the image when the user submits the form, but only if they uploaded a new image. My code looks like this:
class EditProfileForm(forms.ModelForm):
class Meta:
model = Person
fields = ['picture', '...']
def clean_picture(self):
picture = self.cleaned_data['picture']
if picture.file: #This isn't right though
#resize it
return picture
However it seems like picture.file always exists if the model being edited contains a file. I know I can check request.FILES back in the view, but that's very inelegant. Is there a better way?
In general, you can do this with self.changed_data, which returns a list of the names of the fields where the value changed. Whether there's anything special about a FileField that would interfere I don't know offhand.
In js,
In JS, set a global variable file_changed = 0.
HTML input tag < input ng-model="avatar" type="file" onchange="ChangeFile()">
Lets say ChangeFile() will provide a small preview of the file to be uploaded
In function ChangeFile set file_changed=1 once the file is selected.

How to render django form differently based on what user selects?

I have a model and a form like this:
class MyModel(models.Model):
param = models.CharField()
param1 = models.CharField()
param2 = models.CharField()
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
Then I have one drop down menu with different values and based on what value is selected I'm hiding and showing fields of MyForm. Now I have to take one step further and render param2 as a CheckboxInput widget if user selects a certain value from a drop down but in other cases it should be standard text field. So how would I do that?
I know this post is almost a year old, but it took me multiple hours to even find a post related to this topic (this is the only one I found, which came up as related when submitting my own question), so I felt the need to share my solution.
I wanted to have a form that would show and require a text field if an option from a dropdown menu matched a value stored in another model. I had a foreignKey relation between two models and I passed an instance of Model1 into the ModelForm for Model2. If a value chosen for a variable in Model2 matched a variable already set in Model1, I wanted to show and require a textfield. It was basically a "choose Other and then enter your own description" scenario.
I did not want the page to reload (I was trying to have this work in both mobile and desktop browsers with the least delay/reloads and using the same code for both), so I could not use the mentioned multiple forms loading in a view option. I started trying to do it with AJAX as suggested above when I realized I was over thinking the problem.
The answer was using JS and clean methods in the form. I added a non-required field (field1) that was not in Model2 to my Model2Form. I then hid this using jQuery and only displayed it (using jQuery) if the value of another field (field2) matched the value of the variable from Model1. To make that work, I did decide to have a hidden < span > in my template with the pk of the variable so I could easily grab it with jQuery. This jQuery worked perfectly for hiding and showing the field correctly so the user could choose the "other" value and then decided to choose a different one instead (and go back and forth endlessly).
I then used a clean method in my Model2Form for field1 that raised a ValidationError if no value was entered when the value in field2 matched my Model1 variable. I accessed that variable by using "self.other = Model1.variable" in my __ init __ method and then referencing that in the clean_field1 method.
I would have liked to have been able to accomplish this without having to hide and show a field with JS, but I think the only solutions for doing so with views or ajax caused delays/reloads that I did not want. Also, I liked the general simplicity of the method I used, rather than having to figure out how to pass partial forms back and forth through the HTTPRequest.
Update:
In my situation, I was creating entries for lost and found items and if the location where the item was found was not a provided option, then I wanted to show a textbox for the user to enter the location. I created a location object that was set as the "other" location and then displayed the textbox when that object was selected as the "found" location.
In forms.py, I added an extra CharField and use a clean method to check if the field is required and then throw a ValidationError if it wasn't filled in:
class Model2Form(forms.ModelForm):
def __init__(self, Model1, *args, **kwargs):
self.other = Model1.otherLocation
super(Model2Form, self).__init__(*args, **kwargs)
...
otherLocation = forms.CharField(
label="Location Description",
max_length=255,
required=False
)
def clean_otherLocation(self):
if self.cleaned_data['locationFound'] == self.other and not self.cleaned_data['otherLocation']:
raise ValidationError("Must describe the location.")
return self.cleaned_data['otherLocation']
Then in my JavaScript, I checked if the value of the "found" location was the "other" location (the value of which I had in a hidden span on my html page). I then used .show() and .hide() on the textbox's parent element as necessary:
$("#id_locationFound").change( function(){
if ($("#id_locationFound").val() == $("#otherLocation").attr("value")){ //if matches "other" location, display textbox; otherwise, hide textbox
$("#id_otherLocation").parent().show();
}else
$("#id_otherLocation").parent().hide();
});
Your best guess would be to trigger a "POST" request when you select something from your drop down menu.
The Value of that "POST" has to correspond your values you use to determine which field you would like to output.
Now you will actually need two forms:
class MyBaseForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
class MyDropDownForm(MyBaseForm):
class Meta:
widgets = {
'param2': Select(attrs={...}),
}
So as you can see the DropDownForm has been derived from MyBaseForm to make sure it will have all the same properties. But we have modified the widget of one of the fields.
Now you can update your view. Please note, this is untested Python + Pseudocode
views.py
def myFormView(request):
if request.method == 'POST': # If the form has been submitted...
form = MyBaseForm(request.POST)
#submit button has not been pressed, so the dropdown has triggered the submission.
#Hence we won't safe the form, but reload it
if 'my_real_submitbotton' not in form.data:
if 'param1' == "Dropdown":
form = MyDropDownForm(request.POST)
else:
#do your normal form saving procedure
else:
form = ContactForm() # An unbound form
return render(request, 'yourTemplate.html', {
'form': form,
})
This mechanism does the following:
When the form is submitted it checks if you have pressed the "submit" button or have used a dropdown onChange to trigger a submission. My solution doesn't contain the javascript code you need to trigger the submission with an onChange. I just like to provide a way to solve it.
To use the 'my_real_submitbutton' in form.data construct you will be required to name your submit button:
<input type="submit" name="my_real_submitbutton" value="Submit" />
Of course you can choose any string as Name. :-)
In case of a submit by your dropdown field you must check which value has been selected in this drop down menu. If this value satisfies the condition you want to return a Dropdown Menu you create an instance of DropDownForm(request.POST) otherwise you can leave everything as it is and rerender your template.
On the downside this will refresh your page.
On the upside it will keep all the already entered field values. So no harm done here.
If you would like to avoid the page refresh you can keep my proposed idea but you need to render the new form via AJAX.

How to bind an image to an edit form in Django?

I have the following Model:
class Listing(models.Model):
name = models.CharField(max_length=50, verbose_name="Title")
images = models.ManyToManyField('Image')
, with the ManyToManyField linking to this Image class:
class Image(models.Model):
thumb = ImageField(upload_to='images/uploads/')
number = models.PositiveSmallIntegerField()
and a corresponding ModelForm like so:
class ListingEditForm(ModelForm):
image1 = ImageField(required=False, label="Photo 1")
image2 = ImageField(required=False, label="Photo 2")
image3 = ImageField(required=False, label="Photo 3")
class Meta:
model = Listing
exclude = ('images')
The idea is to not limit the number of images that can be associated with a Listing in the backend, but at this time I only need 3 images in the form. Uploading the images works fine, but how would you go about binding the form to a Listing instance so that the images are not 'None' when one views the edit form?
Obviously, this alone won't work, because image1, image2 and image3 are only form fields, and not part of the model:
form = forms.ListingEditForm(instance=listing)
So adding a dictionary as the first parameter seems like the obvious thing to do:
form = forms.ListingEditForm({'image1': ...},instance=listing)
but what should the value of that ... be? And how do I retrieve it from the Listing instance?
I'll answer my own question, even though it's not quite the answer I was looking for. I've looked around, and as far as I know, there is no reliable way in HTML to change the contents of a File input field. So, I could be wrong, but even if you send that information with the request, Django will have no way of showing the information in the field (since it doesn't correspond to a file on the local PC).
So, my solution is simply to send the urls of the images with the request, as one normally would:
return render_to_response('edit.html', {'image1': image1_url, ...})
Then, if this information is present, I use jQuery to place the images next to the file input field in the template, and update it if the user selects a new file. It's not the best, but it works.
I'll still be glad to hear any other solutions.
I would use foreign key relation in Image, and inlineformset_factory for generating the form.
ListingEditForm = inlineformset_factory(Listing, Image, max_num=3, extra=0)
I would also add image name field in Image model. That way user will have indication of uploaded files in form display, and he will also be able to delete images if he whishes so. If you need unlimited uploads you can simply change max_num to 0 and extra to 1.
Of course that way you cannot associate one image with more then one Listing object, but if you need user to be able to delete images that is not recommended anyway.