SWFUpload in Django integrieren

Mit dem SWFUpload Cookies Plugin und einer Django Middleware, lässt sich der Flash-basierte SWFUpload einfach und komfortabel in Django integrieren. Der Vorteil der ganze Prozedur ist für die Benutzer der Seite ein wesentlich einfacher Upload Mechanismus, welcher es erlaubt mehrere Dateien gleichzeitig hochzuladen. Mit diesem kleinen Tutorial möchte ich euch zeigen wie das ganze funktioniert.

Django Logo

Ein paar Worte zu Beginn

Warum überhaupt ein ganzes Tutorial, mehr als einwenig Javascript und Python Code ist das ganze ja nicht. Eigentlich ja, jedoch gibt es genau dort ein kleines Problem. Django’s Authentifizierungssystem gibt bei jeder Server Anfrage den Session-Token via Browser Cookie weiter. SWFUpload hingegen sendet den sessionid Cookie nicht weiter. Somit sieht Django im View nur das request.user ein AnonymousUser ist, egal ob der Uploader authentifiziert war oder nicht.

Um das Problem zu beheben benötigen wir das SWFUpload Cookie Plugin, was nicht mehr als ein wenig JavaScript Code ist, welcher die aktuellen Cookies hinzufügt. Dann werden wir eine Middleware in Django schreiben welche uns den Session Token extrahiert und zurück zu request.COOKIES schreibt.

Vorbereitung

Ich zeige das ganze Vorgehen nun bei dem Beispiel einer (einfachen) Bildergalerie in Django und ich gehe davon aus das euer Webserver statische Dateien wie in settings.MEDIA_ROOT ausliefert und diese im Web via settings.MEDIA_URL zugänglich macht (siehe Django Dokumentation).

Zuerst einmal unser Model in gallery.models

from django.db import Models
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from tagging.fields import TagField
from tagging.models import Tag
from category.models import Category

def get_image_path(instance, filename):
    '''Create dynamic image upload path for every gallery'''
    return 'gallery/%s/%s' % (str(instance.album.slug), filename)

