Мультиаплоад файлов, версия N

в 16:26, , рубрики: django, multiupload, python, метки: ,

Еще с давних времен, когда интернет был молодым и медленным, самым распространенным браузером был не IE, а Mosaic, в интернет не ходили, а дозванивались… меня часто мучал вопрос — почему в файловых диалогах можно выбрать всего один файл? Почему если в форме есть три файловых поля, то в каждое из них надо тыкать? Неужели нельзя удобнее?
Шло время, появился javascript, CGI, но файлополя были все также эгоистичны. Потом появился флеш, html5, ситуация начала меняться, но… Файловое поле имеет ту-же самую суть — файл, а не файлы! Хотя есть множество обходных методов…

Итак, коллеги, как вы уже догадались, я желаю поделиться с вами еще одним «велосипедом». И я буду безумно рад, если он сделает чью-то жизнь чуточку легче. Но перед тем как продолжить, я не могу не поблагодарить создателей такого чудного инструмента, как Plupload — кросбраузерного мультиаплоадера файлов.

asv_files

Итак, знакомьтесь, asv_files — это кастомное поле для формы (в дальнейшем и для модели), позволяющее грузить в себя N файлов. Не только поле, но и некоторая обвязка, позволяющая пережить уже загруженным файлам такое события как невалидность формы и отправку ее в ремонт на дозаполнение. А также метод, легко позволяющий приаттачить эти файлы к любому инстансу любой вашей модели с помощью ContentTypes.

Создатели plupload обещают кроссбраузерность и у меня нет основания им не верить. По крайней мере в IE, Opere, FireFox'e и Хроме поле ведет себя как ему и положено.

Идеология и основные постулаты

Итак, основные постулаты, которыми я руководствовался:

  • Никаких красивостей. Я не настолько хороший телепат чтобы попасть в Ваш дизайн. Темплейт прост и доступен для модификации, главное не потерять имена классов. Текстовые-же сообщения вообще переопределимы через settings.py либо через наследование класса, если для различных файловых полей в форме нужны различные сообщения.
  • На одной web-странице может быть N форм, в каждой форме, в свою очередь, может быть M файловых полей. Форма может пытаться сабмититься бесконечное число раз.
  • Если мы можем работать через html5 — через него и работаем, если нет — в нашем распоряжении flash, silverlight, эгоистичный html4.
  • Если есть незагруженные файлы, то форму сабмитить нельзя (отключаемо).
  • Используем class-based views для большей гибкости и возможности отнаследоваться.
  • Кириллические имена файлов, имена файлов с пробелами и спецсимволами — это зло, следовательно их необходимо транслитирировать и нормализовывать. Но имя файла на пользовательской стороне часто бывает важной информацией, по-этому не теряем его, а храним до поры до времени.
  • файлы могут храниться не только локально, по-этому используем имеющийся в Django механизм Файл-сториджей. Будет активно пилиться интеграция с dropbox, google-drive и яндекс-диском.
  • django.staticfiles — это позитивно и в девелопменте и в деплое.
  • manage.py должен содержать команды для управления файлами.

Инсталляция

Сложность инсталляции поражает воображение:

$ pip install asv_files

При этом asv_files притащит за собой немного зависимостей, одна из которых (asv_media) содержит и jQuery и plupload и способна отдать их через staticfiles.

Затем вам придется подключить приложения asv_files, asv_media и django.contrib.sites в settings.py

INSTALLED_APPS = (
    .....
    'django.contrib.sites',
    .....
    'asv_media',
    'asv_files',
    .....
)

Создать при помощи manage.py необходимые модели:

$ python manage.py syncdb

И пригласить в urls.py бойцов невидимого фронта обработчик запросов:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
    .....
    url('AnY_YoUr_LiKe_PaTh/', include('asv_files.urls', namespace='asv_files')),
    .....

Подключать RPC-ответчик можно по любому URLу, важно сохранить неизменным вышеуказанный namespace.

