Debian, Dojo, Django, Python

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

Django, JSON и формы

Очень интересные результаты выдает Google при поиске по словам django json forms. Большая часть ссылок ведет на Stack Overflow, но там всё одно и то же. Как правило, всё сводится к тому, чтобы: 1) сменить стек технологий; 2) разобрать request.POST как словарь (это не работает); 3) передать данные каким-то другим способом.

А вот рабочее решение, проверенное в Django 1.4.22 (из-за некоторых особенностей мне сейчас приходится пользоваться именно таким старьем).

views.py
# -*- coding: utf-8 -*-

from __future__ import absolute_imports
from django.utils import simplejson as json
from django import http

from app.items import forms
from app.items import models
from api.items import serializers

from rest import response


def api_post_view(request):
    if request.method == 'POST' and request.is_ajax():
        # Ну да, всего одна строка. А вы что думали?
        raw_data = json.loads(request.POST)
        new_item_form = forms.CreateItemForm(raw_data)
        if new_item_form.is_valid():
            new_item = new_item_form.save()
            serializer = serializers.ItemReadSerializer()
            return reponse.JsonResponse(serializer.serialize(new_item))
        else:
            return response.JsonResponse(new_tem_form.errors)
    else:
        return http.HttpResponseBadRequest()

Насчет JsonResponse тоже надо пару слов сказать. В старых версиях такого класса нет, но он отлично портируется из новых. Вот код:

rest.response
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import simplejson as json
from django.http.response import HttpResponse

class JsonResponse(HttpResponse):
    """
    An HTTP response class that consumes data to be serialized to JSON.

    :param data: Data to be dumped into json. By default only ``dict`` objects
      are allowed to be passed due to a security flaw before EcmaScript 5. See
      the ``safe`` parameter for more information.
    :param encoder: Should be a json encoder class. Defaults to
      ``django.core.serializers.json.DjangoJSONEncoder``.
    :param safe: Controls if only ``dict`` objects may be serialized. Defaults
      to ``True``.
    :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
    """

    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
                 json_dumps_params=None, **kwargs):
        if safe and not isinstance(data, dict):
            raise TypeError(
                'In order to allow non-dict objects to be serialized set the '
                'safe parameter to False.'
            )
        if json_dumps_params is None:
            json_dumps_params = {}
        kwargs.setdefault('content_type', 'application/json')
        data = json.dumps(data, cls=encoder, **json_dumps_params)
        super().__init__(content=data, **kwargs)

dojoConfig

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

dojoConfig.js
(function(G) {
    "use strict"; // Возможно, избыточно, поскольку весь Dojo написан без этой директивы

    G.dojoConfig = {
        async: true,        // Для всех браузеров должно быть так
        parseOnLoad: false, // Разбор страницы на виджеты при загрузке? Лучше дождаться domReady
        debug: true, 
        // Самая сложная часть - определение текущей локали браузера. Влияет на всякие
        // внутренние механизмы Dojo, например, какой язык будет использован для виджетов.
        // Хотите календарь на русском языке? Первый день недели - понедельник? Это сюда.
        // Можно и явно указать "ru-ru", но мне нравится вот такой подход.
        locale: navigator.language || navigator.userLanguage
    };
})(this); // this === window

Dgrid: 100% высоты родителя, проблемы рендеринга и totalLength

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

Подключать после CSS плагина Dgrid:
.dgrid {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    height: auto;
}

Решение проблемы взято со StackOverflow.

Но есть другая проблема. Допустим, мы решили разместить DGrid внутри компонента типа ContentPane. При отображении заголовок будет сжат в 0 и наедет на данные. Можно изменить размер окна браузера, чтобы отображалось нормально, а можно:

Дополнительные действия в resize() родителя:
var cpWithDgrid = new ContentPane({
    buildRendering: function() {
        this.inherited(arguments);
        this.grid = new DGrid({}); // Подставьте свой класс для Grid'а
        this.set("content", this.grid);
    },

    resize: function() {
        this.inherited(arguments);
        this.grid.resize();
    }
});

А разгадка такого поведения очень проста: высота элементов таблицы DGrid вычисляется до помещения в DOM, а потому они получают 0 в качестве значения. DGrid, разумеется, под суд. По-хорошему, при запуске вашего виджета он должен всем дочерним виджетам дать команду пересчитать размеры, но конкретно с DGrid это почему-то не работает. Костыль? Костыль. Но работает.

