Debian, AngularJS, Django, Python

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

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

EMACS: Автоматическая установка пакетов

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

  • Он прост и понятен
  • Он работает

Чтобы не раздувать статью, сразу приведу начальную часть своего .emacs:

.emacs
;;; Источники для установки пакетов
(setq package-archives '(
                         ("gnu" . "http://elpa.gnu.org/packages/")
                         ("melpa" . "http://melpa.milkbox.net/packages/")
                         ("org" . "http://orgmode.org/elpa/")
                         ))

;;; Список пакетов, которые нужно установить (на самом деле я не уверен, что все
;;; перечисленнные пакеты мне нужны)
(setq package-list '(
       2048-game          
       ac-emmet
       ac-html
       ac-html-bootstrap
       ac-html-csswatcher
       airline-themes
       auto-complete
       dired+
       dired-rainbow
       dired-subtree
       dired-toggle
       direx
       dirtree
       emmet-mode
       ergoemacs-mode
       flycheck
       flycheck-pos-tip
       flycheck-pyflakes
       gh-md
       git-commit
       git-gutter
       git-lens
       highlight-indentation
       idomenu
       jedi
       jedi-core
       js2-mode
       js2-refactor
       json-mode
       json-reformat
       less-css-mode
       magit
       magit-popup
       markdown-mode
       monokai-theme
       multiple-cursors
       neotree
       paredit
       paredit-everywhere
       popup
       pos-tip
       powerline
       py-autopep8
       py-isort
       python-environment
       python-mode
       pyvenv
       rainbow-delimiters
       rainbow-identifiers
       rainbow-mode
       smartparens
       tern
       tide
       tree-mode
       typescript-mode
       undo-tree
       virtualenvwrapper
       web-beautify
       web-completion-data
       web-mode
       yasnippet))

(package-initialize)

;;; Обновим список пакетов
(unless package-archive-contents
  (package-refresh-contents))

;;; Пробежимся по списку. Если чего-то нет - устанавливаем
(dolist (package package-list)
  (unless (package-installed-p package)
    (package-install package)))

После того, как в начало вашего .emacs будут добавлены указанные строки и редактор перезапущен, он установит все недостающие пакеты из списка автоматически.

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 из исходников

Ради того, чтобы хоть как-то ускорить свой долгострой, я решил пока переключить диски - вместо SSD с Windows 8.1 у меня теперь старый Maxtor LS-120 с Linux Mint.

Репозитории он берёт от Ubuntu LTS, которая на данный момент 14.04. Trusty. Всё почти хорошо, но EMACS там старый, а значит, magit из MELPA не работает, т.к. ему требуется версия 24.4 и выше, а так же git версии 2 и выше. Впрочем, проблема легко решается.

Скачиваем пакет с исходным кодом с официального FTP проекта GNU:

Скачивание исходных кодов
wget ftp://ftp.gnu.org/gnu/emacs/emacs-24.5.tar.xz
tar xf emacs-24.5.tar.xz

Хорошо, распаковали, но EMACS не соберётся, если в системе нет вот этих пакетов:

  • libgtk-3-dev - для интеграции с X-ами, так же разработчики пишут, что лучше использовать заголовки от второй версии GTK, т.к. с этой могут быть проблемы
  • libxpm-dev
  • libtiff5-dev
  • libgif-dev
  • libtinfo-dev
  • libncurses5-dev
  • libacl1-dev

Ставится всё одной командой:

Установка необходимых для сборки пакетов
apt-get install libgtk-3-dev libxpm-dev libtiff5-dev libgif-dev libtinfo-dev libncurses5-dev libacl1-dev -y

После установки можно запустить .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
Exec=emacs
Icon=emacs
Categories=TextEditor;
GenericName=GNU Text Editor

После сохранения и перезапуска DE (можно выйти из системы и войти снова) ярлык появится в главном меню.

P. S. Что касается свежести Git:

Установка новой версии Git:
apt-add-repository git-core/ppa
apt-get update
apt-get install git -y

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.