Tymczasowe pliki w Django do testów i operacji na plikach w locie

Wykorzystanie InMemoryUploadedFile w kodzie aplikacji Django

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')
Do tego widok:
from django.views.generic.edit import CreateView

from testapp.models import *

class UploadImageView(CreateView):
    model = TestModel
    success_url = '/'

upload_image = UploadImageView.as_view()
I formularz (szablon):
<form action="./" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <table>
        {{ form }}
    </table>
    <input type="submit" value="Add" />
</form>
Testy z wykorzystaniem takich tymczasowych plików wyglądałyby następująco:
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)
Odpalając testy zobaczymy że oba przechodzą - jeżeli podamy grafikę - formularz przejdzie, jeżeli plik tekstowy - zwróci błąd.
blog comments powered by Disqus

Kategorie

Strony