Архив рубрики: Django

Django в одном файле!

Я тут размышлял на тему зачем нужен Flask (или другие микрофреймворки). И пришел к мысли, что единственный повод использовать Flask — если приложение настолько маленькое, что умещается в одном файле. А ведь в Django пустой проект — это уже нагромождение из нескольких файлов и папок. Для совсем крошечного приложения это пожалуй излишество.

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

Барабанная дробь! Запускаем Django-проект из одного файла! Та-да!

#!/usr/bin/env python
import os
import sys
from django.conf import settings
from django.conf.urls import patterns, include, url
from django.http import HttpResponse
 
filename = os.path.splitext(os.path.basename(__file__))[0]
 
urlpatterns = patterns('',
    url(r'^$', '%s.home' % filename, name='home'),
)
 
def home(request):
    return HttpResponse('Django rules!')
 
if __name__ == "__main__":
    settings.configure(
        DEBUG=True,
        MIDDLEWARE_CLASSES = [],
        ROOT_URLCONF=filename
    )
 
    from django.core.management import execute_from_command_line
    execute_from_command_line([sys.argv[0], 'runserver'])

Все отлично работает. Теперь поводов использовать Flask не осталось и вовсе.
Кода конечно немного больше чем на Flask, но у проетов всегда есть потенциал вырасти, в этом случае мы легко сможем использовать огромное количество готовых приложений!
Примечание: запускал на Django 1.7.

Запускаем Python-приложения (Django, Flask, etc) через nginx и uwsgi на Ubuntu 12.04

Для примера возьмем Flask. На Django все делается аналогично, но как минимум нужно создать проект, настроить его, а это лишние файлы и телодвижения. А пример на Flask требует только одного файла.

Nginx и uWSGI — это веб сервера. Nginx нам нужен для отдачи статики и проксирования запросов к uWSGI. А uWSGI делает запросы питоновскому приложению и получает от него ответы. Взаимодействие между питоном и веб-сервером происходит по протоколу WSGI.

Наш проект будет располагаться в директории
/home/ilya/webapps/example.com/example/

Там будет лишь один файл (имя может быть любым) —
wsgi.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

Создаем в проекте виртуальное окружение и устанавливаем туда Flask.

$ cd /home/ilya/webapps/example.com/example/
$ virtualenv python
$ . python/bin/activate
$ pip install flask

Устанавливем необходимые пакеты:

sudo apt-get install nginx uwsgi uwsgi-plugin-python

Создаем конфиг для nginx:
/etc/nginx/sites-available/example.conf

server {
    listen 80;
    server_name example.com;
    
    location / {
        uwsgi_pass      unix:/var/run/example.sock;
        include         uwsgi_params;
    }

    location /static/ {
        alias /home/ilya/webapps/example.com/www/static/;
    }
}

Делаем симлинк в папку sites-enabled

sudo ln -s /etc/nginx/sites-available/example.conf /etc/nginx/sites-enabled/example.conf

Создаем конфиг для uwsgi в формате ini (также он понимает форматы xml, json и yaml).
/etc/uwsgi/apps-available/example.ini

[uwsgi]
plugins=python27
vhost=true
socket=/var/run/example.sock
virtualenv=/home/ilya/webapps/example.com/example/python/
module=wsgi
callable=app
pythonpath=/home/ilya/webapps/example.com/example
chdir=/home/ilya/webapps/example.com/example

где
module — имя нашего модуля wsgi.py
callable — это имя объекта в питоновском файле, в случае с джангой он называется «application».

Делаем симлинк в папку apps-enabled:

sudo ln -s /etc/uwsgi/apps-available/example.ini /etc/uwsgi/apps-enabled/example.ini

Перезапускаем оба сервера:

sudo service nginx reload
sudo service uwsgi reload

Проверяем доступность сайта. (Домен должен быть прописан в DNS или /etc/hosts.)

curl example.com

Замечания по отладке

