Debian, Dojo, Django, Python

Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Django 1.8, Python 3, WSGI и Gunicorn

Введение

В данной статье рассказывается, как можно запустить приложение на Django и Python 3 под WSGI. Способ не претендует на звание самого лучшего, замечания по настройке и прочим нюансам приветствуются.

Структура каталогов

Следует создать в каталоге /var/www подкаталог для нашего проекта. Пусть сегодня он будет называться talos. В нём нужно будет создать каталоги для размещения статичных файлов, файлов, загружаемых пользователями, и логов.

От имени www-data
cd /var/www
mkdir talos
cd talos/
mkdir static media log

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

Создание файла запуска

Здесь же, в каталоге /var/www/talos, создадим файл run.bash, который будет запускать приложение, передавая ему нужные параметры.

/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 ещё не установлен, пришло время это сделать:

Установка supervisor
apt-get install supervisor -y

Настройки хранятся в каталоге /etc/supervisor. Главный файл называется supervisor.conf, файлы для запуска приложений следует расположить в каталоге /etc/supervisor/conf.d/, указав расширение .conf. В нашем случае файл будет лежать по пути /etc/supervisor/conf.d/talos.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:

Управление Supervisor'ом
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:

/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";

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

/etc/nginx/conf.d/talos.conf
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, тобы он подхватил новый конфигурационный файл. На этом всё.

Обо всех ошибках или дополнения прошу писать мне на почту dunmaksim@yandex.ru

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, но я написал так:

Использование генератора для создания urlpatterns
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 должен быть установлен, желательно - собран из исходников, это не так уж и сложно):

От имени root
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>
Вся проблема заключалась в классах CSS для тега md-icon. Чтобы он превратился в кнопку, ему должен быть назначен среди прочих класс md-secondary.

Если кому интересно, то ниже код контроллера. Обратите внимание, я не отлавливаю объект события и не вызываю для него stopPropagation() и preventDefault(), это не требуется.

ctrl.js
(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:

Создание ярлыка для EMACS
cd /usr/share/applications/
touch emacs.desktop

Теперь в этот файл нужно вписать следующие строки:

/usr/share/applications/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 Мб, а потом будет его некоторое время распаковывать.

У меня все сторонние библиотеки хранятся в каталоге static/libs/, вам же следует изменить пути к шрифтам (переменная @BASE_PATH) на подходящие.

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, не сложно.

Обратите внимание, для загрузки логотипа используется метод PATCH, а файл логотипа помещяется в объект 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.