class Album(models.Model):
    '''Model for a gallery'''
    name = models.CharField(max_length=150, verbose_name=_('Name'))
    slug = models.SlugField(max_length=150, unique=True)
    content = models.TextField(verbose_name=_('Beschreibung'), blank=True, null=True)

    user = models.ForeinKey(User, related_name='user')
    category = models.ForeignKey(Category, related_name='category', verbose_name=_('Kategorie')

    def __unicode__(self):
        return self.name

    @models.permalink
    def get_absolute_url(self):
        return 'gallery_detail', (), {'slug': self.slug}

    class Meta:
        verbose_name = _(u'Fotoalbum')
        verbose_name_plural = _(u'Fotoalben')

class Picture(models.Model):
    '''Model for images. Every Image must have a gallery.'''
    album = models.ForeignKey(Album, related_name='album')

    image = models.ImageField(max_length=180, upload_to=get_image_path, verbose_name=_('Bild'))
    content = models.TextField(verbose_name=_('Beschreibung'), blank=True, null=True)
    tags = TagField()

    def set_tags(self, tags):
        Tag.objects.update_tags(self, tags)

    def get_tags(self, tags):
        return Tag.objects.get_for_object(self)

    def __unicode__(self):
        return '%s Picture #%d'% (self.album.name, self.id)

    class Meta:
        verbose_name = _(u'Foto')
        verbose_name_plural = _(u'Fotos')

Nun gehen wir davon aus, das in unser View per gallery_add erreicht wird. Will heißen in der urls.py wird der view mit diesem Name versehen. Bevor wir uns jedoch um den View kümmern integrieren wir erst einmal SWFUpload ins Template (bei mir gallery/add.html). Also zunächst einmal das Core Package von SWFUpload downloaden, Archiv entpacken und die Dateien Flash/swfupload.swfswfupload.js und plugins/swfupload.cookies.js ins settings.MEDIA_ROOT Verzeichnis verschieben. Im Template selbst nun die JavaScript Dateien einfügen.

SWFUpload integrieren

Der nächste Schritt ist SWFUpload mit ein klein bisschen JavaScript verfügbar zu machen. In der Dokumentation von SWFUpload ist vieles weitaus ausführlicher beschrieben, wie ich das hier mache, ich beschränke mich auf wesentliche.

var swfu;

    SWFUpload.onload = function () {
        var settings = {
            flash_url: '{{ MEDIA_URL }}swfupload.swf',
            upload_url: '{% url gallery_add slug=slug %}',
            file_size_limit: '10 MB',
            file_types: '*.*',
            file_types_description: 'Bilddateien',
            file_upload_limit: 100,
            file_queue_limit: 0,
            custom_settings: {
                progressTarget : 'fsUploadProgress',
                cancelButtonId : 'btnCancel'
            },
            post_params: {
                'csrfmiddlewaretoken': '{{ csrf_token }}',
                'slug': '{{ slug }}'
            },
            debug: false,

            // Button Settings
            button_placeholder_id: "spanButtonPlaceholder",
            button_width: 128,
            button_height: 30,
            button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT,
            button_cursor: SWFUpload.CURSOR.HAND,

            // The event handler functions are defined in handlers.js
            swfupload_loaded_handler: swfUploadLoaded,
            file_queued_handler: fileQueued,
            file_queue_error_handler: fileQueueError,
            file_dialog_complete_handler: fileDialogComplete,
            upload_start_handler: uploadStart,
            upload_progress_handler: uploadProgress,
            upload_error_handler: uploadError,
            upload_success_handler: uploadSuccess,
            upload_complete_handler: uploadComplete,
            queue_complete_handler: queueComplete,  // Queue plugin event

            // SWFObject settings
            minimum_flash_version: "9.0.28",
            swfupload_pre_load_handler: swfUploadPreLoad,
            swfupload_load_failed_handler: swfUploadLoadFailed
        };

        swfu = new SWFUpload(settings);
    }

Der Parameter flash_url muss auf die swfupload.url verweisen, der Parameter upload_url verweist demnach auf unseren View, den ich später vorstelle. Was zu beachten ist, die URL die zum View führt wird via Slug weitergegeben.

Wie man bereits erkennt hab ich hier custom_settings angegeben und die ganzen Handler-Funktionen ausgelagert. Aber es reicht auch ein minimales Setup wie in der Dokumenation oder Demos beschrieben.

Wichtig sind jedoch die Parameter in post_params! Hier geben wir den csrf_token sowie unseren Slug via POST mit. Das ist wichtig, sonst wird Django böse wenn wir im POST ohne Cross-Site-Request-Forgey – Token ankommen!

Hier meine komplette gallery/add.html:

{% extends "base.html" %}
{% load i18n %}

{% block title %}{% trans "Bilder hinzufügen | fha-django-gallery" %}{% endblock %}

{% block extrahead %}
    <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}swfupload.css" />
    <script type="text/javascript" src="{{ MEDIA_URL }}swfupload.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }}swfupload.queue.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }}swfupload.swfobject.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }swfupload.cookies.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }swfupload.fileprogress.js"></script>
    <script type="text/javascript" src="{{ MEDIA_URL }}swfupload.handlers.js"></script>
    <script type="text/javascript">
    var swfu;

    SWFUpload.onload = function () {
        var settings = {
            flash_url: '{{ MEDIA_URL }swfupload.swf',
            upload_url: '{% url gallery_add slug=slug %}',
            file_size_limit: '10 MB',
            file_types: '*.*',
            file_types_description: 'Bilddateien',
            file_upload_limit: 100,
            file_queue_limit: 0,
            custom_settings: {
                progressTarget : 'fsUploadProgress',
                cancelButtonId : 'btnCancel'
            },
            post_params: {
                'csrfmiddlewaretoken': '{{ csrf_token }}',
                'slug': '{{ slug }}'
            },
            debug: false,

            // Button Settings
            button_placeholder_id: "spanButtonPlaceholder",
            button_width: 128,
            button_height: 30,
            button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT,
            button_cursor: SWFUpload.CURSOR.HAND,

            // The event handler functions are defined in handlers.js
            swfupload_loaded_handler: swfUploadLoaded,
            file_queued_handler: fileQueued,
            file_queue_error_handler: fileQueueError,
            file_dialog_complete_handler: fileDialogComplete,
            upload_start_handler: uploadStart,
            upload_progress_handler: uploadProgress,
            upload_error_handler: uploadError,
            upload_success_handler: uploadSuccess,
            upload_complete_handler: uploadComplete,
            queue_complete_handler: queueComplete,  // Queue plugin event

            // SWFObject settings
            minimum_flash_version: "9.0.28",
            swfupload_pre_load_handler: swfUploadPreLoad,
            swfupload_load_failed_handler: swfUploadLoadFailed
        };

        swfu = new SWFUpload(settings);
    }
    </script>
{% endblock %}

{% url gallery_add slug=slug as gallery_add %}

{% block breadcrumbs %}
    {% blocktrans %}
        Sie befinden sich hier: <a href="/">fha-django-gallery</a> »
        <a href="/account/profile/">Account</a> »
        <a href="{{ gallery_add }}">Bilder hinzufügen</a>
    {% endblocktrans %}
{% endblock %}