У нас получается очень много настроек и все они связаны друг с другом. Например если nginx выдает ошибку то не понятно где ошибка, толи в настройках nginx, толи в настроках uwsgi толи в настройках питон приложения. Поэтому, если что-то не работает:
1) смотри логи серверов
2) смотри создался ли файловый сокет, указанный в настрйках
3) питоновский код можно запустить отдельно и проверить curl’ом

Ссылки по теме:
uWSGI Quickstart
uWSGI Docs
Nginx Documentation
WSGI
PEP 3333 — Python Web Server Gateway Interface

Переключение тем в Django

Задача переключения тем встала передо мной довольно давно. Погуглив и не найдя нормального решения я задал вопрос на формуе, на что мне ответили, мол джанга тебе не какая-нибудь дурацкая CMS, поэтому в ней такого нету. Видимо парни на формуе слишком суровы, чтобы переключать темы, вероятно для смены темы они переписывают весь код заново. В других местах рекомендовали юзать какие-то костыли вроде проверки в шаблоне переменной.

Первое решение которое пришло мне в голову — использовать settings.py, указать шаблоны в TEMPLATE_DIRS, а статику в STATICFILES_DIRS. В этом случае переключение осуществляется правкой конфига. Первое время я использовал этот вариант.

Потом я решил что для отдельной темы лучше создать отдельное приложение. Соответственно в настройках надо указывать лишь нужное приложение в INSTALLED_APPS, а шаблоны и статику Джанга сама найдет.

Затем при написании своей CMS встал вопрос, чтобы позволить пользователю самому переключать темы из админки. Сначала я решил на ходу патчить настройки TEMPLATE_DIRS и STATICFILES_DIRS, но пока я размышлял будет ли это работать, в голову пришло более интересное рещение.

Я хорошо помнил что в настройках, есть параметры STATICFILES_FINDERS и TEMPLATE_LOADERS, соотвественно в них прописанны классы, отвечающие за поиск шаблонов и статики. А значит можно написать свои файндеры и лоудеры.

Структуру тем решил сделать такую. Все темы лежат в папке templates. Тема представляет из себя папку с шаблонами и папку со статикой.

templates/
    theme1/
        static/
            theme1/
                js/
                    theme.js
                css/
                    style.css
                img/
                    logo.png
        base.html
        home.html
        ...
        404.html
        500.html
    theme2/
    theme3/
    ...
    

Тут есть одна особенность, которую я пока не придумал как решить. При выполнении manage.py collectstatic статика всех приложений и в моем случае тем копируется в одно место. Поэтому чтобы статика одной темы не затерла статику другой, внутри папки static создается папка с названием темы.

loaders.py

from django.conf import settings
from django.template.loaders.app_directories import Loader
from django.utils._os import safe_join
 
from themes import get_current_theme
 
 
class ThemeTemplateLoader(Loader):
    is_usable = True
 
    def get_template_sources(self, template_name, template_dirs=None):
        theme_name = get_current_theme()
        try:
            yield safe_join(settings.THEMES_DIR, theme_name, template_name)
        except UnicodeDecodeError:
            # The template dir name was a bytestring that wasn't valid UTF-8.
            raise
        except ValueError:
            # The joined path was located outside of template_dir.
            pass
 
_loader = Loader()

где get_current_theme некая функция, которая возвращяет название текущей темы. В моем случае настроки хранятся в базе и тема выбирается через админку. В самом простом случае может выглядеть как:

get_current_theme = lambda: "theme1"

staticfiles.py

from django.conf import settings
from django.contrib.staticfiles.finders import BaseFinder
from django.contrib.staticfiles import utils
from django.core.files.storage import default_storage, Storage, FileSystemStorage
from django.utils._os import safe_join
 
