Reputation: 4433
I thought my problem was https://github.com/encode/django-rest-framework/issues/937 which should have been fixed by https://github.com/encode/django-rest-framework/pull/1003 but it appears, whether I send in None or empty string, DRF isn't happy.
I'm using Django 1.11.6 and DRF 3.7.7
class Part(models.Model):
image = models.ImageField(null=True, blank=True)
class PartSerializer(serializers.ModelSerializer):
class Meta:
model = Part
fields = ('id', 'image')
class PartDetail(generics.RetrieveUpdateAPIView):
queryset = Part.objects.all()
serializer_class = PartSerializer
parser_classes = (MultiPartParser, FormParser)
# put image, works fine
with tempfile.NamedTemporaryFile(suffix='.jpg') as fp:
image = Image.new('RGB', (100, 200))
image.save(fp)
fp.seek(0)
data = {'image': fp}
self.client.put('/path/to/endpoint', data, format='multipart')
# clear image, attempt #1
data = {'image': None}
self.client.put('/path/to/endpoint', data, format='multipart')
AssertionError: {'image': ['The submitted data was not a file. Check the encoding type on the form.']}
# clear image, attempt #2
data = {'image': ''}
self.client.put('/path/to/endpoint', data, format='multipart')
AssertionError: <ImageFieldFile: None> is not None
Upvotes: 10
Views: 5602
Reputation: 111
I ran into something like this trying to write an Angular app that contacts a Django system through Django REST framework. DRF automatically generates forms for updating objects. If there's a FileField
on an object and you don't upload a file into the update form when you submit it, the framework can automatically remove a previously uploaded file and leave the object with no file at all. The object's field is then null. I wanted my app to have this capability, that is, objects can have an attached file, but it isn't required, and a file can be attached and later removed. I tried to accomplish the removal by constructing a FormData
object and sending it as a PUT request, but I couldn't figure out exactly what value to specify for the file field to make DRF delete the previously uploaded file, like what happens in DRF's automatically generated forms.
These didn't work:
let fd = new FormData();
fd.set('my_file', null); // TypeScript wouldn't let me do this
fd.set('my_file', ''); // Same error as your attempt #2
fd.set('my_file', new Blob([]); // Error about an empty file
What finally worked was
fd.set('my_file', new File([], ''));
which apparently means an empty file with no name. With that, I could send a PUT request that deletes the file attached to the object and leaves the resulting FileField
null:
this.http.put<MyRecord>(url, fd);
where this.http
is a an Angular HttpClient
. I'm not sure how to construct such a PUT request in Python.
To leave the file in place, don't set anything for 'my_file'
on the FormData
.
On the Django side, I was using a subclass of ModelSerializer
as the serializer, and the underlying FileField in the Model had options blank=True, null=True.
Upvotes: 5
Reputation: 11363
This is covered in the docs for FileField.delete
I would create an update
method on your serializer that would clear the image using the ORM call
def update(self, instance, validated_data):
instance.part.delete(save = True)
or something similar.
Upvotes: 1
Reputation: 19
Please make sure that I reimplement your code.
Django version:2.0
DRF version:3.7.7 as you say in the question.
Firstly, the conclusion is that the method in your attempt #2 is right in my tests. However, attempt #1 has no effect and my error is the same as yours. However, the images saved before is not deleted in the filesystem. The followings are my TestCase. Make sure that you use the client provided by the DRF rather than the django itself.
from PIL import Image
from django.test import Client
import io
import os
import unittest
from rest_framework.test import APIClient
class PutTests(unittest.TestCase):
def generate_photo_file(self):
file = io.BytesIO()
image = Image.new('RGBA', size=(100, 100), color=(155, 0, 0))
image.save(file, 'png')
file.name = 'test222.png'
file.seek(0)
return file
def test_imagetest(self):
"""
test put Image object
:return:
"""
self.client = Client()
print(os.path.join(os.path.dirname( os.path.dirname(__file__) ), '2018s.jpg'))
f = self.generate_photo_file()
data={
'image': f,
}
# if you want to insert image in the testCase please let class PartDetail inherits from ListCreateAPIView.
self.client.post('/puttestimage/', data=data, format='multipart')
def test_image2test(self):
# this won't work in my test.
self.client = APIClient()
data = {'image': None}
self.client.put('/puttestimage/1/', data=data, format='multipart')
#
def test_image3test(self):
# this will work in my test.
self.client = APIClient()
data = {'image': ''}
self.client.put('/puttestimage/2/', data=data, format='multipart')
The basic idea is that I first insert one image into the database, they I clear this image by your attempt#1 and #2.
class PartSerializer(serializers.ModelSerializer):
# image = serializers.ImageField(max_length=None, allow_empty_file=True, allow_null=True, required=False)
class Meta:
model = Part
fields = ('id', 'image')
class PartDetail(generics.RetrieveUpdateAPIView):
queryset = Part.objects.all()
serializer_class = PartSerializer
parser_classes = (MultiPartParser, FormParser)
urlpatterns = [
url(r'puttestimage/(?P<pk>[0-9]+)/', PartDetail.as_view(),name='imageput'),
url(r'puttestimage/', PartDetail.as_view(), name='imageputs'),
]
Upvotes: 0
Reputation: 3378
You have to specify the image field explicitly to allow it to be null.
use this:
class PartSerializer(serializers.ModelSerializer):
image = serializers.ImageField(max_length=None, allow_empty_file=True, allow_null=True, required=False)
class Meta:
model = Part
fields = ('id', 'image')
check docs for more details.
Upvotes: 8