{% block content %}
    <h2>{% trans "Bilder zur Galerie hinzufügen" %}</h2>
    <p>{% trans "Um Bilder zur Galerie hinzuzufügen, wählen sie diese bitte aus und klicken anschließend auf Weiter" %}</p>
    <p><a href="{% url gallery_edit slug=slug %}" class="button blue">{% trans "Weiter" %}</a></p>
    <div id="divSWFUploadUI">
        <div class="fieldset flash" id="fsUploadProgress">
            <span class="legend">Warteschlange</span>
        </div>
        <p id="divStatus">0 Dateien hochgeladen</p>
        <p>
            <span id="spanButtonPlaceholder"></span>
            <button id="btnUpload" class="red" type="button">Dateien auswählen</button>
            <button id="btnCancel" type="button" disabled="disabled">Uploads abbrechen</button>
        </p>
    </div>
    <noscript style="background-color: #FFFF66; border-top: solid 4px #FF9966; border-bottom: solid 4px #FF9966; margin: 10px 25px; padding: 10px 15px;">
        {% trans "We're sorry. SWFUpload could not load. You must have JavaScript enabled to enjoy SWFUpload." %}
    </noscript>
    <div id="divLoadingContent" class="content" style="background-color: #FFFF66; border-top: solid 4px #FF9966; border-bottom: solid 4px #FF9966; margin: 10px 25px; padding: 10px 15px; display: none;">
        {% trans "SWFUpload is loading. Please wait a moment..." %}
    </div>
    <div id="divLongLoading" class="content" style="background-color: #FFFF66; border-top: solid 4px #FF9966; border-bottom: solid 4px #FF9966; margin: 10px 25px; padding: 10px 15px; display: none;">
        {% trans "SWFUpload is taking a long time to load or the load has failed.  Please make sure that the Flash Plugin is enabled and that a working version of the Adobe Flash Player is installed." %}
    </div>
    <div id="divAlternateContent" class="content" style="background-color: #FFFF66; border-top: solid 4px #FF9966; border-bottom: solid 4px #FF9966; margin: 10px 25px; padding: 10px 15px; display: none;">
        {% blocktrans %}
            We're sorry. SWFUpload could not load. You may need to install or upgrade Flash Player. Visit the <a href="http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash">Adobe website</a> to get the Flash Player.
        {% endblocktrans %}
    </div>
{% endblock %}

Schreiben der Middleware

Als vorletzten Schritt müssen wir eine Middleware in Django schreiben. Keine Sorge klingt schlimmer wie es eigentlich ist ;). In der Middleware müssen wir mehrere Dinge beachten: Den Slug extrhahieren, genauso mit dem csrf_token fungieren und ganz wichtig die sessionid aus dem POST rausholen und in request.COOKIES schreiben. Das ganze am Besten unter gallery/middleware.py schreiben.

Hier nun die komplette Middleware:

"""This middleware etracts slug, csrf_token and sessionid and gives them django back."""

from django.conf import settings
from django.core.urlresolvers import reverse

class SWFUploadMiddleware(object):
    def process_request(self, request):
        if request.POST.has_key('slug'):
            if (request.method == 'POST') and (request.path == reverse('gallery_add', kwargs={'slug': request.POST['slug']})):
                if request.POST.has_key(settings.SESSION_COOKIE_NAME):
                    request.COOKIES[settings.SESSION_COOKIE_NAME] = request.POST[settings.SESSION_COOKIE_NAME]
                if request.POST.has_key('csrftoken'):
                    request.COOKIES['csrftoken'] = request.POST['csrftoken']

Wie gesagt nicht viel Code und relativ einfach ist es auch. Jedoch müssen wir die Middleware noch in die settings.py einfügen damit Django diese auch kennt. Das ganze sollte am Besten an die zweite Stelle der Middlewares eingefügt werden.

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'gallery.middleware.SWFUploadMiddleware', #adds our new middleware
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
)

Den Upload verarbeiten

So als letztes brauchen wir noch den View der unsere hochgeladenen Bilder entgegen nimmt und verarbeitet. Selbstverständlich funktioniert das nur wenn man angemeldet ist… wär ja schrecklich wenn jeder User irgendwas hochladen dürfte…

Das ganze ist nicht besonders kompliziert, Django typisch halt. Die Dateien kommen in request.FILES an, über diese müssen wir iterieren und daraus ein Objekt erstellen.

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.middleware.csrf import get_token
from gironimo.gallery.models import Album, Picture

@login_required
def add(request, slug):
    '''Adds new images to a given gallery, only authenticated access'''
    if request.method == 'POST':
        album = Album.objects.get(slug=slug)
        for field_name in request.FILES:
            image = request.FILES[field_name]

            picture = Picture.objects.create(album=album, image=image)
            picture.save()

        # say ok to SWFUploader
        return HttpResponse("ok", mimetype="text/plain")

    else:
        return render_to_response('gallery/add.html', {
            'slug': slug,
            'csrf_token': get_token(request)
        }, context_instance=RequestContext(request))

Das wars! Wichtig ist nur das wir den csrf_token mitgeben und SWFUpload ein HttpResponse zurück geben, damit er weiß das alles OK war.

Nun sollte das ganze bei euch wunderbar funktionieren, jedoch bleibt noch viel Arbeit das ganze schön aussehen zu lassen denn Flash Sachen nerven beim designen.

Sollte ich irgendwo einen Fehler gemacht haben oder Blödsinn aufgeschrieben haben, sagt mir das wär echt super. Auch für Verbesserungsvorschläge oder andere Ideen wäre ich dankbar (natürlich ist ein Danke auch super ;))