from themes import get_current_theme
 
 
class ThemeStaticFinder(BaseFinder):
 
    def find(self, path, all=False):
        theme_name = get_current_theme()
        path = safe_join(settings.THEMES_DIR, theme_name, 'static', path)
 
        return [path] if all else path
 
    def list(self, ignore_patterns):
        theme_name = self.get_current_theme()
        location = safe_join(settings.THEMES_DIR, theme_name, 'static')
 
        storage = FileSystemStorage(location=location)
        for path in utils.get_files(storage, ignore_patterns):
            yield path, storage

Прописываем ThemeTemplateLoader и ThemeStaticFinder в settings.py, причем лоудер должен идти перед другими лоудерами.

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'themes.staticfiles.ThemeStaticFinder',
)
 
TEMPLATE_LOADERS = (
    'themes.templates.ThemeTemplateLoader',
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
)

Для полного счастья еще надо написать специальный виджет для выбора тем, в превьюхами. Но можно обойтись и без этого, простым выпадающим списком. Я для хранения настроек сайта и в частности выбранной темы использую django-options (замечу это приложение специфическое его нужно копировать в свой проект как основу и менять модель Options на свое усмотрение).

Автоматизируем автоматические тесты для Django с помощью робота

Автоматические тесты оказывают огромную помощь в работе, однако с ними есть одна существенная проблема — их надо писать. А они как правило очень рутинные, ну очень скучно писать. Я умираю со скуки при написании тестов. В один из таких моментов, когда мне было лень писать очередные тесты, я подумал — а почему бы не неписать робота который будет ходит по ссылкам на страницах и проверять, что HTTP-статус ответа равен 200.

Понятно, что полностью проблему тестирования это не решает, ведь кроме GET запросов надо проверять вскякие формы, да и содержение страницы и блоков тоже нужно проверить. Но кое-какое покрытие это дает, и если на конкретном проекте нет никаких форм, а по большей части вывод информации(например промосайты), то запустив такого робота мы хотябы можем быть уверены что страница грузятся, а не возвращяют 404 или 500 ошибки вследствии банальных опечаток и синтаксических ошибок. Т.е. если надо быстро написать промосайт, но тесты писать уже совсем лень и вермени нету, такие тесты могут помочь в разработке.

Замечу, большая часть времени у меня ушла на решение проблемы тестирования статики, которую я описывал в предыдущей статье. Спустя несколько часов у меня получилось следующее.

# coding: utf-8
import re
from StringIO import StringIO
from lxml import etree
from django.test import TestCase, Client
 
from myapp.models import Page
 
 
class BaseRobotTest(TestCase):
    urls = 'testing_urls'
    host = 'example.com'
    ignore_urls = []
    debug = False
 
    def checks_links(self, url):
        response = self.get(url)
        self.assertEquals(response.status_code, 200)
 
        urls = self.get_link_urls(response) + self.get_css_urls(response) + self.get_js_urls(response) + self.get_img_url(response)
        for url in self.get_internal_urls(urls):
            response = self.get(url)
            if self.debug:
                print response.status_code, url
            self.assertEquals(response.status_code, 200, "%s should return 200 (%s)" % (url, self.__class__))
 
    def get(self, url):
        client = Client()
        return client.get(url, HTTP_HOST=self.host)
 
    def _tree(self, response):
        content = response.content.decode('utf-8')
        parser = etree.HTMLParser()
        return etree.parse(StringIO(content), parser)
 
    def get_link_urls(self, response):
        tree = self._tree(response)
        return tree.xpath("//a/@href")
 
    def get_css_urls(self, response):
        tree = self._tree(response)
        return tree.xpath("//link/@href")
 
    def get_js_urls(self, response):
        tree = self._tree(response)
        return tree.xpath("//script/@src")
 
    def get_img_url(self, response):
        content = response.content.decode('utf-8')
        parser = etree.HTMLParser()
        tree  = etree.parse(StringIO(content), parser)
        return tree.xpath("//img/@src")
 
    def get_internal_urls(self, urls):
        internal_urls = []
        for url in urls:
            url = url.split('#',1)[0] # cut off hashtag
            if url and url != '/' and url.startswith('/') and not url.startswith('//'):
                ignore = False
                for pattern in self.ignore_urls:
                    if re.match(pattern, url):
                        ignore = True
                if not ignore:
                    internal_urls.append(url)
        return internal_urls