Как использовать

Работа с формами в Django практически не зависит от того используете ли вы классические views или перешли на class-based. Суть одна:

описать форму:

from django import forms
from asv_files.fields import CTfilesFormField

class TestForm(forms.Form):
    #.....
    files = CTfilesFormField()
    #.....

использовать ее во вьюхе:

def TestView(request, ...):
    obj = Articles.objects.get(.........)
    if request.method == 'POST':
        form = TestForm(request.POST)
        if form.is_valid():
            # .....
            files = form.cleaned_data['files']
            for i in files.get_files():
                print('file {frn} saved as {fn} ({fp}) and have ID:{id}'.format(
                    frn = i.get_realname(),
                    fn = i.get_filename(),
                    fp = i.get_filepath(),
                    id = i.fileid,
                ))
                i.attach_to(obj) # attach to OBJ through ContentTypes
                #i.delete() # remove file from database and storage
            files.delete() # remove session and her files from database and storage
    else:
        form = TestForm()
    return render(request, 'template.html', {
        'form': form,
    }

не забыв правильно подключить в темплейте медиафайлы:

{% extends "base.html" %}

{% block add_to_head %}
    {{form.media}}
{% endblock %}

{% block wp %}
    <hr>
    <form method="POST" action="{% url demo:form1 %}">{% csrf_token %}
        <table>{{ form.as_table }}</table>
        <input type="submit"> <input type="reset">
    </form>
{% endblock %}

блок «add_to_head», естественно, должен существовать в Вашем базовом темплейте.

Управление через manage.py

В manage.py доступна команда uploads, с помощью которой можно управлять сесиями загрузок и временными файлами (то есть файлами которые загрузили, но форму не засабмитили, либо засабмитили, но программист забыл стереть файлы в обработчике сабмита формы):

$ ./manage.py uploads --help

Usage: ./manage.py uploads [options] <sess_ID or filename>

Managament for upload files and sessions

Options:
  --sessions            list unclosed upload sessions and it's files, if
                        exists
  --realname            display real (user's) file name near file name
  --no-files, --nofiles
                        do not display information about files
  --older=XX            use only sessions older than XX hours
  --only-uuids, --onlyuuids
                        show only session's UUIDs
  --remove              remove upload session and her files

Итак, есть две основных команды:

$ ./manage.py uploads --sessions

и

$ ./manage.py uploads --remove sessID-1 sessID-2 ... sessID-N

Первая показывает незавершенные сессии загрузок, их файлы, и прочую связанную с ними информацию.
Вторая — удалаяет из DB перечисленные сессии и соответствующие им файлы.

Ключи к команде uploads --sessions:

  • --realname — показывать не только имя файла, под которым он сохранен, но и имя файла под которым его пытался загрузить пользователь
  • --no-files — не показывать информацию о файлах. Только идентификаторы сессий
  • --only-uuids — вывести только ID-ы сессий через пробел в одну длинную строчку. Удобно использовать для последующего удаления.
  • --older=XX — показать только сессии, с возрастом (датой создания) более, чем XX часов. Часы можно указывать как целым, дак и десятичным числом (=0.25 — четверть часа)

И в заключении хочу… сказать...

Исходные тексты asv_files лежат тут.
Документация, написанная, как всегда, на скорую руку — рядом.
Небольшая демка (для разнообразия используется CBV), иллюстрирующая работу двух полей рядом — на противоположном конце континента.
А потрясающая документация на CBV — ближе, чем можно подумать

С удовольствием приму критику, багфиксы, предложения.
Спасибо за внимание.

PS: К сожалению вынужден констатировать факт, что нормальной работы в Опере и Сафари не наблюдается. Приношу свои извинения пользователям этих браузеров, принимаю рекомендации по эффективной отладке javascript'a в них. Файрбага для Сафария сильно не хватает.

Автор: xenolog


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js