J'ai une application Django avec une vue qui accepte un fichier à télécharger. Utilisation de Django REST I ') m sous-classer APIView et implémenter la méthode post () comme ceci:
class FileUpload(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
try:
image = request.FILES['image']
# Image processing here.
return Response(status=status.HTTP_201_CREATED)
except KeyError:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'detail' : 'Expected image.'})
Maintenant, j'essaie d'écrire quelques tests pour s'assurer que l'authentification est requise et qu'un fichier téléchargé est réellement traité.
class TestFileUpload(APITestCase):
def test_that_authentication_is_required(self):
self.assertEqual(self.client.post('my_url').status_code, status.HTTP_401_UNAUTHORIZED)
def test_file_is_accepted(self):
self.client.force_authenticate(self.user)
image = Image.new('RGB', (100, 100))
tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')
image.save(tmp_file)
with open(tmp_file.name, 'rb') as data:
response = self.client.post('my_url', {'image': data}, format='multipart')
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
Mais cela échoue lorsque le framework REST tente de coder la demande
Traceback (most recent call last):
File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/Django/utils/encoding.py", line 104, in force_text
s = six.text_type(s, encoding, errors)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/vagrant/webapp/myproject/myapp/tests.py", line 31, in test_that_jpeg_image_is_accepted
response = self.client.post('my_url', { 'image': data}, format='multipart')
File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site- packages/rest_framework/test.py", line 76, in post
return self.generic('POST', path, data, content_type, **extra)
File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/rest_framework/compat.py", line 470, in generic
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/Django/utils/encoding.py", line 73, in smart_text
return force_text(s, encoding, strings_only, errors)
File "/home/vagrant/.virtualenvs/myapp/lib/python3.3/site-packages/Django/utils/encoding.py", line 116, in force_text
raise DjangoUnicodeDecodeError(s, *e.args)
Django.utils.encoding.DjangoUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 118: invalid start byte. You passed in b'--BoUnDaRyStRiNg\r\nContent-Disposition: form-data; name="image"; filename="tmpyz2wac.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff[binary data omitted]' (<class 'bytes'>)
Comment puis-je faire en sorte que le client de test envoie les données sans essayer de les décoder en UTF-8?
Lors du test de téléchargement de fichiers, , vous devez passer l'objet de flux dans la demande, pas les données .
Cela a été souligné dans les commentaires par @ arocks
Passez {'image': fichier} à la place
Mais cela n'expliquait pas complètement pourquoi c'était nécessaire (et ne correspondait pas non plus à la question). Pour cette question spécifique, vous devriez faire
from PIL import Image
class TestFileUpload(APITestCase):
def test_file_is_accepted(self):
self.client.force_authenticate(self.user)
image = Image.new('RGB', (100, 100))
tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg')
image.save(tmp_file)
tmp_file.seek(0)
response = self.client.post('my_url', {'image': tmp_file}, format='multipart')
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
Cela correspondra à une requête Django standard, où le fichier est passé en tant qu'objet de flux, et Django REST Lorsque vous passez simplement les données du fichier, Django et Django REST Framework l'interprète comme une chaîne, ce qui cause des problèmes car il attend un flux.
Et pour ceux qui viennent ici à la recherche d'une autre erreur courante, pourquoi les téléchargements de fichiers ne fonctionneront tout simplement pas, mais les données de formulaire normales: s'assureront de définir format="multipart"
lors de la création de la demande .
Cela donne également un problème similaire, et a été souligné par @ RobinElvin dans les commentaires
C'est parce que je manquais format = 'multipart'
Utilisateurs de Python 3: assurez-vous que open
le fichier dans mode='rb'
(lecture, binaire). Sinon, lorsque Django appelle read
sur le fichier, le utf-8
le codec commencera immédiatement à s'étouffer. Le fichier doit être décodé en binaire et non en utf-8, ascii ou tout autre encodage.
# This won't work in Python 3
with open(tmp_file.name) as fp:
response = self.client.post('my_url',
{'image': fp},
format='multipart')
# Set the mode to binary and read so it can be decoded as binary
with open(tmp_file.name, 'rb') as fp:
response = self.client.post('my_url',
{'image': fp},
format='multipart')
Ce n'est pas si simple de comprendre comment le faire si vous voulez utiliser la méthode PATCH, mais j'ai trouvé la solution dans cette question .
from Django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
with open(tmp_file.name, 'rb') as fp:
response = self.client.patch(
'my_url',
encode_multipart(BOUNDARY, {'image': fp}),
content_type=MULTIPART_CONTENT
)
Pour ceux sous Windows, la réponse est un peu différente. Je devais faire ce qui suit:
resp = None
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file:
image = Image.new('RGB', (100, 100), "#ddd")
image.save(tmp_file, format="JPEG")
tmp_file.close()
# create status update
with open(tmp_file.name, 'rb') as photo:
resp = self.client.post('/api/articles/', {'title': 'title',
'content': 'content',
'photo': photo,
}, format='multipart')
os.remove(tmp_file.name)
La différence, comme indiqué dans cette réponse ( https://stackoverflow.com/a/23212515/7235 ), le fichier ne peut pas être utilisé après sa fermeture dans Windows. Sous Linux, la réponse de @ Meistro devrait fonctionner.