Django 1.8, Python 3, WSGI и Gunicorn
Введение
В данной статье рассказывается, как можно запустить приложение на Django и Python 3 под WSGI. Способ не претендует на звание самого лучшего, замечания по настройке и прочим нюансам приветствуются.
Структура каталогов
Следует создать в каталоге /var/www подкаталог для нашего проекта. Пусть сегодня он будет называться talos. В нём нужно будет создать каталоги для размещения статичных файлов, файлов, загружаемых пользователями, и логов.
cd /var/www
mkdir talos
cd talos/
mkdir static media log
Сам проект будет расположен в виртуальном окружении, которое будет создано чуть позже.
Создание файла запуска
Здесь же, в каталоге /var/www/talos, создадим файл run.bash, который будет запускать приложение, передавая ему нужные параметры.
#!/bin/bash
NAME="talos" # Название приложения
DJANGODIR=/var/www/.virtualenvs/talos/talos # Директорая проекта - путь к виртуальному окружению
# плюс папка с проектом
SOCKFILE=/var/www/sockets/talos.sock # Тут будет лежать сокет
USER=www-data # От чьего имени запускается
GROUP=www-data # Группа для запуска
NUM_WORKERS=3 # Кол-во воркеров, обычно число ядер * 2 + 1
DJANGO_SETTINGS_MODULE=talos.settings # Откуда брать настройки
DJANGO_WSGI_MODULE=talos.wsgi # Имя wsgi-файла для запуска
echo "Starting $NAME as `whoami`"
# Активация окружения
cd $DJANGODIR
source ../bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Если папки для сокета нет, её надо создать
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR
# Запуск через gunicorn с передачей параметров
exec ../bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=/var/www/talos/log/talos.log
Создание виртуального окружения
Последние версии Django написаны на Python 3, да и вообще использовать Python 2 в 2015 году - дурной тон. Ставим нужные пакеты, если их ещё нет:
apt-get install python3-dev virtualenvwrapper -y
Первый пакет нужен для сборки пакетов, поставляемых в исходных кодах (lxml, psycopg2, pillow), второй - для удобного управления виртуальными окружениями. Пакеты установлены, пользователь www-data в системе. Пришло время создать окружение и поставить нужные пакеты:
mkvirtualenv talos --python=/usr/bin/python3
workon talos
В результате в каталоге для виртуальных окружений (у каждого пользователя свой, по умолчанию называется .virtualenvs) будет создан подкаталог talos. В нём будут размещены необходимые для работы с окружением скрипты и несколько других каталогов. Разместим наш проект внутри каталога /var/www/.virtualenvs/talos.
Далее следует поставить в окружение все необходимые пакеты, требуемые для запуска проекта, а так же пакет gunicorn. При необходимости стоит так же обновить pip, его последние версии умеют кэшировать скачанные пакеты.
Не забудьте создать статику и при необходимости подкорректировать файл настроек приложения.
Запуск через supervisor
Если supervisor ещё не установлен, пришло время это сделать:
apt-get install supervisor -y
Настройки хранятся в каталоге /etc/supervisor. Главный файл называется supervisor.conf, файлы для запуска приложений следует расположить в каталоге /etc/supervisor/conf.d/, указав расширение .conf. В нашем случае файл будет лежать по пути /etc/supervisor/conf.d/talos.conf
[program:talos]
command=/var/www/talos/run.bash
user=www-data
group=www-data
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/www/talos/log/supervisor.log
Когда файл будет создан, следует обновить данные Supervisor'а:
supervisorctl update
При необходимости перезапустить то или иное приложение следует вызывать не перезапуск системной службы supervisor, а давать команды supervisorctl:
supervisorctl update # Перечитать файлы конфигации приложений
supervisorctl start talos # Запустить приложение talos
supervosorctl stop talos # Остановить приложение talos
supervisorctl restart talos # Перезапуск приложения talos
supervisorctl status # Посмотреть статус всех приложений
supervisorctl ДЕЙСТВИЕ all # Выполнить ДЕЙСТВИЕ со всеми приложениями, например, перезапуск
Если всё сделано правильно, приложение будет запущено, а в каталоге /var/www/sockets/ появится файл сокета talos.sock (как было настроено в файле run.bash).
Подключение к nginx
Всё, что нужно теперь сделать - указать nginx путь к сокету и откуда брать статику для проекта. В общем-то, всё довольно просто. Nginx лучше ставить из официального репозитория проекта, а не из Debian'овского, там пакет не обновляется годами.
apt-get install nginx -y
Все настройки в каталоге /etc/nginx, главный файл - nginx.conf, настройки для сайтов в .conf-файлах, лежащих в каталоге /etc/nginx/conf.d/.
Создадим файл настроек, общих для всех сайтов: /etc/nginx/proxy_params.conf:
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Url-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 20m;
client_body_buffer_size 1m;
proxy_buffering off;
proxy_send_timeout 180;
proxy_read_timeout 180;
proxy_connect_timeout 180;
proxy_buffer_size 4k;
proxy_buffers 32 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 1m;
add_header X-Frame-Options "SAMEORIGIN";Возможно, данные настройки придётся доработать исходя из реальной конфигурации сервера. Здесь я на этом останавливаться не буду. Переходим к созданию файла конфигурации для нашего проекта:
upstream talos {
server unix:/var/www/sockets/talos.sock fail_timeout=0;
keepalive 30;
}
server {
listen 80;
server_name talos.lo;
keepalive_timeout 3;
access_log off;
error_log /var/www/talos/log/nginx_error.log crit;
location / {
proxy_pass http://talos/;
include /etc/nginx/proxy_params.conf;
}
location /static/ {
alias /var/www/talos/static/;
expires 3d;
}
location /media/ {
alias /var/www/talos/media/;
expires 3d;
}
location ~* \.(7z|jpg|jpeg|gif|png|ico|css|bmp|swf|js|html|txt|doc|docx|pdf|rar|xls|xlsx|zip)$ {
root /var/www/talos/;
expires 3d;
add_header Cache-Control: public;
access_log off;
error_log /var/www/talos/log/nginx_static_error.log;
}
}
Перезапустите nginx, тобы он подхватил новый конфигурационный файл. На этом всё.
Django: пути к шаблонам
Не осилил регулярное выражение для путей к шаблонам Django, поэтому написал несколько функций, облегчающих работу. Допустим, у нас такая структура каталогов:
/template
admin/
index.html
articles/
add.html
detail.html
list.html
news/
add.html
detail.html
list.html
desktop/
index.html
articles/
add.html
detail.html
list.html
news/
add.html
detail.html
list.html
urls.py
Есть много вариантов того, как написать urls.py, но я написал так:
from os.path import join
from django.conf.urls import include
from django.conf.urls import url
from django.views.generic import TemplateView
def template_url(folder, template):
return url(
'^' + template + '.html$',
TemplateView.as_view(template_name=(join(folder, template) + '.html'))
)
def urls_list(prefix, urls_list):
return [template_url(prefix, item) for item in urls_list]
admin = urls_list('admin', [
r'index.html',
r'articles/add',
r'articles/list',
r'articles/detail',
r'news/add',
r'news/list',
r'news/detail',
])
desktop = urls_list('admin', [
r'index.html',
r'articles/add',
r'articles/list',
r'articles/detail',
r'news/add',
r'news/list',
r'news/detail',
])
urlpatterns = admin + desktop
Данная простая конструкция заменяет огромные полотна такого вида:
from django.conf.urls import include
from django.conf.urls import url
from django.views.generic import TemplateView
admin = template_url('admin', [
url('^index.html$', TemplateView.as_view(template_name='admin/index.html')),
url('^articles/add.html$', TemplateView.as_view(template_name='admin/articles/add.html')),
url('^articles/list.html$', TemplateView.as_view(template_name='admin/articles/list.html')),
url('^articles/list.html$', TemplateView.as_view(template_name='admin/articles/list.html')),
url('^news/add.html$', TemplateView.as_view(template_name='admin/news/add.html')),
url('^news/list.html$', TemplateView.as_view(template_name='admin/news/list.html')),
url('^news/list.html$', TemplateView.as_view(template_name='admin/news/list.html')),
])
desktop = template_url('desktop', [
url('^index.html$', TemplateView.as_view(template_name='desktop/index.html')),
url('^articles/add.html$', TemplateView.as_view(template_name='desktop/articles/add.html')),
url('^articles/list.html$', TemplateView.as_view(template_name='desktop/articles/list.html')),
url('^articles/list.html$', TemplateView.as_view(template_name='desktop/articles/list.html')),
url('^news/add.html$', TemplateView.as_view(template_name='desktop/news/add.html')),
url('^news/list.html$', TemplateView.as_view(template_name='desktop/news/list.html')),
url('^news/list.html$', TemplateView.as_view(template_name='desktop/news/list.html')),
])
urlpatterns = admin + desktop
Последняя версия NodeJS через NPM
Наткнулся в сети на очень интересный способ обновления NodeJS до последней версии, не прибегая к услугам пакетного менеджера ОС. Ссылки на статью и оригинал:
В итоге у меня теперь ещё один фид в читаемых RSS.
Отмечу лишь, что установленную через пакетный менеджер версию нужно сначала вычистить из системы, так же рекомендуется удалить каталог /usr/local/lib/node_modules/. Вот команды (NodeJS должен быть установлен, желательно - собран из исходников, это не так уж и сложно):
npm cache clean -f
npm install -g n
n stable
Angular Material и md-list - проблема с дополнительным действием
В Angular Material есть такой хороший компонент - md-list, и работающий с ним в паре md-list-item. Из них можно делать красивые списки, обладающие весьма важными свойствами. Во-первых, каждая строка реагирует на нажатие. Можно реализовать возможность перехода по ссылке. Во-вторых, к каждой строке можно добавить кнопку действия.
Это я всё к чему? А к тому, что сегодня почти час убил на то, чтобы разобраться, почему скопированный почти один в один пример с официальной доки работает у них и не работает у меня.
<md-list>
<md-list-item ng-repeat="item in items" ng-click="openDetail(item)">
<img class="md-avatar" alt="" src=""/>
<p>{{ item.name }}</p>
<md-icon class="material-icons md-secondary md-warn" ng-click="remove(item)">remove</md-icon>
</md-list-item>
</md-list>
md-icon. Чтобы он превратился в кнопку, ему должен быть назначен среди прочих класс md-secondary.Если кому интересно, то ниже код контроллера. Обратите внимание, я не отлавливаю объект события и не вызываю для него stopPropagation() и preventDefault(), это не требуется.
(function (A) {
"use strict";
var inject = [
'$location',
'$mdDialog',
'$scope'
];
function Ctrl(
$location,
$mdDialog,
$scope
){
// Ничто не мешает загружать записи с сервера
$scope.items = [
{id: 1, name: 'Запись №1'},
{id: 2, name: 'Запись №2'},
{id: 3, name: 'Запись №3'},
{id: 4, name: 'Запись №4'}
];
function openDetail(item){
// Переход на другой вид
$location.path('/items/' + item.id);
}
function remove(item){
// Запрос на удаление записи
$mdDialog.show(
$mdDialog
.confirm()
.title("Подтверждение")
.content('Удалить "' + item.name + '"?')
.ok("Да")
.cancel("Нет")
).then(function () {
$scope.items = $scope.items.splice($scope.items.indexOf(item), 1);
});
}
$scope.openDetail = openDetail;
$scope.remove = remove;
}
Ctrl.$inject = inject;
A.module('app').controller('Ctrl', Ctrl);
}(this.angular));
Сборка EMACS из исходников
Скачиваем пакет с исходным кодом с официального FTP проекта GNU:
wget ftp://ftp.gnu.org/gnu/emacs/emacs-24.5.tar.xz
tar xf emacs-24.5.tar.xz
Для поиска более новой версии можно воспользоваться этой ссылкой:
Хорошо, распаковали, но EMACS'у нужна куча библиотек для успешной сборки. Можно поставить те, что нужны для сборки 24 версии, которая идёт в стандартной поставке Ubuntu 14.04:
apt-get install build-essentials -y && apt-get build-deb emacs24
После установки можно запустить .configure и make:
cd emacs-24.5/
./configure && make && make install
Процесс начнётся. Если проверка зависимостей пройдёт успешно, будет запущена компиляция проекта, а затем его установка. Однако, в главном меню не появится значка для запуска EMACS, как это происходит при установке через aptitude или apt-get install. Добавим его вручную. Всего лишь нужно создать файл формата .desktop в каталоге /usr/share/applications:
cd /usr/share/applications/
touch emacs.desktop
Теперь в этот файл нужно вписать следующие строки:
[Desktop Entry]
Version=24.5
Name=GNU Emacs
Type=Application
Comment=GNU Emacs text editor
Terminal=false
Icon=emacs
Categories=TextEditor;IDE
GenericName=GNU Text Editor
Exec=env UBUNTU_MENUPROXY=0 /usr/local/bin/emacs
После сохранения и перезапуска DE (можно выйти из системы и войти снова) ярлык появится в главном меню.
LESS для Google Material Icon Font
У Google для Web-разработки с использованием Angular Material есть даже специальный набор иконок, а так же репозиторий на GitHub с возможностью установки через Bower. Там всё хорошо, но вот CSS для иконок приходится по кускам собирать из официальной документации. Тут я и публикую такой LESS/CSS, собранный собственноручно по результатам чтения официальных доков.
Установка через Bower
bower install material-design-icons
Помимо шрифта в архиве куча иконок в разных форматах, так что будьте осторожны - bower скачает около 30 Мб, а потом будет его некоторое время распаковывать.
material-icons.less
@BASE_PATH: '/static/libs/material-design-icons/iconfont/MaterialIcons-Regular.';
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url("@{BASE_PATH}eot");
/* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url("@{BASE_PATH}woff2") format('woff2'),
url("@{BASE_PATH}woff") format('woff'),
url("@{BASE_PATH}ttf") format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
/* Preferred icon size */
display: inline-block;
width: 1em;
height: 1em;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
-webkit-font-smoothing: antialiased; /* Support for all WebKit browsers. */
text-rendering: optimizeLegibility; /* Support for Safari and Chrome. */
-moz-osx-font-smoothing: grayscale; /* Support for Firefox. */
font-feature-settings: 'liga'; /* Support for IE. */
}
.material-icons.md-18 { font-size: 18px; }
.material-icons.md-24 { font-size: 24px; }
.material-icons.md-36 { font-size: 36px; }
.material-icons.md-48 { font-size: 48px; }
// Rules for using icons as black on a light background.
.material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
.material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
// Rules for using icons as white on a dark background.
.material-icons.md-light { color: rgba(255, 255, 255, 1); }
.material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
Работает данный шрифт через лигатуры. В отличие от FontAwesome, который оперирует классами для тегов <span> и <i>, здесь нужно использовать и класс, и лигатуру:
Пример использования
<md-icon>
<i class="material-icons md-24">menu</i>
</md-icon>
Полный список лигатур находится в каталоге material-design-icons/iconfont/codepoints. Так же есть отдельный ресурс с описанием и показом всех иконок.
Django Rest Framework - обновление поля типа ImageField
Убил сегодня полдня на решение этой проблемы. Чтобы не забыть, сразу же публикую всё здесь.
Исходные данные
Дано:
- Модель, имеющая поле типа
ImageField - Django REST Framework
- ngFileUpload на фронте
Задача: сделать возможным загрузку изображений в указанное поле на основе Class-Based View в DRF.
Решение
Фронт-энд:
Вёрстка
<img ng-src="{$ item.logo200x200 $}" ng-model="logo" ngf-select ngf-change="uploadLogo(files)" accept="image/*" />
Да, всего одна строка. Вы можете поместить указанное изображение в любой подходящий контейнер, например, панель из Twitter Bootstrap.
Что делает этот код:
| Параметр | Описание |
|---|---|
ng-src="{$ item.logo200x200 $}" |
Связываем свойство модели и источник для нашего изображения. Делается через директиву Angular ng-src, как того советует официальная документация. На скобки в виде '{$' и '$}' не обращайте внимания. Т.к. на сервере используется стандартный шаблонизатор Django, приходится для Angular использовать другие скобки. |
ng-model="logo" |
Для выбора файлов будет использоваться отдельная модель - logo |
ngf-select |
Указываем, что данное изображение (можно использовать вообще-то что угодно) является полем ввода для плагина ngFileUpload |
ngf-change="uploadLogo(files)" |
При изменении значения поля выполняем указанную функцию. Загрузка без нажатия кнопки "Загрузить", в общем, достаточно лишь выбрать файл. |
accept="image/*" |
Разрешаем выбирать любые изображения. Фильтр для окна выбора файла. |
После того, как будет произведён клик по указанному изображению, откроется обычное окно открытия файла. Когда же файл будет выбран, запустится функция загрузки изображения:
LogoController.js
$scope.uploadLogo = function() {
if ($scope.logo.length < 1) {
return;
}
Upload.upload({
url: logoUrl, // /api/item/3/logo/
file: $scope.logo,
method: 'PATCH'
}).success(function(data) {
$scope.item.logo = data.logo;
});
};
Я описал лишь одну функцию контроллера. Надеюсь, догадаться, что нужно инжектировать $scope и Upload, не сложно.
file - потом именно его будем обрабатывать на сервере.Бэк-энд
Нам понадобятся модель, отдельный сериализатор для логотипов и отдельное представление. Так же размеры всех логотипов следует нормализовать - не более 200px по большей стороне. Для этого можно написать отдельную функцию - resize_logo(), принимающую как аргумент экземпляр нашей модели.
core.helpers.py
from PIL import Image
MAX_THUMBNAIL_SIZE = 200
def resize_logo(instance):
"""
Resize model logo to needed sizes.
"""
width = instance.logo.width
height = instance.logo.height
filename = instance.logo.path
max_size = max(width, height)
if max_size > MAX_THUMBNAIL_SIZE: # Да, надо изменять размер
image = Image.open(filename)
image = image.resize(
(round(width / max_size * MAX_THUMBNAIL_SIZE),
round(height / max_size * MAX_THUMBNAIL_SIZE)),
Image.ANTIALIAS
)
image.save(filename)
Пришло время описать саму модель, переопределив её метод save() таким образом, чтобы при сохранении размеры изображения для логотипа нормализовались, как нам нужно:
core.items.models.py
from os import path
from django.db import models
from core.helpers import resize_logo
class ItemModel(models.Model):
name = models.CharField(
"Название",
max_length=255,
help_text='Максимум 255 знаков',
null=False,
blank=False
)
logo = models.ImageField(
"Логотип",
upload_to=path.join('item', 'logo'), # Отдельный каталог для аватаров
null=True,
blank=True,
)
def save(self, *args, **kwargs):
# Сначала модель нужно сохранить, иначе изменять/обновлять будет нечего
super(ItemModel, self).save(*args, **kwargs)
# Приводит размеры лого к одному виду - 200px по наибольшей стороне
if self.logo:
resize_logo(self)
class Meta:
app_label = 'core'
db_table = 'item'
verbose_name = 'элемент'
verbose_name_plural = 'элементы'
Теперь можно описать части, относящиеся к API - сериализатор, представление и часть конфигурации URL.
api.items.serializers.py
from rest_framework import serializers
from core.items.models import ItemModel
# Тут должны быть описаны остальные сериализаторы, сейчас же опускаю для краткости
class ItemLogoSerializer(serializers.ModelSerializer):
class Meta:
model = ItemModel
Как видно, сериализатор крайне прост. Опишем наше представление.
api.items.api.py
from rest_framework import permissions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from core.items.models import ItemModel
from .serializers import ItemLogoSerializer
class ItemLogoAPIView(APIView):
permission_classes = [
permissions.IsAdminUser,
]
serializer_class = ItemLogoSerializer
# Обновление модели - методом PATCH, как я уже писал выше
def patch(self, *args, **kwargs):
# Находим нужную модель (по-хорошему надо обернуть в try ... except, но
# сейчас я этого делать не буду, чтобы не загромождать код)
instance = ItemModel.objects.get(pk=kwargs.get('pk'))
# Получаем из запроса наш файл (как указали выше, в JS)
instance.logo = self.request.FILES['file']
# Сохраняем запись (тут должна быть проверка значений встроенными в DRF
# методами, но сейчас я этого делать не буду)
instance.save()
# Возвращаем ответ - нашу сериализованную модель и статус 200
return Response(
ItemLogoSerializer(instance).data,
status=status.HTTP_200_OK
)
permission_classes.Теперь - самое простое - конфигурация URL:
api.items.urls.py
from django.conf.urls import url
# Тут должен быть импорт остальных сериализаторов
from .api import ItemLogoAPIView
urlpatterns = [
# А здесь должны быть остальные URL (создание/получение/обнавление)
url(r'^(?P\d+)/logo/$', ServiceLogoAPIView.as_view()),
]
Ну что ж, всё выглядит не таким уж сложным. Пришло время закрыть вопросы на Toster'е и StackOverflow.