Django предоставляет отличные средства для быстрого построения back-end без лишних телодвижений. Однако создание на его основе галлереи связано с множеством ручной работы по загрузке каждого файла в отдельности.

Решений у этой проблемы несколько:
  1. загрузка архива и его распаковка на сервере,
  2. использование специального поля,
  3. имитация множественных post-запросов.

Я выбрал последний вариант, так как в этом случае нужно внести минимум изменений. Все операции выполняются на клиенте. Использовать для этого будем swfuploader.

Задумка проста, модифицируем с��андартный интерфейс добавления, так что бы перехватывать событие сохранения объекта. Заполненные поля будем использовать в качестве шаблона для создания множества объектов для выбранных файлов.

Итак выполним следующие действия.

  1. Выкачиваем набор примеров swfupload: http://swfupload.googlecode.com/files/SWFUpload%20v2.2.0.1%20Samples.zip.
  2. из него нам падобятся следующие файлы:

  3. Кладем их в медиа директорию djangosite/media/js/swfupload.
  4. Прописываем js в djangosite/gallery/admin.py:
    from django.contrib import admin
    from djangosite.gallery.models import Image

    class ImageAdmin(admin.ModelAdmin):
      class Media:
        js = (
           '/media/js/jquery.min.js',
           '/media/js/swfupload/swfupload.js',
           '/media/js/swfupload/swfupload.queue.js',
           '/media/js/swfupload/swfupload.cookies.js',
           '/media/js/multiupload.js',
        )
      
    admin.site.register(Image,ImageAdmin)


    * This source code was highlighted with Source Code Highlighter.
  5. Создаем управляющий js-код djangosite/media/js/multiupload.js

    jQuery().ready(function(e){<br>  DjangoMultiUpload.init();<br>});<br><br><br>var DjangoMultiUpload = new function(){<br>  <br>  // Общие переменные<br>  this.swfu = null;<br>  this.gui_submit = null;<br>  this.old_upload = null;<br>  this.busy = false;<br>  <br>  // Параметры для swfupload<br>  this.debug = false;<br>  this.flash_url = "/media/js/swfupload/swfupload.swf";<br>  this.upload_url = "";<br><br>  this.file_size_limit = "100 MB";<br>  this.file_types = "*.jpg;*.gif;*.jpeg";<br>  this.file_types_description = "Файлы изображений";<br>  this.file_upload_limit = 100;<br>  this.file_queue_limit = 0;<br>  this.custom_settings = {<br>    progressTarget : "fsUploadProgress",<br>    cancelButtonId : "btnCancel"<br>  };<br>  this.button_image_url = "/media/js/swfupload/XPButtonUploadText_61x22.png",<br>  this.button_width = "61",<br>  this.button_height = "22",<br>  this.button_placeholder_id = "spanButtonPlaceHolder",  <br>  this.file_post_name = null;<br><br>  //  Проверяем ос на Linux (проблемы совместимости).<br>  //  Проверяем количество input file, <br>  //    Должен быть один. <br>  //    деолжен быть обязательным<br>  //  Проверяем не редактирование ли это?<br>  //  Готовим Ui.<br>  //  Инициализируем swfupload<br>  this.init = function(e){<br><br>    if (navigator.appVersion.indexOf("Linux")!=-1)<br>      return false;<br>    <br>    this.old_upload = jQuery('input[type=file]');<br>    <br>    if (this.old_upload.size() != 1)<br>      return false;<br>    <br>    if (!jQuery("label[for=" + this.old_upload.attr('id') + "]").hasClass('required'))<br>      return false;<br><br>    if (this.old_upload.prev().get(0).tagName == "BR")<br>      return false;<br>    <br>    this.gui();<br>    <br>    this.swfu = new SWFUpload(this);<br>    <br>  }<br>  <br>  // Скрываем оригинальный input file<br>  // Добавляем холдер для контрола swfuploader<br>  // Добавляем перехватчик отправки форм<br>  // Скрываем лишние кнопки<br>  this.gui = function(){<br>    <br>    this.file_post_name = this.old_upload.attr('name');<br>    <br>    jQuery("<span id='spanButtonPlaceHolder'></span>").insertAfter(this.old_upload);<br>    <br>    <br>    <br>    jQuery('form').submit(function(e){<br>      DjangoMultiUpload.submit(e);<br>      return false;<br>    });<br>    <br>    this.old_upload.hide();<br>    jQuery('input[name=_continue]').hide();<br>    jQuery('input[name=_addanother]').hide();<br>    <br>  }<br>  <br>  // Проверка на занятость<br>  // Проверка обязательные поля<br>  // Запуск закачки<br>  this.submit = function(e){<br>    <br>    if (this.busy){<br>      alert('Uploading. Please wait.');<br>      return false;<br>    }<br>  <br>    if (this.prepare_post_data()){<br>      <br>      jQuery('input[name=_save]').attr('value',0);<br>          <br>      this.busy = true;<br>          <br>      this.swfu.startUpload();<br>    }<br>    <br>    return false;<br>    <br>  }<br>  <br>  // Получаем все лейблы<br>  // По ним находим поля и прописываем их в postdata swfuploader<br>  // Если поле обязательное и не зполнено меняем цвет и возварщаем false<br>  // При переборе игнорируем input-file<br>  // На случай linux плеера передаем имя поля с файлом<br>  this.prepare_post_data = function (){<br>    <br><br>    <br>    var labels = jQuery('label');<br>    <br>    if (!labels.size())<br>      return 0;<br>    <br>    var field = null;<br>    var label = null;<br>    var success = true;<br>    <br>    for (i = 0 ; i < labels.size();i++ )<br>    {<br>      <br>      label = jQuery(labels.get(i))<br>      <br>      if (label.attr('for') == this.old_upload.attr('id'))<br>        continue;<br><br>      field = jQuery('#' + label.attr('for'));<br>      <br>      if (label.hasClass('required') && !field.attr('value')){<br>        label.css('color','red !important');<br>        success = false;<br>      }<br><br>      this.swfu.addPostParam(field.attr('name'), field.attr('value'));<br>      <br>    }<br>    <br>    this.swfu.addPostParam('file_post_name', this.file_post_name)<br>    <br>    return success;<br>    <br>  }<br>  <br>  // По завершении загрузки файла<br>  this.upload_complete_handler = function(file)<br>  {<br>    jQuery('input[name=_save]').attr('value', parseInt(jQuery('input[type=submit]').attr('value'))+1)<br>  };<br>  <br>  // По завершении загрузки всех файлов идем к списку объектов<br>  this.queue_complete_handler = function(numFilesUploaded)<br>  {<br>    <br>    this.busy = false;<br>    <br>    if (!this.settings.debug){<br>      window.location="../";<br>    }<br>  };  <br>  <br>  // Прочие события<br>  this.file_queued_handler = function(file){};<br>  this.file_queue_error_handler = function(file, errorCode, message){};<br>  this.file_dialog_complete_handler = function(numFilesSelected, numFilesQueued){};<br>  this.upload_start_handler = function(file){};<br>  this.upload_progress_handler = function(file, bytesLoaded, bytesTotal){};<br>  this.upload_success_handler = function(file, serverData){};<br>  this.upload_error_handler = function(file, errorCode, message){};<br>  <br>};<br><br>      <br><br>      <br><br>* This source code was highlighted with Source Code Highlighter.


  6. у меня повсюду в админке используется jQuery. Если у вас его нет, так же кладем djangosite/media/js/jquery.min.js.
  7. А теперь немного грязнохакерства. В djangosite/gallery/middleware.py прописываем:

    from django.conf import settings

    class MultiUploadHacksMiddleware(object):
      def process_request(self, request):

        # Некоторые flash плееры не отправляют или отправляют неправильный sessionid
        # Берем его из POST
        if request.POST.has_key(settings.SESSION_COOKIE_NAME):
          request.COOKIES[settings.SESSION_COOKIE_NAME] = \
            request.POST[settings.SESSION_COOKIE_NAME]

        # Другие не умеют менять название POST переменной с файлом
        # Возьмем его из заранее подготовленной
        if request.POST.has_key("file_post_name") and request.FILES.has_key("Filedata"):
          request.FILES[request.POST["file_post_name"]] = request.FILES["Filedata"]
          del request.FILES["Filedata"]


    * This source code was highlighted with Source Code Highlighter.


  8. Добавляем наши хаки в settings.py

    MIDDLEWARE_CLASSES = (
      'djangosite.gallery.middleware.MultiUploadHacksMiddleware',
      'django.middleware.common.CommonMiddleware',
      'django.contrib.sessions.middleware.SessionMiddleware',
      'django.contrib.auth.middleware.AuthenticationMiddleware',
    )


    * This source code was highlighted with Source Code Highlighter.



Готово, теперь наши пользователи могут грузить фотки пачками. Можно пойти дальше — добавить прогресс бар и прочие навороты, но в моем случае в этом не было необходимости.

Ограничения:
  • у модели должно быть только одно поле — файл,
  • оно должно быть обязательным,
  • прочие поля (кроме id, разумеется) не должны быть уникальными,


В идеале хотелось что бы:
  • flash-плеер сразу брал правильные cookie (django все равно ругается но файлы создает),
  • linux flash-плеер поддерживал flie_post_name,
  • был метод openDialog() в swfupload для использования собственных контролов.


На всякий случай демка.