Надо ещё про работу с Rest сказать. Мои таблицы получают данные с сервера. В поле collection в качестве значения я указываю экземпляра класса-наследника dstore/Rest. Самое интересное, что при получении данных с сервера в логах часто можно увидеть сообщение об ошибке: Store reported null or undefined totalLength. Make sure your store (and service, if applicable) are reporting total correctly! Разумеется, это не правда. На самом деле DGrid ожидает увидеть в ответе сервера поле total, а не totalLength.

Поскольку на сервере у меня обычно Django + Django REST Framework, нужно соблюдать его требования. Одним из них является завершающий слеш в конце любого запроса. В итоге создал миксин, с которым смешиваю все создаваемые мной классы-наследники dstore/Rest:

Дополнительные действия в resize() родителя:
define([
    "dojo/_base/declare"
], function(declare) {
    return declare(null, {

        ascendingPrefix: "",

        _getTarget: function(id) {
            // По требованиям DRF в конце URL должен стоять /
            var target = this.target;

            // А можно решить с помощью строковых литералов {$id}, но не буду
            if (target.slice(-1) == '/') {
                return target + id + '/';
            } else {
                return target + '/' + id + '/';
            } // fi
        } // _getTarget
    }); // declare
}); // define

CSRF-Token в Dojo Toolkit 1.x и Django

Постоянно забываю, как в Dojo Toolkit 1.x автоматически цеплять CSRF-Token к XHR-запросам. Ниже просто код, который должен запускаться при старте приложения (FrontEnd).

Где-то в загрузчике приложения...
require([
  "dojo/cookie",
  "dojo/request/notify",
  "dojo/domReady!"
], function(cookie, notify) { 
  notify("send", function(response, cancel) {
    response.xhr.setRequestHeader("X-SCRFToken", cookie("csrftoken"));
  });
});

И каждый раз забываю, как правильно выставить CSRF-Token в Django. Для этого нужно не так уж и много:

Код ниже актуален для Django 1.4. В новых версиях, например, 1.8 функция csrf перенесена в модуль django.template.context_processors
Модуль, отвечающий за прорисовку индексной страницы.
# -*- coding: utf-8 -*-

u"""Набор видов для построения базового интерфейса приложения."""

from django.shortcuts import render_to_response
from django.views.generic import View
from django.core.context_processors import csrf


class Index(View):

    u"""Главное окно приложения."""

    template_name = "index.html"

    def get(self, request):
        # Установка CSRF-токена
        c = {}
        c.update(csrf(request))
        return render_to_response(self.template_name, c)

Но этого мало. Нужно ещё в теле главной страницы разместить скрытое поле, куда будет записан токен. Кстати, только так в старых версиях Django можно указать cookie для его хранения.

index.html
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="UTF-8"/>
    <title>Установка CSRF-Token'а</title>
  </head>
  <body>
    <!-- ТО САМОЕ СКРЫТОЕ ПОЛЕ -->
    <input name="csrftoken" type="hidden" value="{% csrf_token %}" />
    <!-- /ТО САМОЕ СКРЫТОЕ ПОЛЕ -->
  </body>
</html>

Запуск Django-приложений через mod_python в Astra Linux SE

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

Запуск Python-Web-приложений в Astra Linux

В Astra Linux до версии 1.6 не было иного способа нормального запуска Web-приложений, написанных на Python, кроме mod_python. В настоящее время самый популярный способ - WSGI, однако, в сертифицированных версиях Astra Linux SE (1.4, 1.5) есть только указанный выше модуль для Apache. Безусловно, есть и третий способ - запуск приложения в отладочном режиме через вот эту команду:

python manage.py runserver

Надо ли объяснять, что делать так не нужно?

Итак, задачи, решение которых я предлагаю в этой статье:

  • Настройка Apache для работы в рамках ALD (Astra Linux Domain)
  • Настройка виртуального хоста для Django-приложения
  • Авторизация в приложении Django через ALD/Kerberos

Настройка ALD и Apache

Все выполняемые операции требуют привилегий пользователя root.
Если кому-то покажется, что далее идёт пересказ официальной инструкции для администратора, то вам не кажется - взято отсюда.

Установка необходимых модулей Apache.

Устанавливаем два модуля Apache (считаем, что Apache уже установлен на сервере).

Установка модулей
apt-get install libapache2-auth-mod-kerb libapache2-mod-python

Включаем эти модули и перезапускаем WEB-сервер:

Включение модулей, перезапуск сервера
a2dismod auth_pam
a2enmod python
a2enmod auth_kerb

Настройка ALD.

Теперь нужно настроить сервер таким образом, чтобы клиенты, подключающиеся в рамках сессии ALD, прозрачно авторизовались на сервере. В инструкции всё описано, лишь немного постараюсь прояснить моменты, которые у меня вызвали вопросы.

Создадим принципала в ALD и добавим его в группу mac

ald-admin service-add HTTP/server.domain.lan
ald-admin sgroup-svc-add HTTP/server.domain.lan --sgroup=mac

Теперь создадим файл ключа Kerberos и дадим права на него пользователю www-data.

KEYTAB="/etc/apache2/keytab"
ald-client update-svc-keytab HTTP/server.domain.lan --ktfile=$KEYTAB
chown www:data $KEYTAB
chmod 644 $KEYTAB

Теперь можно перезапустить WEB-сервер:

service apache2 restart

Создание виртуального хоста Apache.

Подготовительные операции выполнены, на клиентах Firefox настроен на использование GSS API для авторизации (about:config, потом в параметр network.negotiate-auth.delegation-uris вписываем http://,https://). Самое время создать виртуальный хост для нашего Django-проекта, лежащего в каталоге /var/www/site/.

Apache Virtual Host: /etc/apache2/sites-available/astra-django-project
<VirtualHost *:80>
    ServerName   server.domain.lan
    ServerAdmin  useradmin@domain.lan
    DocumentRoot /var/www/site

    AddDefaultCharset utf-8

    <Directory "/var/www/site/”>
        Options -Indexes FollowSymLinks -MultiViews
        AllowOverride None

        AuthType       Kerberos
        KrbAuthRealms  DOMAIN.LAN
        KrbServiceName HTTP/server.domain.lan
        Krb5Keytab     /etc/apache2/keytab
        KrbMethodNegotiate on
        KrbMethodK5Passwd off
        KrbSaveCredentials on
        require valid-user

        Order deny,allow
        Allow from all
    </Directory>

    <Location "/">
        SetHandler    python-program
        PythonHandler django.core.handlers.modpython
        SetEnv        DJANGO_SETTINGS_MODULE site.settings
        PythonOption  diango.root /var/www/site
        PythonPath    "['/var/www/site/',] + sys.path"
        PythonAutoReload On
    </Location>

    <Location "/media/”>
        SetHandler None
    </Location>

    <Location "/static/">
        SetHandler None
    </Location>

    <LocationMatch "\.(jpg|gif|png)$">
        SetHandler None
    </Location>

    ErrorLog /var/www/site/log/error.log
    LogLevel warn
    SetEnfIf Request_URI "\.jpg$|\.gif$|\.css$|\.js" is_static
    CustomLog /var/www/site/log/access.log combined env=!is_static # Убрать лишнее из логов доступа, например, статику
</VirtualHost>

Не забываем включить наш сайт в список разрешенных:

a2ensite astra-django-project
service apache2 reload

Побочные эффекты

Возможно, у меня руки кривые, или я чего-то не знаю, но есть некоторые факты, которым я не нахожу другого объяснения, например, откусывание заголовков HTTP сервером Apache. Допустим, у нас есть вот такой код:

Возврат JSON с заголовками.
# -*- coding: utf-8 -*-

u"""Виды для обработки данных абонентов."""

from django.shortcuts import get_object_or_404
from django.views.generic import View

from abonent.models import Abonent
from rest.responses import JsonResponse

from .serializers import AbonentSimpleSerializer


class AbonentRootView(View):

    def get(self, request):
        u"""
        Именно здесь происходит сериализация модели в JSON.

        Возвращаемый объект - JsonResponse.
        """
        
        root_abonent = Abonent.objects.filter(parent=None)[0]
        serializer = AbonentSimpleSerializer()

        response = JsonResponse({
            "items": [
                serializer.serialize(root_abonent),
            ],
            "total": 1
        })

        # Тут задаем заголовки, чтобы библиотека dgrid могла с ними работать
        # Не надо смотреть на цифры, они сейчас не имеют значения (просто пример)
        response["Content-Range"] = "items: 1-1/1"

        return response

Не суть важно, как происходит сериализация (сериализатор возвращает словарь), важно то, что происходит в строке, где устанавливается заголовок ответа Content-Range. Если запустить отладочный сервер, то в заголовках ответа мы его увидим. Если выполнять этот же код с помощью Apache, т. е. так, как я выше написал, заголовок будет просто выброшен. Как это лечить в Astra Linux 1.4, я не знаю. Но, например, при использовании библиотеки DStore использовать свойство useRangeHeaders будет нельзя. Т. е. и DGrid тоже работать не будет, нужно помимо свойства items передавать ещё и total. Не такая уж большая проблема, но под определение подводного камня подходит хорошо.

Elpy, python-mode и use-package

Emacs - отличный редактор для тех, кто пишет на Python, и есть куча пакетов для поддержки этого языка. Я уже раньше писал, что теперь храню свою конфигурацию и управляю пакетами с помощью use-package, однако, долго не мог найти способ для красивого скрещивания замечательного пакета elpy cо стандартным python-mode.

Решение оказалось не очень сложным, хотя на его поиск пришлось потратить некоторое время. Ниже выкладываю кусок своего init.el.

init.el для elpy + python-mode
(use-package python-mode
  :mode ("\\.py\\'" . python-mode)
  :init(add-hook 'python-mode-hook #'elpy-enable)
  :config
  (use-package elpy
    :bind
    ("M-," . elpy-goto-definition)
    :init
    (elpy-enable)
    (defalias 'workon 'pyvenv-workon)
    :config
    (add-to-list 'company-backends 'elpy-company-backend)
    (elpy-enable))
  (use-package py-autopep8
    :hook
    (python-mode . py-autopep8-enable-on-save))
  (use-package py-isort
    :init
    (add-hook 'before-save-hook #'py-isort-before-save)))

Этот код не делает никакой магии. В общем-то, тут вообще нет ничего интересного.

Самому себе оставляю памятку: в настройках settings.el уже включена автоматическая загрузка недостающих пакетов с помощью use-package. Для всех Python'ьих файлов как major-mode указан python-mode. При его инициализации дополнительно подключаются пакеты elpy, py-autopep8 и py-isort, почти все настройки которых уже вынесены сами знаете куда. Кроме того, каждый пакет при загрузке ставит хуки на определенное действие. Например, автоматическое форматирование или упорядочение импортированных модулей.

Не уверен в том, какую строку с elpy-enable можно удалить, чтобы ничего не сломалось. Сегодня выяснять лень, потом разберусь.

Красивый способ хранения настроек Emacs

Интересный подход к хранению конфигурации Emacs

Интересный способ хранения конфигурации подсмотрел у автора use-package. Если вкратце, то все настройки для конкретных пакетов вынесены в файл settings.el, а вот список используемых пакетов и нюансы взаимодействия между ними описаны целиком и полностью в init.el.

Чтобы далеко не ходить, вот пример кода:

init.el
;;; init.el --- Summary
;;; Commentary:
;;; Main EMACS settings file, load settings from parts.

;;; Code:


;; Added by Package.el.  This must come before configurations of
;; installed packages.  Don't delete this line.  If you don't want it,
;; just comment it out by adding a semicolon to the start of the line.
;; You may delete these explanatory comments.
(package-initialize)

(eval-when-compile(require 'cl))

(require 'package)

(setq package-archives nil)

(add-to-list 'package-archives '("melpa-stable" . "http://stable.melpa.org/packages/") t)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("org" . "http://orgmode.org/elpa/") t)
(add-to-list 'package-archives '("gnu" . "http://elpa.gnu.org/packages/") t)
(add-to-list 'package-archives '("elpy" . "https://jorgenschaefer.github.io/packages/") t)

(setq package-enable-at-startup nil)
(package-initialize nil)

;;; Загрузка и установка пакета use-package, если его нет в системе
(unless(package-installed-p 'use-package)
  (message "EMACS install use-package.el")
  (package-refresh-contents)
  (package-install 'use-package))

(require 'use-package)

;;; Шрифты
(set-face-attribute 'default nil :height 110)
(when (member "DejaVu Sans Mono" (font-family-list))
  (set-face-attribute 'default nil :font "DejaVu Sans Mono"))

;;; Заставляю хоткеи работать и в русской раскладке
(defun cfg:reverse-input-method (input-method)
  "Build the reverse mapping of single letters from INPUT-METHOD."
  (interactive
   (list (read-input-method-name "Use input method (default current): ")))
  (if (and input-method (symbolp input-method))
      (setq input-method (symbol-name input-method)))
  (let ((current current-input-method)
        (modifiers '(nil (control) (meta) (control meta))))
    (when input-method
      (activate-input-method input-method))
    (when (and current-input-method quail-keyboard-layout)
      (dolist (map (cdr (quail-map)))
        (let* ((to (car map))
               (from (quail-get-translation
                      (cadr map) (char-to-string to) 1)))
          (when (and (characterp from) (characterp to))
            (dolist (mod modifiers)
              (define-key local-function-key-map
                (vector (append mod (list from)))
                (vector (append mod (list to)))))))))
    (when input-method
      (activate-input-method current))))

(cfg:reverse-input-method 'russian-computer)


;; Resize windows
(global-set-key(kbd "S-C-") 'shrink-window-horizontally)
(global-set-key(kbd "S-C-") 'enlarge-window-horizontally)
(global-set-key(kbd "S-C-") 'shrink-window)
(global-set-key(kbd "S-C-") 'enlarge-window)

;; Isearch
(global-set-key (kbd "C-M-r") 'isearch-backward-other-window)
(global-set-key (kbd "C-M-s") 'isearch-forward-other-window)

(defun xah-new-empty-buffer()
  "Open a new empty buffer.
  URL `http: // ergoemacs.org / emacs / emacs_new_empty_buffer.html'
  Version 2015 - 06 - 12"
  (interactive)
  (let((ξbuf(generate-new-buffer "untitled")))
  (switch-to-buffer ξbuf)
  (funcall(and initial-major-mode))
  (setq buffer-offer-save t)))

;; Save/close/open
(global-set-key(kbd "C-w") 'kill-this-buffer)
(global-set-key(kbd "C-s") 'save-buffer)
(global-set-key(kbd "C-S-s") 'write-file)
(global-set-key(kbd "C-r") 'revert-buffer)
(global-set-key(kbd "C-a") 'mark-whole-buffer)
(global-set-key(kbd "M-'") 'comment-or-uncomment-region)
(global-set-key(kbd "C-o") 'dired)
(global-set-key(kbd "C-n") 'xah-new-empty-buffer)
(global-set-key(kbd "C-+") 'text-scale-increase)
(global-set-key(kbd "C--") 'text-scale-decrease)

;; Buffers and windows
(global-set-key(kbd "C-") 'next-buffer)
(global-set-key(kbd "C-") 'previous-buffer)
(global-set-key(kbd "C-") 'other-window)

(global-set-key(kbd "M-3") 'delete-other-windows)
(global-set-key(kbd "M-4") 'split-window-horizontally)
(global-set-key(kbd "M-5") 'split-window-vertically)
(global-set-key(kbd "M-6") 'balance-windows)

(global-set-key(kbd "C-f") 'isearch-forward)
(global-set-key(kbd "C-h") 'query-replace)
(global-set-key(kbd "C-S-h") 'query-replace-regexp)

(global-set-key(kbd "M-a") 'execute-extended-command)
(global-set-key(kbd "M-x") 'kill-whole-line)
(global-set-key(kbd "") 'keyboard-quit)

(when(get-buffer "*scratch*")
  (kill-buffer "*scratch*"))

(fset 'yes-or-no-p 'y-or-n-p); ; ; Shortcuts for yea and no

;;; Format file before save
(defun format-current-buffer()
  (indent-region (point-min) (point-max)))
(defun untabify-current-buffer()
  (if (not indent-tabs-mode)
    (untabify (point-min) (point-max)))
      nil)
(add-to-list 'write-file-functions 'untabify-current-buffer)
(add-to-list 'write-file-functions 'delete-trailing-whitespace)

;;; САМЫЙ ВАЖНЫЙ МОМЕНТ - ВЫНЕСЕНИЕ НАСТРОЕК СРЕДЫ В settings.el и загрузка их оттуда
(setq custom-file "~/.emacs.d/settings.el")
(load-file "~/.emacs.d/settings.el")

;;; Теперь просто описание используемых пакетов
;;; Настройки в большинстве случаев подтягиваются из файла settings.el
(use-package airline-themes
  :requires powerline
  :init
  (load-theme 'airline-molokai))

(use-package all-the-icons)

(use-package beacon
  :diminish
  :commands beacon-mode)

(use-package company
  :diminish
  :commands company-mode
  :config
  ;; From https://github.com/company-mode/company-mode/issues/87
  ;; See also https://github.com/company-mode/company-mode/issues/123
  (defadvice company-pseudo-tooltip-unless-just-one-frontend
    (around only-show-tooltip-when-invoked activate)
    (when (company-explicit-action-p)
      ad-do-it))

  (defun check-expansion ()
    (save-excursion
      (if (outline-on-heading-p t)
        nil
          (if (looking-at "\\_>") t
            (backward-char 1)
            (if (looking-at "\\.") t
              (backward-char 1)
              (if (looking-at "->") t nil))))))

  (define-key company-mode-map [tab]
    '(menu-item "maybe-company-expand" nil
    :filter (lambda (&optional _)
      (when (check-expansion)
        #'company-complete-common))))

  (eval-after-load "yasnippet"
    '(progn
      (defun company-mode/backend-with-yas (backend)
        (if (and (listp backend) (member 'company-yasnippet backend))
          backend
          (append (if (consp backend) backend (list backend))
          '(:with company-yasnippet))))
          (setq company-backends
        (mapcar #'company-mode/backend-with-yas company-backends))))
          :init (global-company-mode t))

(use-package company-quickhelp
  :bind
  (:map company-active-map
    ("C-c h" . company-quickhelp-manual-begin)))

(use-package css-mode
  :mode "\\.css\\'")

(use-package elpy
  :mode "\\.py\\'"
  :requires python-mode
  :hook (add-hook 'before-save-hook #'elpy-format-code)
  :init
  (elpy-enable)
  (defalias 'workon 'pyvenv-workon))

(use-package emmet-mode
  :mode  ("\\.html\\'" . emmet-mode)
  :bind
    ("C-j" . emmet-expand-line))

(use-package flycheck
  :commands flycheck-mode
  :init(add-hook 'after-init-hook #'global-flycheck-mode))

(use-package highlight-numbers
  :hook(prog-mode . highlight-numbers-mode))

(use-package ibuffer
  :bind([f2] . ibuffer)
  :init
  (add-hook 'ibuffer-mode-hook #'(lambda ()(ibuffer-switch-to-saved-filter-groups "default"))))

(use-package js2-mode
  :mode ("\\.js\\'" . js2-mode)
  :requires flycheck
  :bind(
    :map js2-mode-map
    ("M-n" . flycheck-next-error)
    ("M-p" . flycheck-previous-error))
  :config
  (add-to-list 'flycheck-disabled-checkers #'javascript-jshint)
  (flycheck-add-mode 'javascript-eslint 'js2-mode)
  (flycheck-mode 1)
  (js2-highlight-unused-variables-mode t))

(use-package json-mode
  :mode (("\\.json\\'" . json-mode)
         ("\\.bowerrc\\'" . json-mode)
         ("\\.jshintrc\\'" . json-mode)))

(use-package json-reformat
  :after json-mode)

(use-package magit
  :bind([f5] . magit-status))

(use-package markdown-mode
  :mode "\\.md\\'")

(use-package monokai-theme
  :init(load-theme 'monokai))

(use-package neotree
  :requires all-the-icons
  :bind
  ([f8] . neotree-toggle))

(use-package powerline)

(use-package py-isort
  :hook
  (add-hook 'before-save-hook 'py-isort-before-save))

(use-package rainbow-delimiters
  :hook
  (prog-mode . rainbow-delimiters-mode))

(scroll-bar-mode -1)

(use-package tide
  :requires js2-mode
  :after js2-mode
  :mode ("\\.js\\'" . tide-mode)
  :hook((before-save-hook . tide-format-before-save)
        (typescript-mode-hook . setup-tide-mode)))

(use-package web-beautify)

(use-package web-mode
  :commands web-mode
  :requires web-beautify
  :mode("\\.phtml\\'" . web-mode)
       ("\\.html\\'" . web-mode)
  :hook (add-hook 'before-save-hook 'web-beautify-html-buffer t t))

(use-package yasnippet
  :after prog-mode
  :defer 10
  :diminish yas-minor-mode
  :mode("/\\.emacs\\.d/snippets/" . snippet-mode)
  :config
  (yas-load-directory (expand-file-name "snippets" user-emacs-directory))
  (yas-global-mode 1))

(use-package yasnippet-snippets
  :after yasnippet)

;;; init.el ends here

Что интересного я взял у автора?

Во-первых, саму организацию хранения описания пакетов. Во-вторых, он отсортировал вызовы use-package по алфавиту. Быстрый поиск не нужен, если можно просто нажать пару раз [PgDn]. В-третьих, он и в файле настроек применил сортировку по алфавиту.

Для тех, кому интересен оригинальный конфиг, его можно найти здесь.