Tymczasowe pliki w Django do testów i operacji na plikach w locie
Czasami w Django zachodzi potrzeba operacji na plikach w locie, np. przechwycenie wysłanego pliku z formularza, zmodyfikowanie go i podstawienie do modelu do zapisu. W testach formularzy też przyda nam się obiekt pliku, który pozwoli zwalidować formularz. ContentFile jest rozwiązaniem połowicznym - możemy np. ustawić ten obiekt jako wartość pola w modelu, ale już do formularza oczekującego na plik graficzny się nie nada. W sieci znajdziemy przykłady ze StringIO - ten obiekt sam też nie da rady bo brakuje mu metod jakie Django chce wywoływać. Jest na szczęście w Django InMemoryUploadedFile, które może przydać się do różnych operacji na plikach.
InMemoryUploadedFile to obiekt pliku, który możemy używać w Django do praktycznie dowolnych celów. Formularze, czy modele bez problemu obsłużą plik podany w postaci takiego obiektu. Próba użycia np. ContentFile na pliku graficznym może skończyć się zapisem wadliwego pliku. Użycia StringIO.StringIO wyjątkiem przy próbie wywołania nieistniejącej metody (oba rozwiązania są prezentowane w sieci dla starszych wersji Django). Na takie problemy trafiłem dodając obsługę Djangowskiego API File Storage dla zapisu grafik z ckeditora.
Rozwiązanie wygląda np. tak:import Image
import StringIO
def get_temporary_text_file():
io = StringIO.StringIO()
io.write('foo')
text_file = InMemoryUploadedFile(io, None, 'foo.txt', 'text', io.len, None)
text_file.seek(0)
return text_file
def get_temporary_image():
io = StringIO.StringIO()
size = (200,200)
color = (255,0,0,0)
image = Image.new("RGBA", size, color)
image.save(io, format='JPEG')
image_file = InMemoryUploadedFile(io, None, 'foo.jpg', 'jpeg', io.len, None)
image_file.seek(0)
return image_file
StringIO.StringIO() to klasa udająca obiekt pliku. Można do niej zapisywać dane tak jakby byłby to otwarty do zapisu plik. Przy tworzeniu tymczasowego pliku tekstowego używam metody "write". W przypadku grafiki za zapis odpowiada biblioteka PIL.
Mając gotowy plik w StringIO owijam go w InMemoryUploadedFile. Klasa ta przyjmuje jako argumenty: obiekt pliku, nazwa pola (w przypadku formularzy), nazwa pliku, typ mime, rozmiar, kodowanie. W powyższych przykładach "pomijam" nazwę pola i kodowanie dając im None. Na koniec używam seek(0) by przejść do początku pliku. Jest to wymagane do poprawnego działania takiego pliku w kodzie Django (File Storage API, używanie w formularzach).
Zastosowania
InMemoryUploadedFile może się przydać gdy chcemy wykonać jakieś operacje na pliku przed jego zapisem, gdy np. plik jest generowany przez kod (dostajemy adres URL, pobieramy i chcemy przypisać do pola w modelu lub zapisać za pomocą storage API z pominięciem pisania po lokalnym dysku) itp. W testach pozwala to uniknąć stosowania dodatkowych plików statycznych do testowania formularzy.
Zastosowanie w testach
Załóżmy że mamy prostą aplikację "testapp" z takim oto modelem:from django.db import models
class TestModel(models.Model):
image = models.ImageField(upload_to='test/', blank=True, verbose_name='Test')
from django.views.generic.edit import CreateView
from testapp.models import *
class UploadImageView(CreateView):
model = TestModel
success_url = '/'
upload_image = UploadImageView.as_view()
<form action="./" method="post" enctype="multipart/form-data">
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit" value="Add" />
</form>
import Image
import StringIO
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.urlresolvers import reverse
from django.test import TestCase
def get_temporary_text_file():
io = StringIO.StringIO()
io.write('foo')
text_file = InMemoryUploadedFile(io, None, 'foo.txt', 'text', io.len, None)
text_file.seek(0)
return text_file
def get_temporary_image():
io = StringIO.StringIO()
size = (200,200)
color = (255,0,0,0)
image = Image.new("RGBA", size, color)
image.save(io, format='JPEG')
image_file = InMemoryUploadedFile(io, None, 'foo.jpg', 'jpeg', io.len, None)
image_file.seek(0)
return image_file
class ImageUploadViewTests(TestCase):
def test_if_form_submits(self):
test_image = get_temporary_image()
response = self.client.post(reverse('upload-image'), {'image': test_image})
self.assertEqual(302, response.status_code)
def test_if_form_fails_on_text_file(self):
test_file = get_temporary_text_file()
response = self.client.post(reverse('upload-image'), {'image': test_file})
self.assertEqual(200, response.status_code)
error_message = 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
self.assertFormError(response, 'form', 'image', error_message)
Comment article