Стравливем роботу какую-то страницу проекта. Он находит на ней все внутренние ссылки, css, js файлы и картинки, делает к ним запрос и проверяет, что статуст ответа 200.

Например натравим робота на главную страницу:

class HomePageRobotTest(BaseRobotTest):
    ignore_urls = (
        "^/download/",
    )
 
    def setUp(self):
        super(HomePageRobotTest, self).setUp()
        # выполняем подготовительные действия, если они нужны для отображения страницы
 
    def test_home_page(self):
        self.checks_links('/')

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

Кстати неплохо бы сделать, чтобы найденные страницы он распарсивал и шел бы по ссылкам дальше. На текущем проекте надобности такой нет, т.к. на главной странице есть ссылки на все остальные. Вероятно допишу данный функционал на следующем проекте.

Тестирование статики в Django

Тесты дело обычное, но тестирование статики в Джанге, оказывается задача нестандартная. Почемуто она не освещена ни в документации ни на stackoverflow.

Раздача статики в девелоперском окружении выглядит так:
urls.py

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
# ... прочие урлы ...
urlpatterns += staticfiles_urlpatterns()

Однако staticfiles_urlpatterns работает только когда DEBUG=True, а при запуске тестов мы имеем DEBUG=False. Изменение параметра DEBUG в конфиге проблемы не решает, во время запуска тестов DEBUG всегда устанавливается в False. А это значит, что мы не можем получить статику из тестов. В документации этот момент никак не освещен. Поиск в гугле тоже ничего не дал. И я полез в исходники джанги в найти способ проманкипатчить функцию serve или одну нижележащих функций. Изрядно покопавшись в исходниках джанги, я нашел что функция serve(замечу что функция с таким названием там далкео не одна) имеет параметр insecure. А это как раз то что нам нужно. Даже удивительно, что разработчики не описали это в документации. Остается добавить к урлам, новый хендлер с параметром insecure=True и готово. Однако в продакшене нам такое поведение не нужно поэтому вынесем урлы для тестов в отдельный файл.

testing_urls.py

from django.conf import settings
from django.conf.urls.defaults import include, patterns, url
from urls import *
 
urlpatterns += patterns('',
    url(r'^%s(?P<path>.*)$' % settings.STATIC_URL.replace(settings.FORCE_SCRIPT_NAME,'').lstrip('/'), 'django.contrib.staticfiles.views.serve', {'insecure':True}),
)

Тесты дальше пишуться как обычно, главное не забыть указать атрибут urls = ‘testing_urls’.

from django.conf import settings
from django.test import TestCase, Client
 
class MyTestCase(TestCase):
    urls = 'testing_urls'
 
    def test_img(self):
        client = Client()
        response = client.get(settings.STATIC_URL + '1.png')
        self.assertEquals(response.status_code, 200)

Счастливого TDD!

Мультизагрузка файлов в Django

Понадобилось мне создать галерею из фотоальбомов и фотографий. Стандартная джангвская админка вобщем-то решает эту задачу. Однако при наполнении сайта тестовыми данными меня несколько утомило загружать файлы по одному, и я начал думать как бы так сделать, чтобы загрузить все фотки разом.

Есть две простых модели Album и Photo отношение один-ко-многии.

models.py

from django.db import models
 
class Album(models.Model):
    title = models.CharField(u'название', max_length=100, blank=True, default='')
    image = models.ImageField(u'изображение', upload_to=upload_to)
 
class Photo(models.Model):
    album = models.ForeignKey(Album, verbose_name=u'альбом', related_name='photos')
    image = ResizedImageField(u'изображение', upload_to=upload_to)
    title = models.CharField(u'название', max_length=255, blank=True, default='')
