Debian, Dojo, Django, Python

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

CSRF-токены и CBV, требующие авторизации

Комментариев нет

Всё, что нужно знать о CSRF-токенах в Django

Всё Middleware оставлены по-умолчанию, т.е. в settings.py никаких изменений не вносилось.

Ниже идёт код, который позволяет прописать в шаблон Cookies с CSRF-токеном:

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


class Index(View):

    def get(self, request):
        context = {}
        context.update(csrf(request))
        return render_to_response(
            'index.html',
            RequestContext(request, context)
        )

В сам шаблон нужно не забыть включить одну важную строку:

{% csrf_token %}

Без этой строки печенька покрошена на страницу НЕ БУДЕТ. Не знаю, почему, просто вот такой интересный факт.

Миксин для CBV (Class Based Views), которым нужна авторизация:

from os import path

from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.views.generic import View


class LoginRequiredMixin(object):
    """Собственно примесь """

    @classmethod
    def as_view(cls, **initkwargs):
        view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
        return login_required(view, login_url='/login/')


class ProfileView(LoginRequiredMixin, View):

    def get(self, request, *args, **kwargs):
        c = {}
        c.update(csrf(request))

        user = request.user

        template_path = path.join(
            'admin',
            'index.html',
        )

        return render_to_response(
            template_path,
            RequestContext(request, c)
        )

Комментариев нет :

Отправить комментарий

EMACS для Python

4 комментария

Введение

Решил наконец написать статью о том, как настроить EMACS для удобной работы с Python'ом. Ниже список рассматриваемых вопросов:

  • Разбиение конфигурации на части
  • Пакеты
  • Ergoemacs
  • Anaconda-mode

Разбиение конфигурации на части

Долгое время мой .emacs представлял собой всего один файл. Естественно, править его было не очень удобно. С течением времени он рос и становился толще, наконец, в определённый момент поддерживать его стало совершенно невозможно. К счастью, я подсмотрел на StackOverflow, как можно разбить его на несколько маленьких частей.

Для подгрузки параметров из других файлов нужно в .emacs написать следующее:

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

;;; Code:

(load "~/.emacs.d/fonts.el")
(load "~/.emacs.d/packages.el")
(load "~/.emacs.d/ergoemacs.el")
(load "~/.emacs.d/personal.el");
(load "~/.emacs.d/keyboard.el")
(load "~/.emacs.d/variables.el")
(load "~/.emacs.d/flycheck.el")
(load "~/.emacs.d/rust.el")
(load "~/.emacs.d/python.el")
(load "~/.emacs.d/web.el")

;;; .emacs ends here

Здесь load - функция, выполняющая загрузку указанного файла. Сам файл находится в каталоге .emacs.d/ и называется fonts.el. Туда я положил настройки шрифтов. Затем идёт подгрузка файла, отвечающего за список пакетов. При необходимости недостающие пакеты будут установлены из MELPA, MELPA-STABLE или другого из указанных репозиториев, впрочем, об этом я уже писал ранее.

Я не буду давать рекомендаций по поводу того, на сколько частей нужно разбивать конфигурацию, как эти части называть и так далее, каждый решает сам, потому что, например, несколько дней назад я не видел необходимости выносить настройки шрифтов отдельно, а сейчас решил, что так будет лучше. Как видите, за настройки для разработки на Python'е отвечает файл .emacs.d/python.el, в него я поместил все нужные параметры EMACS'а.

Пакеты

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

anaconda-mode

Данный пакет служит для поддержки функциональности IDE при работе с файлами Python'а, как то быстрый переход к декларациям, вывод документации и т.д.

company

Данный пакет рекомендуется как более современная замена пакету auto-complete. Ничего не имею против последнего, но лично мне использовать указанный приятнее.

company-anaconda

Автодополнение в режиме anaconda-mode, без этого подсказки работать не будут.

ergoemacs-mode

В общем-то, не скажу, что этот пакет жизненно необходим, но он даёт необходимые удобства, например, упрощает работу с командами, заменяя стандартные хоткеи EMACS'а на свои, более близкие к тем, которыми пользуются нормальные люди, например, комбинации Ctrl+C и Ctrl+V, а так же многие другие начинают работать так, как это принято в подавляющем большинстве современных программ, т.е. копируют и вставляют текст. Подробнее можно почитать на сайте проекта.

flycheck, flycheck-pos-tip

Стандарт де-факто для проверки синтаксиса. Какой именно синтаксис и чем будет проверяться - настраивается. По умолчанию для Python'а используются flake8, pylint и pycompile. Естественно, чтобы данные средства работали, они должны присутствовать в системе.

helm

Система подсказок. Допустим, нажали Вы Alt+X, начинаете вводить lis, а он сразу выдаёт отфильтрованные команды, в которых присутствует данная строка. Можно стрелками выбрать нужную строку и нажать Enter. Умеет подсказывать не только команды, но так же имена файлов, виртуальные окружения и многое другое, легко расширяется. Практически незаменимое средство.

neotree

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

python-mode

Базовый пакет для поддержки EMACS'ом Python'а.

py-autopep8

Пакет позволяет применить autopep8 - средство для автоформатирования кода по стандартам. Можно настроить на автоформат кода при сохранении файла. Удобно, хотя в старых проектах, где код писан ногами, порождает тонны баттхёрта у тех, кто будет делать ревью вашего кода.

py-isort

Сортирует импортированные файлы. Можно передать разные настройки. Лично я предпочитаю импорт 1-1, т.е. одна строка - один модуль или класс. Рекомендую, т.к. в этом случае с помощью flycheck будет проще удалять неиспользованные модули - выделил строку, нажал Backspace, сохранил.

pyvenv, virtualenvwrapper

Позволяет EMACS'у видеть виртуальные окружения и нормально с ними работать.

pip-requirements

Небольшой пакет для удобного редактирования файла зависимостей. Если кто не в курсе - файл REQUIREMENTS создаётся в корне Python-проекта и позволяет в одну команду установить все нужные для работы зависимости:

pip install -r REQUIREMENTS -U

В чём плюс данного пакета? Он выдаёт подсказки по именам, подгружая список с PyPi - главного всемирного рассадника питонячьих пакетов.

Установка через pip (от имени root)

pip install autopep8 flake8 isort pylint -U

Ergoemacs

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

;;; Package --- Summary
;;; Comment:
;;; Settings for ergoemacs-mode

;;; Code:

(require 'ergoemacs-mode)

;;; Ergoemacs
(setq ergoemacs-theme nil)
(setq ergoemacs-keyboard-layout "us")
(setq ergoemacs-ini-mode t)
(setq ergoemacs-use-menus t)
(setq ergoemacs-smart-paste nil)
(setq ergoemacs-ctl-c-or-ctl-x-delay 0.3)
(ergoemacs-mode 1)

;;; ergoemacs.el ends here

Anaconda-mode

Тут речь не только об anaconda-mode, ниже приводится весь файл настроек для Python. Надеюсь, комментарии помогут в понимании данных настроек

;;; Package --- Summary
;;; Commentary:
;;; Settings for Python

;;; Code:

;;; Импорт необходимых модулей
(require 'py-autopep8)
(require 'py-isort)
(require 'pip-requirements-mode)

;;; Псевдоним для команды pyvenv-workon, пользователи virtualenv оценят удобство
(defalias 'workon 'pyvenv-workon)

;;; Автоматически загружать модуль python-mode, писать в статус баре "Python-mode"
(autoload 'python-mode "python-mode" "Python mode." t)

;;; Применять python-mode для файлов с расширением .py
;;; Использовать интерпретатор python для python-mode
(add-to-list 'auto-mode-alist '("\\.py\\'" . python-mode))
(add-to-list 'interpreter-mode-alist '("python" . python-mode))

(add-to-list 'auto-mode-alist '("\\REQUIREMENTS" . pip-requirements-mode))
(add-to-list 'auto-mode-alist '("\\REQUIREMENTS.txt" . pip-requirements-mode))
(add-to-list 'auto-mode-alist '("\\requirements" . pip-requirements-mode))
(add-to-list 'auto-mode-alist '("\\requirements.txt" . pip-requirements-mode))

;;; Подсвечивать строки, которые обычно используются при отладке
(defun annotate-pdb()
  (interactive)
  (highlight-lines-matching-regexp "import ipdb")
  (highlight-lines-matching-regexp "import pdb")
  (highlight-lines-matching-regexp "set_trace()")
  (highlight-phrase "TODO")
  (highlight-regexp "FIXME")
  (highlight-regexp "BUG")
  )

;;; В Python-mode автоматически включать anaconda-mode, pdb и
;;; автоматически применять autopep8 при сохранении файла
(add-hook 'python-mode-hook 'anaconda-mode)
(add-hook 'python-mode-hook 'annotate-pdb)
(add-hook 'python-mode-hook 'py-autopep8-enable-on-save)

;;; Перед сохранением так же отсортировать импортированные модули.
;;; Правило сортировки: одна строка - один модуль
(add-hook 'before-save-hook 'py-isort-before-save)
(setq py-isort-options '("-sl"))

;;; python.el ends here

Теперь следует перезапустить EMACS, чтобы он подхватил настройки, и начать работать. Ниже приводится вид моего EMACS'а, когда я пишу на Python.

Конфиг на GitHub

4 комментария :

Отправить комментарий

Ошибка "memmove does not exist on this platform"

Комментариев нет

Который раз вижу вот эту ошибку:

extensions/expat/lib/xmlparse.c:75:2: error: #error memmove does not exist on this platform, nor is a substitute available

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

Решение

  • Открыть файл /usr/include/python2.7/pyconfig.h
  • Добавить в его конец строку

    #define HAVE_MEMMOVE 1

Послесловие: хватит уже пользоваться Python 2.7, Гвидо не для того старался с тройкой.

Комментариев нет :

Отправить комментарий

md-autocomplete со значением по умолчанию

Комментариев нет

Задача

С помощью Angular Material создать autocomplete со значением по-умолчанию. Статья навеяна вопросом со StackOverflow.

Самому как раз сегодня понадобилось решить эту же задачу. Результат можно посмотреть здесь.

Решение

HTML-разметка

<body ng-app="app" flex layout="column" layout-margin ng-controller="Main">
  <md-content layout="column" class="md-whiteframe-z1" layout-margin>
    <md-toolbar>
      <div class="md-toolbar-tools">
        <h3>Form</h3>
      </div>
    </md-toolbar>
    <md-content class="md-whiteframe-z1">
      <div class="md-padding">
        <md-input-container>
          <label for="firstname">First name</label>
          <input type="text" name="firstname" ng-model="user.firstname" />
        </md-input-container>
        <md-input-container>
          <label for="lastname">Last name</label>
          <input type="text" name="lastname" ng-model="user.lastname" />
        </md-input-container>
        <md-autocomplete md-selected-item="user.group" md-items="item in loadGroups(filterText)" md-item-text="item.title" md-search-text="filterText">
          <md-item-template>{{ item.title }}</md-item-template>
          <md-not-found>No items.</md-not-found>
        </md-autocomplete>
      </div>
    </md-content>
  </md-content>
  <md-content class="md-whiteframe-z1" layout-margin>
    <md-toolbar>
      <div class="md-toolbar-tools">
        <h3>Model as JSON</h3>
      </div>
    </md-toolbar>
    <md-content class="md-padding">
      <p>
        {{ user | json }}
      </p>
    </md-content>
  </md-content>
</body>

JavaScript

(function(A) {
  "use strict";

  var app = A.module('app', ['ngMaterial']);

  function main(
    $q,
    $scope,
    $timeout
  ) {
    $timeout(function() {
      $scope.user = {
        firstname: "Maxim",
        lastname: "Dunaevsky",
        group: {
          id: 1,
          title: "Administrator"
        }
      };
    }, 500);

    $scope.loadGroups = function(filterText) {
      var d = $q.defer(),
        allItems = [{
          id: 1,
          title: 'Administrator'
        }, {
          id: 2,
          title: 'Manager'
        }, {
          id: 3,
          title: 'Moderator'
        }, {
          id: 4,
          title: 'VIP-User'
        }, {
          id: 5,
          title: 'Standard user'
        }];

      $timeout(function() {
        var items = [];

        A.forEach(allItems, function(item) {
          if (item.title.indexOf(filterText) > -1) {
            items.push(item);
          }
        });

        d.resolve(items);
      }, 1000);

      return d.promise;
    };
  }

  main.$inject = [
    '$q',
    '$scope',
    '$timeout'
  ];

  app.controller('Main', main);
}(this.angular));

Теперь немного о том, что происходит.

Сначала происходит инициализация модели. В примере задержка ответа эмулируется с помощью $timeout, на практике следует использовать подгрузку данных с помощью $http или $resource. При этом поле, значение которого должно быть связано с полем автодополнения, уже инициализировано, при этом в нём находится целая модель. Вопрос, как получить основную модель, содержащую вложенное поле, излишен. Этим должен заниматься сервер. Например, в Django REST Framework для таких полей используются сериализаторы объектов.

На втором этапе идёт описание способов подгрузки элементов в список. Ну, тут ничего сложного, в общем-то. С помощью того же $timeout эмулируется задержка, во время которой идёт фильтрация элементов списка. Функция возвращает promise, который разрешается списком отфильтрованных записей, когда приходит время.

Комментариев нет :

Отправить комментарий

EMACS: автоустановка пакетов и хоткеи в русской раскладке

Комментариев нет

Введение

Наткнулся совершенно случайно на две отличных статьи про EMACS.

Если лень читать, то ниже приводятся готовые коды для включения в .emacs

Автоустановка пакетов

Автоустановка пакетов
(require 'cl)
(require 'package)

;; Список пакетов для установки, на самом деле у меня их под 50 штук, но смысл
;; публиковать их здесь?

(defvar cfg-var:packages '(
    anaconda-mode
    company
    company-anaconda
    company-quickhelp
    company-tern
    company-web
    emmet-mode
    ergoemacs-mode
    flycheck
    powerline
    py-autopep8
    py-isort
    web-mode
    yafolding
    yasnippet
    ))

(defun cfg:install-packages ()
    (let ((pkgs (remove-if #'package-installed-p cfg-var:packages)))
        (when pkgs
            (message "%s" "Emacs refresh packages database...")
            (package-refresh-contents)
            (message "%s" " done.")
            (dolist (p cfg-var:packages)
                (package-install p)))))

(add-to-list 'package-archives '("gnu" . "http://elpa.gnu.org/packages/") t)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("melpa-stable" . "http://stable.melpa.org/packages/") t)
(add-to-list 'package-archives '("org" . "http://orgmode.org/elpa/") t)
(package-initialize)

(cfg:install-packages)

Рабочее решение, которое гораздо лучше тех, что предложены на StackOverflow.

Хоткеи в русской раскладке

Хоткеи в русской раскладке
;; Это надо добавить ближе к началу .emacs
(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)

Комментариев нет :

Отправить комментарий

Немного о Material Design Lite и Angular Material

Комментариев нет

Хочу сказать пару слов про библиотеку Material Design Lite (MDL).

TL;DR Если нужны только стили, сгодится. Если требуется качественный JS и интеграция с Angular - посмотрите в сторону Angular Material. Впрочем, в своих проектах я использую их вместе, это не так уж сложно.

Стили

Что касается стилей, тут всё довольно неплохо. Во-первых, даже при установке через bower скачиваются исходные файлы. Можно открыть нужный .scss и внести правки. Не уверен, что многие часто пользуются этой возможностью, но в любом случае она есть, и для этого даже не нужно клонировать репозиторий с GitHub.

В библиотеке используется БЭМ от Яндекса. Может, подход и хорош, но я пока не оценил всех его преимуществ и он кажется мне несколько многословным. Директивы или полифиллы мне как-то ближе, впрочем, даже к такой "разговорчивости" фреймворка привыкаешь довольно быстро.

Очень даже хорошо, что из коробки идёт поддержка стилей для таблиц, карточек, панели навигации и некоторых других объектов. По сравнению с Angular Material - шаг вперёд.

JS

А вот с JS не всё гладко. Во-первых, поведение меню. В Angular Material разработчики предусмотрели позиционирование таким образом, чтобы оно не уезжало за экран. В MDL приходится самому заботиться об этом и подбирать нужные стили. Впрочем, это не такая уж большая проблема. Есть кое-что ещё.

MDL выполняет обновление компонентов и установку обработчиков на них только в момент загрузки страницы. Да, он дожидается DOMReady, после чего выполняет заполнение свойств элементов. При использовании какого-либо роутинга в Angular сразу появляется проблема. Решается довольно легко, спасибо StackOverflow:

Приложение
Application.run(['$rootScope', '$location', '$timeout', function (
    $rootScope,
    $location,
    $timeout
) {
    $rootScope.$on("$viewContentLoaded", function () {
        $timeout(function () {
            componentHandler.upgradeAllRegistered();
        });
    });
}]);

Нет некоторых нужных полей, например, поле для выбора даты, которое появилось в Angular Material 0.11.0. Впрочем, обе библиотеки прекрасно работают вместе.

Если пишете SPA на Angular, для создания навбара и верхнего тулбара лучше используйте те, что идут в Angular Material. Почему? Потому что эти элементы статичны в MDL, а нам же нужно будет иногда менять их содержимое, не так ли? Да и привязка к контроллерам будет проще, если сделать один ng-view, без использования ng-include или сторонних вещей типа ui-route.

Комментариев нет :

Отправить комментарий

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

2 комментария

Наткнулся в сети на очень интересный способ обновления NodeJS до последней версии, не прибегая к услугам пакетного менеджера ОС. Ссылки на статью и оригинал:

В итоге у меня теперь ещё один фид в читаемых RSS.

Отмечу лишь, что установленную через пакетный менеджер версию нужно сначала вычистить из системы, так же рекомендуется удалить каталог /usr/local/lib/node_modules/. Вот команды (NodeJS должен быть установлен, желательно - собран из исходников, это не так уж и сложно):

От имени root
npm cache clean -f
npm install -g n
n stable

2 комментария :

Отправить комментарий

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

1 комментарий

Скачиваем пакет с исходным кодом с официального 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 (можно выйти из системы и войти снова) ярлык появится в главном меню.

1 комментарий :

Отправить комментарий

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.

Комментариев нет :

Отправить комментарий

Django - изменить размер изображения перед сохранением

2 комментария

Как изменить размер изображения перед сохранением? Никак. Но далее я опишу путь, на который указал пользователь StackOwerflow в своём ответе вот на этот вопрос.

Вкратце:

  • Сохраняем объект
  • Проверяем наличие данных в нужном поле типа ImageField
  • Открываем сохранённое изображение и меняем его свойства, как нам нужно

Пример

from PIL import Image
from django import models

from os import path

# Максимальный размер изображения по большей стороне
_MAX_SIZE = 300

class CarManufacter(models.Model):
    """Производитель автомобилей, два поля
       name - название, строка
       logo - логотип, изображение
    """
    name = models.CharField(
        'Наименование',
        max_length=100,
        unique=True
    )
    logo = models.ImageField(
        upload_to=path.join('car', 'manufacter', 'logo'),
        null=True,
        blank=True
    )

    def save(self, *args, **kwargs):
        # Сначала - обычное сохранение
        super(CarManufacter, self).save(*args, **kwargs)

        # Проверяем, указан ли логотип
        if self.logo:
            filepath = self.logo.path
            width = self.logo.width
            height = self.logo.height

            max_size = max(width, height)

            # Может, и не надо ничего менять?
            if max_size > _MAX_SIZE:
                # Надо, Федя, надо
                image = Image.open(filename)
                # resize - безопасная функция, она создаёт новый объект, а не
                # вносит изменения в исходный, поэтому так
                image = image.resize(
                    (round(width / max_size * _MAX_SIZE),  # Сохраняем пропорции
                    round(height / max_size * _MAX_SIZE)),
                    Image.ANTIALIAS
                )
                # И не забыть сохраниться
                image.save(filename)

2 комментария :

Отправить комментарий

Зависимости в Angular

1 комментарий

Внеднение зависимостей

Все знают, что Angular построен на постоянном и вездесущем внедрении зависимостей, т.н. dependency injection. Существуют как минимум три способа сделать это.

1. Implicit annotation

Зависимости просто перечисляются как аргументы нашего модуля.

Пример

(function (A){
    "use strict";
    A.module('app', []).controller('MyCtrl', function($scope, $sce){
        //Тут должен быть код контроллера
    });
}(this.angular));
Не используйте этот способ!

Всё хорошо до момента минификации нашего кода. Это потому, что при таком объявлении функций Angular для определения нужных зависимостей превращает их код в строку, которую затем разбирает. А при минификации, как известно, все идентификаторы заменяются на более короткие, что делает наши скрипты неработоспособными.

2. $inject

Хороший способ внедрения зависимостей - создать статическое свойство функции под названием $inject. В нужный момент Angular возьмёт список зависимостей оттуда.

Пример

(function (A){
    "use strict";

    function MyCtrl($scope, $sce){
        //Код контроллера
    }

    MyCtrl.$inject = [ '$scope', '$sce' ]; //Строки не сжимаются никогда

    A.module('app', []).controller('MyCtrl', MyCtrl);
}(this.angular));

Уже лучше. Такой код успешно переживёт минификацию.

3. Inline array annotation

Весьма хороший метод, не хуже предыдущего, но многие статические анализаторы кода, например, в Eclipse, считают такое поведение ошибкой (Они не правы!):

Пример

(function (A){
    "use strict";

    function MyCtrl($scope, $sce){
        //Код контроллера
    }

    A.module('app', []).controller('MyCtrl', [ '$scope', '$sce', MyCtrl ]);
}(this.angular));

Такой код тоже хорошо переживёт минификацию.

Дополнительно

Недавно узнал, что в Angular даже есть специальная директива для запрета внедрения зависимостей первым способом - ng-strict-di. Указывать её надо рядом с ng-app:

Пример

<!doctype html>
<html ng-app="app" ng-strict-di>
    <head>
        <!-- Код -->
    </head>
    <body>
        <!-- И тут тоже код -->
    </body>
</html>

После этого попытка внедрения зависимостей через Implicit Annotation будет вызывать ошибку.

1 комментарий :

Отправить комментарий

DRF, часть 1 - Проектирование структуры API

6 комментариев

Введение

Автор не является экспертом с мировым именем в данном вопросе и всего лишь публикует здесь свои мысли, которые кажутся ему правильными на момент написания. Для более полной информации о работе с Django REST Framework обращайтесь к официальной документации.

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

  1. 1. Дуб - дерево.
  2. 2. Олень - животное.
  3. 3. Смерь - неизбежна.
  4. 4. api - отдельное приложение в нашем проекте
  5. 5. Следует придерживаться общепринятых правил именования частей API
  6. 6. API должен быть версионным
  7. 7. API должен быть простым

С первыми тремя пунктами всё ясно, поэтому перейду сразу к четвёртому и нижеследующим.

Структура каталогов и версионность

Сначала я пытался запихнуть API в разные части основного проекта, создавал в каталоге с приложениями файлы api.py, serializers.py и permissions.py.

О том, насколько это плохая идея, я узнал почти сразу же, когда начал путаться с тем, что и где лежит. Стоило только переименовать один из каталогов, как сразу же всё начинало сыпаться. В итоге я пришёл к тому, что API - не просто отдельное приложение, содержащее только файлы __init__.py и urls.py (со ссылками на нужные файлы в приложениях Django), а полноценный модуль со множеством вложенных модулей. В общем, почувствуйте разницу:

Было
/api/
    __init__.py
    urls.py
/news/
    __init__.py
    admin.py
    api.py
    models.py
    serializers.py
    tests.py
    urls.py
    views.py
/comments/
    __init__.py
    admin.py
    api.py
    models.py
    serializers.py
    tests.py
    urls.py
    views.py
Стало:
/api/
    /v1/
        /news/
            /comments/
                __init__.pt
                api.py
                permissions.py
                serializers.py
                urls.py
            __init__.py
            api.py
            permissions.py
            serializers.py
            urls.py
    __init__.py
    urls.py
/core/
    /comment/
        __init__.py
        api.py
        permissions.py
        serializers.py
        urls.py    
    /news/
        /comments/
             __init__.py
            admin.py
            models.py
            tests.py
            views.py
         __init__.py
        admin.py
        models.py
        tests.py
        views.py

Надеюсь, структура понятна. Во-первых, всё, что связано с API, переехало в одноимённое приложение. Во-вторых, API теперь поддерживает версионность. Для этого нужно всего ничего - создать соответствующие каталоги и правильно описать файл urls.py в самом начале. Я сделал так:

api/urls.py
from django.conf.urls import include
from django.conf.urls import url

urlpatterns = [
    url(r'', include('api.v2.urls')),
    url(r'^v1/', include('api.v1.urls')),
    url(r'^v2/', include('api.v2.urls')),
]

Естественно, где-то в главном файле urls.py есть строка, в которой конфигурация URL для API присоединяется к общей конфигурации URL через всё тот же include().

Как видно из этого файла, если пользователь нашего API не указывает версию, он использует последнюю доступную. В то же время, при необходимости можно указать любую из имеющихся. В одной из статей я видел тезис, согласно которому компании, которые заботятся о своих клиентах и хотят сделать свой сервис успешным, крайне редко или вообще никогда не меняют API. Естественно, не меняеть его вообще никогда вряд ли получится, но можно сделать различия минимальными или предоставить возможность в течение длительного времени использовать старый API, т.к. разработчикам интересней писать что-то новое, а не переписывать старое только потому, что теперь какой-то метод в нашем API перестал работать.

Именование API

Сначала хочу сказать, как делать НЕ НАДО:

Вешать всё на один URI и в зависимости от содержимого полученных пакетов выполнять то или иное действие либо возвращать нужный набор данных.
Пример:
http://example.org/api/

Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.

Использовать глаголы в частях URL или для выполнения определённых действий.
Пример
GET /api/news/get/?id=1     - получить новость с id=1
GET /api/news/get/all/      - получить все новости
POST /api/news/add/         - добавить новость
POST /api/news/update/?id=1 - обновить новость с id=1
POST /api/news/delete/?id=1 - удалить новость с id=1
Ещё примерчик
GET  /api/news/?id=1               Получение записи с id=1
POST /api/news/?id=1?action=update Обновление записи с id=1
POST /api/news/?id=1?action=delete Удаление записи с id=1

Это API? Конечно же, нет! Суть REST-сервисов в том, что требуемое действие определяется HTTP-заголовком!

Пример
URL - /api/news/:id

GET    /api/news/ - получить список всех новостей
POST   /api/news/ - создать новость
GET    /api/news/12/ - вернёт новость с id=12
PATCH  /api/news/12/ - обновить новость с id=12
DELETE /api/news/12/ - удалить новость с id=12

Разница, как говорится, налицо.

Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).

Названия сущностей во множественном числе. Каждой сущности - отдельный URL.
Пример

/api/articles/:id/ - статьи
/api/friends/:id/  - друзья
/api/news/:id/     - новости
/api/users/:id/    - пользователи
/api/videos/:id/   - видео
Пример:
GET    /api/acticles/:id/comments/ - получить комментарии к статье
GET    /api/news/:id/comments/     - получить комментарии к новости
POST   /api/articles/:id/comments/ - добавить комментарий к статье
POST   /api/news/:id/comments/     - добавить комментарий к новости
DELETE /api/comments/articles/:id/ - удалить комментарий к статье
DELETE /api/comments/news/:id/     - удалить комментарий к новости

Сериализаторы и всё остальное.

Я считаю, что при написании новой версии API могут измениться поля, с которыми работают сериализаторы, поэтому для каждой версии их лучше создавать заново. Кроме того, сами методы для работы с данными могут стать другими. Отсюда следует простой вывод (до которого мне пришлось доходить своим умом пару месяцев):

Каждой версии API - свои права, сериализаторы и представления. Не следует делать предположений, что в течение продолжительного времени список полей моделей будет постоянным, а права на доступ к частям API не изменятся.

Простота API - почти недостижимый идеал. Чтобы сделать его простым, придётся проделать очень сложную работу. Не удивляйтесь. Кнопка включения на корпусе компьютера - очень простой интерфейс, однако, по одному нажатию на неё выполняется огромное количество самых разнообразных и сложных действий, на глубокое понимание которых некоторые люди тратят большую часть своей жизни. Таким же должен быть и ваш API - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.

Итоги

  • Разработчики хотят работать с сущностями, а не с URL. Сделайте интуитивно понятными все URL вашего API.
  • Разработчики ленивы и не хотят изучать 34 параметра, влияющие на результат вызова одного единственного метода (из 56, имеющихся в наличии). Старайтесь избегать реализации поведения API через параметры. limit, offset, filter и sorting - необходимое зло, от них никуда не деться.
  • Действие, выполняемое методом API, должно зависеть от HTTP-заголовка. GET для чтения, POST для создания, PUT/PATCH для обновления и DELETE для удаления записей.
  • Пишите документацию к вашему API. Даже самая лучшая структура URL для API неспособна описать различные тонкости и нюансы его работы.

6 комментариев :

Отправить комментарий

CSRFToken в Angular 1.4

Комментариев нет

В новом Angular 1.4 одно из приятных изменений - возможность задавать куку и заголовок для CSRF-токена. Раньше делали так:

angular.module('app', ['ngCookies']).run(['$http', '$cookies', function($http, $cookies){
    var token = $cookies.csrftoken;
    $http.defaults.headers.common['X-CSRFToken'] = token;
    $http.defaults.headers.post['X-CSRFToken'] = token;
}]);

Сейчас на этапе конфигурирования приложения можно сделать такой финт:

angular.module('app', []).config(['$httpProvider', function($httpProvider){
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
}]);

Оригинальная документация - здесь. Проверял работу на Django 1.8.

Комментариев нет :

Отправить комментарий

EMACS - начало

Комментариев нет

Введение

Я не буду здесь рассказывать в деталях, чем плоха та или иная IDE или чем мне не угодили VIM и Sublime Text. Начнём лучше с того, чем EMACS хорош.

EMACS хорош в первую очередь тем, что это не просто текстовый редактор, а настоящий конструктор, из которого можно сделать как полноценную IDE для разработки практически на любом языке, так и издательскую систему или текстовый процессор. Основная фишка - возможность расширять возможности редактора путём написания сценариев на языке EMACS Lisp. К счастью, всё уже написано до нас, и лично мне ни разу не приходилось писать что-то своё, однако при этом я смог превратить EMACS в IDE для WEB- и Python-разработки. Да, эту статью я тоже пишу в нём.

Во-вторых, он быстр. Он не требует JRE для своей работы, написан на C и EMACS Lisp, запускается и работает очень быстро.

Это свободное ПО, что для обычного российского пользователя означает как раз "Free beer".

Он не перегружен меню, диалоговыми окнами, кучей разных панелей и т.д. Лаконичный интерфейс легко может быть расширен или сведён к пугающему минимализму - выбор за вами.

Огромная база готовых пакетов. Там есть практически всё, что нужно, от поддержки редких языков до веб-браузера и игр. Серьёзно!

Установка

Установка в Windows предельно проста - нужно зайти на этот сайт и скачать архив с последней версией. Распаковываем её в любое нужное место, находим в каталоге bin файл runemacs.exe и создаём для него ярлык на Рабочем столе.

Установка в Linux может быть чуть сложнее. Как правило, в Debian и Ubuntu LTS идёт устаревшая версия дистрибутива. Я рекомендую использовать не ниже 24.0, в противном случае часть пакетов (можно считать их плагинами) не будет работать. Если в официальном репозитории слишком старая версия, придётся собирать EMACS из исходных кодов, что заслуживает отдельной статьи. Здесь на этом останавливаться не буду.

Помимо самого EMACS необходимо иметь в системе установленную систему контроля версий Git. Пользователи Windows могут скачать здесь.

Комбинации клавиш

Оригинальные комбинации клавиш EMACS устарели. Нажимать [Ctrl] (в любом руководстве по EMACS эта клавиша обозначается как [C]) мизинцем неудобно. Когда-то [Ctrl] располагался на месте сегодняшнего [Alt], и пользоваться им было удобно, но времена мэйнфреймов давно прошли... Подробный разбор того, почему комбинации клавиш в EMACS такие, какие есть, и почему это плохо, производится здесь [EN]. Нет смысла изучать дефолтные настройки комбинаций клавиш, всё равно придётся потом переучитваться. Однако, в экстренных ситуациях может пригодиться как минимум две команды:

Комбинация Действие
[M-X] Вход в минибуфер для ввода команд
[C-X, C-C] Завершение работы редатора

После установки и активации пакета ergoemacs-mode комбинации клавиш будут переопределены. Рекомендую сделать это как можно быстрее, т.к. отвыкнуть от Ctrl+X, Ctrl+C и Ctrl+V очень тяжело, да и нет смысла этого делать.

Терминология

Буфер - аналог вкладки с документом в других тектовых редакторах и IDE. Так же это область, куда выводятся сообщения плагинов или самого редактора.

Минибуфер - самый маленький буфер, расположенный в нижней части окна EMACS. В него вводятся различные команды. Для входа в него следует использовать комбинацию клавиш [M-X] без установленного и включенного пакета ergoemacs-mode, либо [M-A]. После ввода команда запускается на выполнение нажатием [Enter] (в документации EMACS эта клавиша называется [RET])

Пакет - аналог плагина в других редаторах и IDE. Представляет собой сценарий или набор сценариев на языке EMACS Lisp. Часть пакетов является встроенной в редактор и поставляется вместе с ним, другие нужно скачивать из репозитриев. Отметим, что сейчас практически не используется способ установки пакета ручным клонированием репозитория с GitHub, далее под репозиториями будут пониматься специализированные хранилища пакетов.

Основной режим - определяет поведение EMACS для буфера. От этого режима зависит поведение различных клавиатурных комбинаций и набор доступных дейстий. Например, при написании этой статьи я использую web-mode, что даёт подсветку синтаксиса, автоматическое форматирование и применение Emmet для создания разметки. Основной режим может быть лишь один (имеется пакет, позволяющий включить сразу несколько основных режимов для одного буфера, но я им ни разу не пользовался).

Дополнительный режим - в то время, как основной режим может быть лишь один, дополнительных режимов можно включить столько, сколько нужно. Например, сюда относятся средства проверки орфографии, возможность автокомплита, автоформатирование кода и т.д. Большая часть пакетов для EMACS работает именно в дополнительном режиме.

Первоначальная настройка

Настройка EMACS может производиться как минимум двумя способами.

Настройка через правку файла конфигурации .emacs

Основной файл настроек EMACS называется .emacs и располагается в домашнем каталоге пользователя. В случае с Windows это как правило каталог вроде этого:

C:\Users\xPhoenix\AppData\Roaming\.emacs

Однако, можно переопределить домашний каталог для хранения настроек (важно понимать, что файл .emacs станет вашей прелессссстью, которую вы будете оберегать и лелеять), создав для текущего пользователя системную переменную под именем HOME, и указав в ней путь к каталогу, который следует считать домашним для EMACS (возможно, работает и для других программ из мира Linux, не проверял). У меня эта переменная выглядит так:

D:\xphoenix\

Кроме этого файла EMACS при работе создаёт так же создаёт в домашнем каталоге папку .emacs.d. В ней будут храниться скачанные пакеты, сниппеты, файлы .desktop и т.д. Не надо его удалять!

В любом случае, в наш .emacs следует поместить как минимум вот эти строки:

(defalias 'yes-or-no-p 'y-or-n-p)

(setq package-archives '(
                         ("elpy" . "http://jorgenschaefer.github.io/packages/")
                         ("gnu" . "http://elpa.gnu.org/packages/")
                         ;; ("melpa" . "http://melpa.milkbox.net/packages/")
                         ("melpa-stable" . "http://melpa-stable.milkbox.net/packages/")
                         ("org" . "http://orgmode.org/elpa/")
                         ))

Первая строка указыает на то, что вместо ввода строк yes и no в ответы на запросы системы можно просто нажать [y] или [n] соответственно.

Чуть ниже идёт указание репозиториев для поиска пакетов. Репозиторий MELPA очень популярен и в Интернете, вы будете часто встречать отсылки именно к нему, но использовать его опасно, т.к. пакеты туда попадают прямо из ветки master с GitHub. Поэтому я предпочитаю MELPA-STABLE. Пусть пакеты старые и их гораздо меньше, зато ничего не сломается при очередном обновлении. Впрочем, в редких случаях я пользуюсь MELPA, например, если пакета вообще нет в STABLE или тот, что есть, слишком старый. Так же существует репозиторий MARMELADE, но я не вижу смысла его использовать, т.к. некоторые пакеты в нём даже более старые, чем в MELPA-STABLE.

Настройка через customize

В одной из версий EMACS в стандартную поставку стал входить пакет sustomize. Просто выполните одноименную команду, и попадете в буфер, где будет поле для поиска и множество гиперссылок для перехода к нужным настройкам. Отмечу лишь, что настройка некоторых параметров через customize конфликтует с ручной правкой .emacs, однако, в ряде случаев является более удобной. Некоторые параметры EMACS можно настроить только ручным редактированием .emacs, в customize вы просто не найдёте нужного раздела.

Установка пакетов

Указав в вашем .emacs репозитории так, как указано выше, перезапустите редактор (можно сделать и без перезапуска, но не буду усложнять) и выполните команду list-packages. Откроется буфер, содержащий список пакетов в доступных репозиториях. Выбор пакета для установки осуществляется нажатием клавиши [I], отмена выбора - [U], пометка на удаление - [D]. После того, как будет выбрано, что делать с пакетами, следует нажать клавишу X для запуска операций. EMACS установит и при необходимости выполнит компиляцию нужных пакетов.

Есть несколько пакетов, которые я настоятельно рекомендую к установке.

  • auto-complete - предназначение ясно из названия. Через него работают многие другие пакеты, например, jedi.
  • autopair - автоматически закрывает скобки
  • company - ещё одно средство для автокомплита, через него работают некоторые пакеты, которые не работают с auto-complete
  • emmet-mode - незаменимое средство для верстальщиков. Рекомендую ставить из MELPA, т.к. в STABLE очень старая версия, которая работает хуже и многого не умеет.
  • ergoemacs-mode - пакет, устанавливающий комбинации клавиш, удобные для использования людьми, а не Ричардом Столлманом. Подробное описание клавиш здесь.
  • flycheck - модуль проверки чего угодно на лету. На самом деле по-тихому вызывает имеющиеся в системе средства проверки и статического анализа кода. Имеет возможность конфигурирования того, какие средства и с какими параметрами следует использовать. Является более новым и прогрессивным в сравнении с загнивающим flymake.
  • jedi - автокомплит для Python-разработчиков. Настройка данного пакета достойна отдельной статьи.
  • js2-mode - расширенный по сравнению с js-mode режим правки JavaScript-кода. Ставить js3-mode не рекомендую, т.к. проект давно загнулся.
  • less-css-mode - не вижу смысла объяснять назначение данного пакета.
  • monokai - тема из Sublime Text 2, пожалуй, лучшая, что мне приходилось видеть.
  • neotree - порт плагина NerdTree из VIM, отображает слева (или справа, настраивается) дерево каталогов и файлов. Лучше ставить из MELPA.
  • rainbow-mode, rainbow-delimiters - в паре подсвечивают скобки и другие элементы разными цветами, позволяет легко находить ошибки типа "Забыл закрыть скобку".
  • web-beautify - доступен только из MELPA, позволяет автоматически форматировать JS, CSS и HTML-файлы. Для работы требует установленный в системе NodeJS и его пакет web-beautify.
  • web-mode - добавляет в EMACS возможность редактировать файлы XML, HTML, XHTML и др. Обеспечивает подсветку синтаксиса и т.д.

Комментариев нет :

Отправить комментарий

Получение аргументов URL в сериализаторах Django REST Framework

2 комментария

Небольшая заметка о том, как в ClassBasedView Django REST Framework получить значение параметра из URL.

Допустим, наши URL сконфигурированы таким образом:

urls.py
from django.conf.urls import url

from .api import ArticleListCreateView
from .api import ArticleDetailView
from .api import ArticleCommentListCreateView

urlpatterns = [
    url(r'^article/$', ArticleListCreateView.as_view()),
    url(r'^article/((\d+))/$', ArticleDetailView.as_view()),
    url(r'^article/((\d+))/comment/$', ArticleCommentListCreateView.as_view()),
]

Делать какую-либо работу для вида ArticleDetailView не приходится - достаточно создать класс, унаследованный от RetrieveUpdateDestroyAPIView.

Получить доступ к параметру pk в виде не проблема, достаточно переопределить метод get_queryset(), например, так:

article.comment.api.py
from rest_framework.generics import RetrieveUpdateDestroyAPIView

from article.models import Article
from .models import ArticleComment


class ArticleCommentListCreateView(RetrieveUpdateDestroyAPIView):

    model = ArticleComment

    # Возвращаем только комментарии к указанной статье
    def get_queryset(self):
        return ArticleComment.objects.filter(article=self.kwargs['pk])

    def get_serializer_class(self):
        if self.request.method == 'GET':
            return ArticleCommentReadSerializer
        return ArticleCommentWriteSerializer

А вот получить в сериализаторе значение параметра pk можно не вполне очевидным способом:

serializers.py
from django.shortcuts import get_object_or_404
from rest_framework import serializers

from .models import ArticleComment
from article.models import Article

# ArticleReadSerializer не описан из-за своей простоты

class ArticleCommentWriteSerializer(serializers.ModelSerializer):

    article = serializers.PrimaryKeyRelatedField(
        queryset=Article.objects.all(),
        default=None
    )

    # Заменяем валидатор поля article таким образом, чтобы всегда
    # подставлялось значение из URL
    def validate_article(self, value):
        # Кто бы мог подумать???
        article_id = self.context['view'].kwargs['pk']
        return get_object_or_404(Article, pk=article_id)

    class Meta:
        model = ArticleComment

2 комментария :

Отправить комментарий

Расширение контроллеров в Angular

Комментариев нет

Краткое содержание

В статье рассказывается о том, как в Angular можно реализовать наследование контроллеров.

Суть проблемы

Мне приходится писать довольно много кода на Angular, при этом заметил, что от контроллера к контроллеру меняются, порой, лишь незначительные части. Например, везде, где я использую ngInfiniteScroll, в $scope приходится помещать переменные allLoaded и loading, а так же обработчик loadMore(), выполняющий загрузку новых элементов. По незнанаю, приходилось многократно дублировать практически одинаковый код. Однако, один из пользователей StackOverflow нашёл очень хорошее решение данной проблемы.

Решение

Решение оказалось на удивление простым и заключается в использовании сервиса $controller.

BaseClass.js
(function (A){
    "use strict";
    // Базовый контроллер, который содержит код, общий для всех других контроллеров,
    // которые будут наследоваться от него
    A.module('App').controller('ListController', ['$scope', function($scope){
        $scope.allLoaded = false;
        $scope.items = [];
        $scope.loading = false;

        $scope.loadMore = function(){
            if ($scope.loading){
                return;
            } 
            // Тут какая-то работа по загрузке данных
        };

        $scope.refresh = function(noPlease){
            if (noPlease){
                return;
            }
            $scope.items.length = 0; // Более правильное решение, нежели $scope.items = [];
                                     // т.к. в этом случае сохраняется ссылка на оригинальный
                                     // объект
        };    
    }]);
}(this.angular));
ChildClass.js
(function(A){
    "use strict";
    A.module('App').controller('UserListController', [ '$controller', '$scope', function($controller, $scope){

            // Расширяем контроллер
            $controller('ListController', {
                $scope: $scope
            });

            // Теперь можно собственные свойства описывать
        }]);
}(this.angular));

Комментариев нет :

Отправить комментарий

Схема БД или таблицы в PostgreSQL

Комментариев нет

Всё время забываю, как в PostgreSQL сделать дамп схемы БД, без данных. Команда на удивление проста:

pg_dump -s -d %DATABASE_NAME% -U %USER_NAME% > %FILE_NAME%
Параметр Назначение
-s Ключ указывает на то, что нужно сделать копию только схемы БД
-d Ключ, задающий имя БД, с которой нужно работать.
-U Ключ, указывающий пользователя, от имени которого будет делаться копия схемы. Пользователь должен иметь доступ хотя бы на чтение данной БД.
%FILE_NAME% Файл, в который следует сохранить вывод. В противном случае вся схема будет выведена на экран.

А если вместо -s указать -t и потом имя таблицы, то будет снята схема только с неё, например:

pg_dump -d project -U xphoenix -t auth_users > auth_users_shema.sql

Комментариев нет :

Отправить комментарий

Проблемы с кириллицей в имени пользователя Windows

Комментариев нет

При использовании Atom столкнулся с тем, что некоторые плагины отказывались работать, выдавая странные сообщения в консоль. Сначала я решал проблему тем, что правил переменную PATH на уровне системы и на уровне пользователя, однако, это особого эффекта не имело, т.к. стоило только поставить более новую версию io.js, как PATH тут же сбрасывалась к примерно такому виду:

    C:\Python34\;C:\Python34\Scripts;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Program Files (x86)\Git\cmd;C:\Program Files (x86)\iojs\;C:\Users\%30o$321\AppData\Roaming\npm

Из этого видно, что портится путь к текущей системной папке пользователя. Изменение свойств папок "Видео", "Изображения", "Документы" и т.д. тут не может ничем помочь. Я использую учётную запись Microsoft со всеми её удобствами вроде OneDrive, WindowsPhone и т.д. и отказываться от неё в пользу локальной учётной записи не хотел (не за то деньги плачены). Поиск привёл меня к решению.

На первом этапе нам нужно создать дополнительную учётную запись типа "Администратор". Способ создания значения не имеет, я рекомендую через "Панель управления" (пользователи Windows 8.1 непрофессиональной редакции выбора не имеют). Теперь нужно загрузиться в безопасном режиме и войти в систему под новосозданной учётной записью. После этого нужно запустить редактор реестра - Regedit. Следует открыть раздел реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList. В нём содержится несколько подразделов. Если щёлкнуть на одном из них, то появится список ключей, одним из которых будет ProfileImagePath. Заменяем имеющееся значение на нужное, например, вот так:

C:\Users\Максим -> C:\Users\xPhoenix

Так же следует обязательно переименовать имеющуюся папку профиля. После этого следует перезагрузить компьютер и при необходимости удалить созданную ранее дополнительную учётную запись администратора.

Проблема с кириллицей в имени пользователя наблюдается и в некоторых других программах. Насколько помню, из-за неё среда MATLAB 7.5 вообще отказывалась сохранять файлы.

Комментариев нет :

Отправить комментарий

Собственная модель пользователя Django (>=1.8)

24 комментария

Введение

Статья была обновлена 5 июня 2015 года и содержит исправление некоторых ошибок и дополнительную информацию.
На GitHub был опубликован репозиторий с исходными кодами, которые содержат ряд исправлений и дополнений. Нашли ошибку? Создайте pull-request или issue, я обязательно посмотрю.

На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели User, имеющейся в Django.

В настоящее время так же широко используются ещё два способа:

  • Создание связанной с пользователем 1 к 1 модели профиля.
  • Полная замена стандартной модели пользователя на свою с последующим переписыванием бэкэндов авторизации.

Первый способ лично мне не нравится своей устарелостью. Ещё в Django 1.5 была проделана огромная работа по рефакторингу существующих модулей, связанных с пользователями, что дало возможность расширять имеющуюся модель. К тому же, способ с дополнительной моделью заставляет вас писать дополнительный код, например, на отлов события создания или удаления модели пользователя.

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

Создание расширенной модели

Создавать новую модель пользователя мы будем на основе уже имеющейся в Django, т.е. будем использовать наследование. Первым делом следует создать новое приложение Django:

djangoadmin.py startapp extuser

Мне нравится использовать имя extuser для решения поставленной задачи потому, что, оно полностью передаёт суть и назначение данного приложения - расширение стандартной модели пользователя.

Модифицируем файл models.py нового приложения.

extuser/models.py

from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.models import BaseUserManager
from django.db import models


class UserManager(BaseUserManager):

    def create_user(self, email, password=None):
        if not email:
            raise ValueError('Email непременно должен быть указан')

        user = self.model(
            email=UserManager.normalize_email(email),
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_admin = True
        user.save(using=self._db)
        return user


class ExtUser(AbstractBaseUser, PermissionsMixin):

    email = models.EmailField(
        'Электронная почта',
        max_length=255,
        unique=True,
        db_index=True
    )
    avatar = models.ImageField(
        'Аватар',
        blank=True,
        null=True,
        upload_to="user/avatar"
    )
    firstname = models.CharField(
        'Фамилия',
        max_length=40,
        null=True,
        blank=True
    )
    lastname = models.CharField(
        'Имя',
        max_length=40,
        null=True,
        blank=True
    )
    middlename = models.CharField(
        'Отчество',
        max_length=40,
        null=True,
        blank=True
    )
    date_of_birth = models.DateField(
        'Дата рождения',
        null=True,
        blank=True
    )
    register_date = models.DateField(
        'Дата регистрации',
        auto_now_add=True
    )
    is_active = models.BooleanField(
        'Активен',
        default=True
    )
    is_admin = models.BooleanField(
        'Суперпользователь',
        default=False
    )

    # Этот метод обязательно должен быть определён
    def get_full_name(self):
        return self.email

    # Требуется для админки
    @property
    def is_staff(self):
        return self.is_admin

    def get_short_name(self):
        return self.email

    def __str__(self):
        return self.email

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = UserManager()

    class Meta:
        verbose_name = 'Пользователь'
        verbose_name_plural = 'Пользователи'

Рассмотрим этот код.

Менеджер моделей

Сначала идёт описание менеджера для данной модели. Описывать его нужно для того, чтобы правильно работали методы создания нового пользователя. Чуть ниже в коде будет указано, что для работы с объектами типа ExtUser нужно использовать именно его. Можно определить несколько менеджеров при необходимости, каждый из которых будет отвечать за свою часть работы.

Модель ExtUser

В этой модели как ключевое указано поле email. Я считаю, что это один из лучших способов для авторизации пользователей. Лучше может быть только двухфакторная авторизация через SMS. Создав ключевое поле, следуте обязательно указать, что оно используется в качестве имени пользователя:

USERNAME_FIELD = 'email'

Делали бы мы авторизацию через номер телефона - указали бы так:

phone = CharField(
    'Номер телефона'
    max_length=20,
    unique=True,
    db_index=True
)

#Чуть ниже в этом же классе:
USERNAME_FIELD = 'phone'

Надеюсь, общий принцип понятен. Дальше уже идёт отсебятина, которую можно не писать. Например, аватары, отчество и т.д. Список полей в каждом проекте будет разным. Не забудьте только описать методы проверки прав has_perm() и has_module_perms() соответственно.

Формы

Наш класс не будет работать, если не создать для него формы админки. Создадим в приложении файл forms.py.

extuser/forms.py

from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth import get_user_model


class UserCreationForm(forms.ModelForm):
    password1 = forms.CharField(
        label='Пароль',
        widget=forms.PasswordInput
    )
    password2 = forms.CharField(
        label='Подтверждение',
        widget=forms.PasswordInput
    )

    def clean_password2(self):
        password1 = self.cleaned_data.get('password1')
        password2 = self.cleaned_data.get('password2')
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError('Пароль и подтверждение не совпадают')
        return password2

    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)
        user.set_password(self.cleaned_data['password1'])
        if commit:
            user.save()
        return user

    class Meta:
        model = get_user_model()
        fields = ('email',)


class UserChangeForm(forms.ModelForm):

    '''
    Форма для обновления данных пользователей. Нужна только для того, чтобы не
    видеть постоянных ошибок "Не заполнено поле password" при обновлении данных
    пользователя.
    '''
    password = ReadOnlyPasswordHashField(
        widget=forms.PasswordInput,
        required=False
    )

    def save(self, commit=True):
        user = super(UserChangeForm, self).save(commit=False)
        password = self.cleaned_data["password"]
        if password:
            user.set_password(password)
        if commit:
            user.save()
        return user

    class Meta:
        model = get_user_model()
        fields = ['email', ]


class LoginForm(forms.Form):

    """Форма для входа в систему
    """
    username = forms.CharField()
    password = forms.CharField()

Здесь описаны две формы - для создания нового пользователя и для смены пароля. Т.к. я использую Django REST Framework, я сделал эти формы для того, чтобы корректно работало обновление модели пользователя, т.к. поле password является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.

Как правило, ошибка обновления модели в DRF происходит при полном обновлении модели. Для частитчного обновления нужно указывать в заголовке HTTP-запроса метод PATCH вместо PUT.

Пожалуй, единственное, на что тут нужно обратить внимание - это описание связи наших форм с моделью пользователя. Если в будущем понадобится создать новую модель или переименовать её, переписывать код не придётся, т.к. используется функция get_user_model(), возвращающая класс модели пользователя, используемый для авторизации. Если проще, то эта функция возвращает класс, указанный в параметре AUTH_USER_MODEL в файле settings.py нашего приложения.

Админка

Изменения придётся внести и в файл admin.py:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group

from .forms import UserChangeForm
from .forms import UserCreationForm
from .models import ExtUser


class UserAdmin(UserAdmin):
    form = UserChangeForm
    add_form = UserCreationForm

    list_display = [
        'date_of_birth',
        'email',
        'firstname',
        'is_admin',
        'lastname',
        'middlename',
    ]

    list_filter = ('is_admin',)

    fieldsets = (
                (None, {'fields': ('email', 'password')}),
                ('Personal info', {
                 'fields': (
                     'avatar',
                     'date_of_birth',
                     'firstname',
                     'lastname',
                     'middlename',
                 )}),
                ('Permissions', {'fields': ('is_admin',)}),
                ('Important dates', {'fields': ('last_login',)}),
    )

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': (
                'date_of_birth',
                'email',
                'password1',
                'password2'
            )}
         ),
    )

    search_fields = ('email',)
    ordering = ('email',)
    filter_horizontal = ()

# Регистрация нашей модели
admin.site.register(ExtUser, UserAdmin)
admin.site.unregister(Group)

Настройка Django

Пришло время переустановить Windows внести изменения в файл settings.py.

settings.py

# Тут должен быть импорт нужных модулей.

SECRET_KEY = # Какой-то ключ, автоматически созданный Django

DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []

EXTERNAL_APPS = (
    # У меня, например, здесь разные внешние библиотеки
)

INTERNAL_APPS = (
    # Тут список наших приложений
    'extuser',
    # А тут его продолжение
)

DJANGO_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
)

INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS + DJANGO_APPS

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.locale.LocaleMiddleware',
)

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.csrf',
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.request',
)

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
)

LOGIN_URL = r"/login/"

AUTH_USER_MODEL = 'extuser.ExtUser'

Миграции

Заключительный этап настройки нашей модели - проведение миграций. Запустите скрипт:

python manage.py migrate

Для создания нового пользователя-администратора используйте команду createsuperuser:

python manage.py createsuperuser

24 комментария :

Отправить комментарий