Запускаем 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!

Поднимаем шлюз на Ubuntu за 10 минут

Есть один комп подключенный к интернету, нужно раздать интернет компам из локальной сети. Кстати замечу что таким же образом можно раздавать доступ например к виртуальным сетям, в том числе к VPN.

Настройки шлюза

Откройте файл

sudo vi /etc/sysctl.conf

и допишите строчку

net.ipv4.ip_forward=1

чтобы настройки применились без перезагрузки

sudo sysctl -w net.ipv4.ip_forward="1"

прописываем правила

sudo iptables -t nat -A POSTROUTING -s 192.168.0.129 -j MASQUERADE
sudo iptables -A FORWARD -i eth0 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT

192.168.0.129 — адрес компа в локальной сети для которого мы раздаем интернет.
eth0 — интерфейс подключенный к локальной сети;
eth1 — интерфейс подключенный к интернету;

Настройка компов в локальной сети

Нужно прописать статический IP и прописать адрес шлюза (т.е. того компа который мы настраивали выше), а также прописать DNSы. Это кажется очевидным, но всякий случай напомню, IP должен быть в тойже сети что и шлюз.

Вот и все после этого уже должно все работать. Но для полного счастья нужно выполнить еще один шаг.

Сохраняем настройки

sudo sh -c "iptables-save > /etc/iptables.up.rules"

Теперь чтобы после перезагрузки настройки восстановились автоматически пишем создаем файл в папке /etc/network/if-up.d со следующим содержимым:

#!/bin/sh
iptables-restore < /etc/iptables.up.rules

Сброс настроек

Если в процессе вам понадобиться очистить настройки iptables:

sudo iptables -F
sudo iptables -t nat -F

Успехов!

Мультизагрузка файлов в 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

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

Установка Windows 7 и Ubuntu Desktop 12.04 LTS на Macmini параллельно с Mac OS

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

Установка Windows параллельно с Mac OS

Небольшое отступление. Если вы хотите установить Windows параллельно Mac OS, а линукс вам не нужен, то делается это проще простого.

1) Берем флешку размером под образ винды

2) Находим образ винды в формате .iso

3) Из Mac OS запускаем Boot Camp Assistant и следуем инструкциям. Нам будет предложено создать загрузочную флешку с виндой, на которую дополнительно установятся маковские драйвера. Затем выделяем место под виндоус и собственно устанавливаем винду.

4) После установки виндоус заходим на флешку в папку WindowsSupport и запускаем setup.exe — будут уствновленны все маковские драйвера.

5) Устанавливаем rEFIt (опционально)

После установки для выбора операционной системы при загрузке удерживем клавишу Alt. Если вы выполнили 5й шаг, то это не понадобится.

Установка Windows и Ubuntu параллельно с Mac OS

1) Нужно создать загрузочную флешку с Ubuntu. Если у вас на каком-то компе установлена убунта, то воспользуйтесь программой Startup Disk Creator. Чтобы создать загрузочную флешку с убунтой из другой системы, используйте гугл.

2) Устанавливаем rEFIt. Эту операцию можно выполнить в самом начале или в самом конце, когда все операционки уже будут установленны. Единственно с ним есть какая-то магия, возможно Mac OS придется пару раз перезагрузить после установки этой штуки, чтобы появилось загрузочное меню. По крайней мере так было у меня. Если вы решили произвести установку rEFIt в конце, то в процессе всех установок вам может понадобиться переключаться между ОСями. Для этого удерживем клавишу Alt во время загрузки.

3) Из Mac OS запускаем Disk Utility и создаем два раздела один под винду, другой под линукс. Эта улита довольно тупая и не знает стандартных файловых систем типа NTFS или EXT4, поэтому создаем наши разделы в FAT32. В дальнешем мы создадим на этих разделах нужные файловые системы.

4) Устанавливем сначала Ubuntu. Я пытался ставить сначала виндоус, но он каждый раз слетал после установки убунты. Во избежание такого нежелательного поведения ствами сначала Ubuntu. Убунта ставиться как обычно, правда есть пара важный замечаний. Прежде мы создали раздел под убунту, но ведь нормальные люди не ставят весь линукс в один раздел, вот и мы не будем. Во время установки убунты, разбиваем тот раздел на три раздела — для системы (EXT4 /), для домашней диектории (EXT4 /home/) и для файла подкачки (SWAP). Важно! Указываем чтобы загрузчик установился в тот же раздел, что и убунта.

5) Устанавливаем винду. Тут все просто, единственный момент при установке отформатируйте целевой раздел в NTFS.

Засада

Не пытайтесь работать через маковский Disk Utility с линуксовыми разделами. Да и с виндовыми разделами я бы эксперементировать не советовал. Например эта штука не умеет удалять SWAP. После того как я попытался удалить линуксовые разделы через эту хрень, у меня линукс перестал грузиться с флешки. И даже GParted Live CD не захотел грузиться с флешки чтобы исправить эту проблему. Я конечно долго материл мак, но толку от этого мало. В итоге решил проблему тем, что установил виндоус, а на него Acronis Disk Director 11 Home, после чего смог разбить диск как надо.

Эпилог

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

Как заставить 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

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