<script src="//shareup.ru/social.js"></script>

Дла начала надо добавить в форму возможность выбирать несколько файлов. Для этого я создал свой виджект.

widgets.py

from django.contrib.admin import widgets
from django.utils.safestring import mark_safe
 
class MultiFileInput(widgets.AdminFileWidget):
 
    def render(self, name, value, attrs=None):
        attrs['multiple'] = 'true'
        output = super(MultiFileInput, self).render(name, value, attrs=attrs)
        return mark_safe(output)

Далее надо прицепить это виджет в админку.

admin.py

class AlbumAdmin(admin.ModelAdmin):
    pass
 
class PhotoAdminForm(forms.ModelForm):
 
    class Meta:
        model = Photo
        widgets = {'image':MultiFileInput}
 
class PhotoAdmin(admin.ModelAdmin):
    form = PhotoAdminForm
 
admin.site.register(Album, AlbumAdmin)
admin.site.register(Photo, PhotoAdmin)

Теперь мы можем выбирать в форме множество файлов. Однако это не все, нужно при сохранении для каждого файл создавать объект класса Photo. Сначала я думал, что придется переопределять вьюхи, урлы и шаблоны, но оказалось что достаточно только переопределить одну вьюху.

from django.contrib import messages
from django.shortcuts import  redirect
from django.utils.encoding import smart_str
 
 
class PhotoAdmin(admin.ModelAdmin):
    form = PhotoAdminForm
 
    def add_view(self, request, *args, **kwargs):
        images = request.FILES.getlist('image',[])
        is_valid = PhotoAdminForm(request.POST, request.FILES).is_valid()
 
        if request.method == 'GET' or len(images)<=1 or not is_valid:
            return super(PhotoAdmin, self).add_view(request, *args, **kwargs)
        for image in images:
            album_id=request.POST['album']
            try:
                photo = Photo(album_id=album_id, image=image)
                photo.save()
            except Exception, e:
                messages.error(request, smart_str(e)) 
 
        return redirect('/admin/gallery/photo/')

Здесь есть хитрость. Если у страница запрашивается методом GET или пользователь загружет один файл, то мы вызываем метод родительского класса, тем самым избавля себя от дублирования стандартной логики. А вот если у нас пришел метод POST и выбранных файлов больше одного, то тут мы сохраняем все загруженные файлы.

Собственно все. Теперь махом можно загрузить пачку файлов.

Развертывание Django-проектов на хостинге

Как упростить разворачивание проектов на среднестатестическом shared-хочтинге или VDS? Вот как это происходит у меня.

1. Клонируем репозиторий

Удобнее это делать с помощью гита, но если на хостинге нету, то можно по SFTP залить

git clone git@bitbucket.org:username/myproject.git

2. Виртуальное окружение

На хостинге у нас может быть множество проектов, для которых требуются разные пакеты или версии пакетов. Чтобы для каждого проекта можно было установить свой набор пакетов, используется virtualenv. Обычно на питоновских хостингах он уже установлен. Если же нет, то просто скачиваем virtualenv.py и запускаем его как обычный скрипт.

Создаем виртуальное окружение в папке python.

vitrualenv python

Если вы собираетесь использовать системные питоновские пакеты, а не только те которые установили вы сами, вам может понадобиться опция —system-site-package. В более старых версиях этой опции нет и системные пакеты включены по умолчанию.

Активируем виртуальное окружение

. python/bin/activate

Есть способ использовать индивидуальный набор пакетов без виртуального окружения. В этом случае все пакеты нужно скопировать в одну папку и добавить путь к папке в PYTHONPATH. Этот способ менее удобен, но иногда пригождается.

3. Установка пакетов

На память устанавливать пакеты это не дело, у меня все пакеты для проекта записаны в файлах в папке requirements. Например файл production.pip для одного из проектов у меня выглядит так:

django>=1.4
south
sorl-thumbnail
pytils
django-autoslug
django-admin-tools
django-model-utils
django-file-resubmit
django-cleanup
django-resized
django-pencil
django-pager

