Wednesday, August 25, 2010

Test-driven development with Django ImageField

Suppose you had the following model:

class ImageGalleryAdminForm(forms.ModelForm):
    archive = forms.FileField(label="ZIP archive", required=False)

    class Meta:
        model = get_model('uploads', 'ImageGallery')
class Image(BaseFileModel):
    file = models.ImageField(upload_to=get_image_path)
    gallery = models.ForeignKey(ImageGallery, default=get_default_gallery)
How would you write test cases to verify that you could actually upload a ZIP archive? Well, here's how the Django documentation describes things:

Submitting files is a special case. To POST a file, you need only provide the file field name as a key, and a file handle to the file you wish to upload as a value. For example:

>>> c = Client()
>>> f = open('wishlist.doc')
>>>'/customers/wishes/', {'name': 'fred', 'attachment': f})
>>> f.close()
(The name attachment here is not relevant; use whatever name your file-processing code expects.)

Why can you pass a file handler inside a dictionary? You have to dig into the Django django.test.client code to understand why it's even possible.

    def post(self, path, data={}, content_type=MULTIPART_CONTENT,
             follow=False, **extra):
        Requests a response from the server using POST.                                                                                                                 
        if content_type is MULTIPART_CONTENT:
            post_data = encode_multipart(BOUNDARY, data)
Notice that content_type is by default set to MULTIPART_CONTENT. It invokes the function encode_multipart(), which has these particular important lines:
def encode_multipart(boundary, data):
    Encodes multipart POST data from a dictionary of form values.  

    The key will be used as the form data name; the value will be transmitted                                                                                           
    as content. If the value is a file, the contents of the file will be sent                                                                                           
    as an application/octet-stream; otherwise, str(value) will be sent.                                                                                                 
    # Not by any means perfect, but good enough for our purposes.                                                                                                       
    is_file = lambda thing: hasattr(thing, "read") and callable(
    for (key, value) in data.items():
        if is_file(value):
            lines.extend(encode_file(boundary, key, value))
Django creates a lambda function that determines if the file handler you pass-in has a read() function. If it does, it will consider the dictionary value as a file. It will then invoke encode_file() and create a MIME-encoded submission.

Try it out for yourself at the Python interpreter:

python shell
>>> from django.test.client import BOUNDARY
>>> f = open("/tmp/")
>>> django.test.client.encode_multipart(BOUNDARY, {'d' : f})

You should start to see something like the following:
'--BoUnDaRyStRiNg\r\nContent-Disposition: form-data; name="d"; filename=""\r\nContent-Type: application/octet-stream\r\n\r\nPK\x03\x04\x14\x00\x00\x00\x08\x00\'G\xbc<\xcb\x99\x05\xbb:\xaa\x00\x00\xb8\xaa\x00\x00\n\x00\x00\x00dragon.jpg\

No comments:

Post a Comment