Устанавливаем пакеты для нужного окружения:

pip install -r requirements/production.pip
pip install -r requirements/development.pip
pip install -r requirements/testing.pip

4. Автоматизация.

Второй и тертий пункт можно автоматизировать. Изначально я пользовался fabric, но его проблема в том, что он есть не на всех хостингах. А его установка требует компиляции бинарников, что большинство shared-хостингов запрещают. Другой способ автоматизации — make.

Makefile

run:
                python/bin/python manage.py runserver

install:
                virtualenv|grep system-site-packages && virtualenv python --system-site-packages || virtualenv python
                python/bin/pip install -r requirements/production.pip
                python/bin/pip install -r requirements/development.pip
                python/bin/pip install -r requirements/testing.pip

В этом случае развертывание виртуального окружения и пакетов намного приятнее.

make install

5. Прочие настройки

Далее в зависимости от веб-сервера(apache/nginx) и его модулей(mod_wsgi/mod_fastcgi) нужно произвести соотвествующие настройки. Если вы предпочитаете разворачивать проекты для определенного хостинга, то вполне разумно дописать в Makefile код создания этих настроек.

Запуск Django через Apache и mod_wsgi в Debian / Ubuntu.

Предполагаем apache2 уже установлен, а мы хотим запустить под ним Django. А еще нам нужно чтобы наш проект использовал vitrualenv.

Существует несколько популярных модулей для апача, через которые можно запускать питоновский код: mod_wsgi, mod_fastcgi, mod_fcgi и mod_python. Последний уже много лет как не поддерживается, и имеет много других недостатков. Разница между первыми тремя для нашей цели не принципиальна. Я в данном случае я выбрал mod_wsgi.

Прежде всего нужно установить mod_wsgi.

sudo apt-get install libapache2-mod-wsgi

После чего неплохо бы перезапустить Apache.

sudo /etc/init.d/apache2 restart

В домашней директории сайта (на хостингах это обычно папка www) нужно создать два файла .htaccess и django.wsgi со следующим содержимым.

.htaccess

AddHandler wsgi-script .wsgi
Options +ExecCGI
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ django.wsgi/$1 [QSA,PT,L]

django.wsgi

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, os

# если вы не используете virtualenv, то следующие три строки не нужны
virtual_env = '/home/user/example.com/python/' 
activate_this = os.path.join(virtual_env, 'bin/activate_this.py')
execfile(activate_this, dict(__file__=activate_this))

sys.path.insert(0, '/home/user/example.com')
sys.path.insert(0, '/home/user/example.com/myproject')

os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings'

import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

Во втором файле поправьте пути и название проекта на свои. После этого все должно работать.

Файл django.wsgi по сути — это обычный спкипт на питоне, из этого следует две вещи:

1. У него должны быть права на исполнение:

chmod +x django.wsgi

2. Чтобы убедиться, что файл работает можно его запустить:

./django.wsgi

Еще замечу, что более правильно расположить django.wsgi в папке cgi-bin (в этом случае подправьте путь в .htaccess).

Собственно все должно работать, если нет смотрите логи апача.

А теперь немножко магии. Каждый раз перезагружать апач при изменении кода проекта это не дело. Есть более удобный вариант.

touch django.wsgi

Правда работать это чудо будет только если наше приложение запущено в режиме демона. Для этого надо сделать следующее.

Открываем конфиг виртуального хоста /etc/apache2/sites-available/example.com и вписываем туда две строки:

WSGIDaemonProcess app processes=2 threads=4
WSGIProcessGroup app

app — название приложения, остальные параметы смотрите в документации.

С этими строками конфиг виртуального хоста будет выглядеть как-то так:

    ServerName example.com
    ServerAlias www.example.com
    ...
    WSGIDaemonProcess app processes=2 threads=4
    WSGIProcessGroup app

Пожалуй все. Удачного деплоя!

Как заставить Django помнить выбранные файлы при ошибках валидации формы?

Представим типичный сценарий. Пользователь заходит в админку, скажем интернет магазина, чтобы добавить продукт. Он заполняет поля формы, выбирает изображение и жмет «Сохранить». Если пользователь забыл заполнить одно из обязательный полей или ввел неверное значение, возникает ошибка валидации. Во всех полях отображаются значения которые пользователь ввел, кроме полей типа с файлами (FileField и ImageField). Это связно с политиками безопасности в браузере. Мы не можем получить реальный путь до файла из файлового поля (input type=»file»).

Однако есть обходной маневр. Мы можем сохранить файл в файловый кеш, и при рендере файлового виджета дополнительно рендерить поле типа hidden. В этом поле будет выводиться ключ нашего кеша. И таких хитрым образом обойдем ограничения браузеров.

В теории все просто. На практике несколько сложнее. Кто хочет, может написать сам, но есть уже готовое решение — django-file-resubmit.

Устанока придельно проста:

pip install django-file-resubmit

settings.py

INSTALLED_APPS = {
    ...
    'file_resubmit',
    ...
}

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    },
    "file_resubmit": {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        "LOCATION": '/tmp/file_resubmit/'
    },
}

Файлы логичнее хранить в FileBasedCache, что и указываем в настройках. Можно также настроить таймаут для кеша, по умолчанию в джанге 5 минут.

Есть два варианта использования:

1. Используем виджеты AdminResubmitFileWidget и AdminResubmitImageWidget из этого приложения. В этом случае придется прописывать виджеты для всех полей, что может быть утомительно.

admin.py

from django.forms import ModelForm
from file_resubmit.admin import AdminResubmitImageWidget, AdminResubmitFileWidget

class MyModelForm(forms.ModelForm)

    class Meta:
        model = MyModel
        widgets = {
            'picture': AdminResubmitImageWidget,
            'file': AdminResubmitFileWidget, 
        }

2. Использование примеси — добавляем примесь AdminResubmitImageWidget к базовым классам админковского класса нашей модели , в этом случаем магическим образом все нужные виджеты заменяются на AdminResubmitFileWidget.

admin.py

from django.contrib import admin
from file_resubmit.admin import AdminResubmitMixin

class MyModelAdmin(AdminResubmitMixin, admin.ModelAdmin):
    pass

После этого пользователи перестают терять выбранные файлы, при неверно заполненых формах. Очень удобная штука, рекомендую.

Уменьшаем размер оригинала при загрузке изображения в ImageField в Django

Для Django есть великолепная библиотека sorl-thumbnail, которая позволяет создавать превьюхи изображений любого размера. К сожалению она не умеет уменьшать оригинал.

Современные фотопараты создают довольно тяжелые изображения 4-7Мб и больше. Один из моих заказчиков быстро переполнил 3Гб на хостинге загружая фотографии такого размера. У другого заказчика фотографий меньше и хостинг побольше, но грузятся 4-мегабайтовые фотографии медленно, поэтому просматривать их утомительно. Конечно случаи бывают разные иногда действительно нужны тяжелые фотографии в большом разрешении. В большинстве же случаем будет лучше уменьшить такую фотку например до 800х600 или 1024х768, и весить такая фотка будет уже всего 50-100Кб.

Вобщем для решения этой задачи я написал приложение django-resized.

Установка

pip install django-resized

Использование

from django_resized import ResizedImageField
 
class MyModel(models.Model):
    ...
    image = ResizedImageField(max_width=800, max_height=600, upload_to='whatever')

ResizedImageField наследуется от ImageField, дополнительно принимает два параметра max_width, max_height. По умолчанию ресайзит изображение под размер 800×600.

Если у вас установлен sorl-thumbnail, то ResizedImageField будет наследоваться от sorl.thumbnail.ImageField.

Теперь то клиентам будет не так просто переполнить место на хостинге 😉