Debian, Dojo, Django, Python

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

Показаны сообщения с ярлыком django. Показать все сообщения
Показаны сообщения с ярлыком django. Показать все сообщения

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)

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. Не такая уж большая проблема, но под определение подводного камня подходит хорошо.

Gulp для сборки приожений Angular 1.x

Немного про Gulp

Было время, когда мне очень нравился Grunt, а причина была проста - другие средства сборки Web-проектов только начинали развиваться (2013 год, если что). Вскоре я открыл для себя Gulp. В сравнении с Grunt это был действительно удобный инструмент, и я надолго остановился именно на нём.

Всё никак не найду время на изучение Angular 2 и Webpack, поэтому один из мелких проектов пишу на Angular 1.x (актуальная версия на момент написания статьи - 1.6.x). Кто писал на нём, знает, что все контроллеры, директивы, сервисы и прочие компоненты лучше размещать в отдельных файлах, однако, потом на странице нужно подключать каждый файл отдельно.

Мириться с этим было нельзя, я поискал на StackOverflow похожие запросы и выяснил, что многие сталкивались с этой проблемой. Я предлагаю своё решение, являющееся компиляцией из разных источников.

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

В каталоге проекта инициализируем npm, отвечаем на несколько простых вопросов, после чего ставим необходимые плагины:

npm init && npm install gulp gulp-rename gulp-uglify gulp-include gulp-watch --save
Название Описание
gulp-rename Позволяет указать имя целевого файла, отличное от исходного. Используется для создания отладочных и минимизированных версий файлов.
gulp-uglify Сжимает JS. Используется для создания минимизированных продуктовых версий.
gulp-include Позволяет вставлять код из одного файла в другой подобно тому, как это делает директива #include> в C-подобных языках. Так же позволяет делать вызов require, при этом код будет вставлен только в том случае, если не вставлялся ранее.
gulp-watch Отслеживает изменения в нужных файлах, при необходимости автоматически вызывая нужные Gulp Task

gulpfile.js

В каталоге приложения так же создадим файл по имени gulpfile.js (регистр важен) со следующим содержимым:

gulpfile.js
(function (r) {
    "use strict";

    var gulp = r("gulp"),
        uglify = r("gulp-uglify"),
        include = r("gulp-include"),
        rename = r("gulp-rename"),
        sources = {
            login: "./src/login.js",
            app: "./src/app.js"
        },
        DEST = "./todoist/static/app/"

    gulp.task("js-dev", function () {
        // Сборка без минификации, специально для отладки
        gulp.src(sources.login)
            .pipe(gulp.dest(DEST));

        gulp.src(sources.app)
            .pipe(include())
            .pipe(gulp.dest(DEST));
    });

    gulp.task("js-prod", function () {
        gulp.src(sources.login)
            .pipe(uglify())
            .pipe(rename("login.min.js"))
            .pipe(gulp.dest("./todoist/static/app/login.min.js"));

        gulp.src(sources.app)
            .pipe(include())
            .pipe(uglify())
            .pipe(rename("app.min.js"))
            .pipe(gulp.dest(DEST));
    });

    gulp.task("watch", function () {
        gulp.watch("./src/**/*.js", ["js-dev"]);
    });

    gulp.task("default", ["js-dev", "watch"]);

}(require));

Надеюсь, комментарии к тому, что тут происходит, не нужны.

Файлы приложения

Все исходные файлы приложения у меня лежат в src/, для контроллеров создан каталог controllers, для сервисов - services (директивы и фильтры пока не успел написать, поэтому и каталогов нет).

Так выглядит главный файл приложения:

app.js
(function (A) {
    "use strict";
    var app = A.module("todoist", ["ui.bootstrap", "ngRoute", "ngCookies"]);

    app.config(["$interpolateProvider", "$httpProvider", "$routeProvider", function ($interpolateProvider, $httpProvider, $routeProvider) {
        $interpolateProvider.startSymbol("{$");
        $interpolateProvider.endSymbol("$}");

        $httpProvider.defaults.xsrfCookieName = "csrftoken";
        $httpProvider.defaults.xsrfHeaderName = "X-CSRFToken";
        $httpProvider.defaults.headers.common.Authorization = "Basic";


        $routeProvider.when("/", {
            controller: "MainController",
            templateUrl: "/ui/index.html"
        }).when("/users", {
            controller: "UserListController",
            templateUrl: "/ui/users/list.html"
        }).when("/users/:id", {
            controller: "UserDetailController",
            templateUrl: "/ui/users/detail.html"
        }).otherwise({
            redirectTo: "/login"
        })
    }]);

    //=include ./controllers/*.js
    //=include ./services/*.js"
}(this.angular));

При сборке вместо комментариев //=include ./controllers/*.js будет вставлено содержимое лежащих там файлов - как раз то, чего я и хотел добиться.

Последний шаг - подключить файл приложения в шаблоне Django:

<script src="{% static 'app/app.js' %}"></script>

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)
        )

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

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 - изменить размер изображения перед сохранением

Как изменить размер изображения перед сохранением? Никак. Но далее я опишу путь, на который указал пользователь 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)

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

Введение

Автор не является экспертом с мировым именем в данном вопросе и всего лишь публикует здесь свои мысли, которые кажутся ему правильными на момент написания. Для более полной информации о работе с 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 неспособна описать различные тонкости и нюансы его работы.

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.

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

Небольшая заметка о том, как в 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

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

Введение

Статья была обновлена 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

str object has no attribute _meta

Досадная ошибка, с которой пришлось столкнуться, вынесена в название статьи. Потратил два часа на поиски, просмотрел все файлы по нескольку раз - и ничего. Сегодня с утра (а утро вечера мудренее) обнаружил причину.

Было:
class UserChangeForm(forms.ModelForm):

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

def clean_password(self):
    return self.initial['password']

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 UserChangeForm(forms.ModelForm):

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

def clean_password(self):
    return self.initial['password']

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 Meta:
    model = get_user_model()
    fields = ('email',)
Ошибки исключены
class Meta:
    model = get_user_model()
    fields = ['email']

Анонс статьи по Django REST Framework и Angular

В сети крайне мало информации по работе с Django REST Framework, а на русском можно сказать что нет вообще. Учитывая сложность вопросов на Toster и устарелость статей на Хабре, решил написать довольно объёмную статью по работе с данным фреймворком. Сам не являюсь профессионалом в нём и WEB-разработке вообще, но поделиться накопленными знаниями будет не лишне.

Примерное оглавление

  • Зачем нужен DRF?
  • Установка, подключение к проекту.
  • Сериализаторы
  • Виды-функции и виды-классы
  • Типовые задачи
  • Подключение Angular Resource
  • Несколько советов по API
  • Исходный код моего модуля Angular для работы с API с объяснением написанного

Пишите в комментариях, какие вопросы дополнительно следует включить в статью, постараюсь рассмотреть их все.

Django и $resource

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

Решил использовать для реализации RESTful API своего приложения такой модуль Angular, как angular-resource. Проблем с установной и подключением, как обычно, не было. Создал свой сервис:

(function (A, U) { //U === undefined
    "use strict";
        
    A.module('DesktopApplication').factory('Category', function ($resource) {
        var instance = {
            selectedRow: U, //Используется в моём проекте для обмена данными между контроллерами,
                            //не является обязательным элементом
            resource: $resource('/category/rows/:id', {id: '@id'})
        };
        return instance;        
    });
}(this.angular));

Казалось бы, всё должно работать. И ведь правда, вот такое работает (GET-запрос на получение данных):

//Где-то в коде, использующем сервис Category
Category.resource.query(function (items){
    $scope.rows = items;
});

А вот такое уже нет:

//Чуть ниже
Category.save({'id': 4, 'name': 'Username'}, function (result) {
    $scope.success = result.success;
});

Проблема оказалась в том, что даже явно указав слэш в конце строки ресурса, я получил POST-запросы, идущие к адресам без слэша, и Django такие запросы тут же банит. Начался поиск и чтение StackOverflow.

Решение

Решение оказалось довольно простым. Нужно указать в настройках приложения, что удалять концевой слэш для URL в AJAX-запросах не надо (а по-умолчанию включено). Кроме того, для корректной работы запросов методом PUT и DELETE нужно дополнительно конфигурировать $http. Иначе декоратор @csrf_protect из комплекта Django будет резать такие запросы. С учётом вышесказанного, скрипт настройки приложения стал выглядеть так (для сокращения размера выброшены прочие модули, которые я на самом деле использую в своём проекте):

(function(A) {
    "use strict";
    A.module('DesktopApplication', [ 'ngCookies', 'ngResource')
     .config(function ($interpolateProvider, $resourceProvider) {
         $interpolateProvider.startSymbol('{$');
         $interpolateProvider.endSymbol('$}');
         $resourceProvider.defaults.stripTrailingSlashes = false;
      })
     .run(function ($http, $cookies) {
          $http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
          $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
          $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
     });
}(this.angular, this.jQuery));

AngularJS, Django и POST

Введение

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

  • Как передавать CSRF-Token Django при каждом запросе?
  • Почему данные, отправляемые через POST, не видны в объекте request Django?
  • CSRF-Token

    Для CSRF-Token нашлось очень простое решение. Достаточно подключить модуль angular-cookies и при старте приложения задать параметры сервиса $http. С учётом того, что скобки для Angular у меня заменены с {{ и }} на {$ и $}, заготовка главного модуля приложения у меня выглядит так:

    (function(A){
        "use strict";
        A.module('DesktopApplication', ['ngCookies']).config(function($interpolateProvider){
            $interpolateProvider.startSymbol('{$');
            $interpolateProvider.endSymbol('$}');
    
        }).run(['$http', '$cookies', function ($http, $cookies){
            $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
            $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
            }]);
    }(this.angular));

    Естественно, на сервере при отдаче шаблонов нужно, чтобы соответствующие Cookies были установлены. Делается очень просто:

    return render_to_response('template.html', RequestContext(request))

    Angular и POST

    Отдельной болью стала отправка данных на сервер через POST. Было потрачено несколько часов на чтение Тостера, официальной документации по Angular и Django, пока наконец я не наткнулся на решение одного из пользователей StackOverflow. Суть решения такова: Angular при отправке данных не превращает их в ожидаемый на бекэнде формат, а передаёт их как строку.

    Допустим, у нас имеется такой объект:

    userdata = {
        login: 'userlogin',
        password: '123456'
    };

    Попробуем отправить эти данные:

    $http.post('/login/', userdata).success(function(data){
        //Что-то делаем с полученными в ответ данными
    });

    В консоли Firebug будет выведена строка запроса:

    "{"login": "userlogin", "password": "123456"}"

    Попробуем получить эти данные:

    def login_view(request):
        if request.method == 'POST' and request.is_ajax():
            username = request.POST.get('login', '')
            password = request.POST.get('password', '')

    Мы ожидаем, что в третьей и четвёртой строках будет примерно следующее:

    username = 'username'
    password = '123456'

    На самом деле этого не происходит, т.к. данные должны быть переданы в таком виде:

    login=userlogin&password=123456

    Проблема в том, что в текущей версии Angular (1.3.4 на момент написания статьи) нет функции для приведения объекта в такой вид. Поэтому автор посоветовал использовать старый добрый jQuery, точнее его функцию param:

    $http.post('/login/', $.param(userdata)).success(function(data){
        //Обработка ответа сервера
    });

    Вот такой код будет работать. Так же пользователи предлагали самописные функции, выполняющие ту же работу, но я советую использовать проверенные решения. Кроме этого обязательно присутствие в заголовке пост параметра Content-Type, равного application/x-www-form-urlencoded, соответствующий код приведён выше.

    Apache2, nginx, virtualenv и Python 2.6 в Debian

    Как я уже писал, в моей компании многие продукты писаны на Python, при том некоторые на Python 2.6, что не может не удручать, т.к. в большинстве популярных дистрибутивов давно используется Python 2.7 и выше. В этой статье рассказывается, как настроить Debian 7.1 Wheezy. Как обычно, всё делается под root или через sudo. Явно я об этом больше нигде в статье писать не буду.

    Собственно, почему Debian?

    В отличие от той же CentOS 6.4 Debian удобнее в администрирование. Говоря это, я имею ввиду, не количество пакетов, а то, что он "из коробки" имеет кучу оптимальных настроек, в то время как в CentOS приходится всё допиливать под себя.

    1. Огромное количество пакетов. Я не знаю, есть ли где-то ещё больше. Если есть - напишите мне.
    2. Python 2.6 в официальном репозитории. Да, Debian 7.1 не требует возни с дохлыми змеями.
    3. Под него есть официальные сборки PostgreSQL и Nginx. Я раньше тоже думал, что MySQL наше всё, а сейчас вообще не понимаю, как можно пользоваться этой поделкой, не соблюдающей 90% мировых SQL-стандартов. Да, если вы пишете скрипты, руководствуясь SQL-92 или SQL-99, можете быть уверены, что в PostgreSQL они будут работать без каких-либо изменений, в то время как в MySQL их придётся переписывать, и весьма основательно (к слову, в разработке PostgreSQL принимают участие несколько профессоров математики и информатики, что как бы говорит о качестве получающегося продукта).
    4. Стабильность. Пакеты старые, но если они есть в репозитории, то можно не сомневаться, что они будут работать так, как то ожидается.

    Немного про виртуальные окружения

    Пакет python-virtualenv позволяет на одной системе иметь несколько версий не только интерпретатора, но и одних и тех же пакетов. Допустим, нужно, чтобы проект работал с soaplib версии 0.8.0, а при очередном плановом обновлении системы версия пакета повысится до 1.0.0, некоторые функции будут удалены как устаревшие, в итоге приложение перестанет работать вовсе. Чтобы такого не случалось, лучше всего вовремя создать виртуальное окружение. Пакеты в нём будут именно той версии, которая нужна, независимо от того, какая версия пакета установлена в системе.

    Также следует пару слов сказать про пакет virtualenvwrapper. Это небольшой скрипт для ленивых питонистов, который позволяет удобно и быстро управлять созданными виртуальными окружениями.

    Бывает два вида виртуальных окружений - изолированные и открытые. Отличия таковы: изолированное окружение не видит пакетов, установленных в системе. При создании открытого виртуального окружения можно быть уверенным, что приложение будет видеть те пакеты, что установлены в системе. Если, например, все приложения используют psycopg2, lxml и pytz одной и той же версии, почему бы не установить его на уровне системы, а затем не заставить окружения "видеть" его?

    Nginx как фронтэнд Apache2

    В этой статье не будет рассказываться, как заставить Nginx работать в связке с Gunicorn и Supervisor. Я расскажу, как научить Nginx раздавать статику, а все остальные запросы переадресовывать Apache. Сразу скажу о недостатках используемого подхода.

    • Первое - модуль mod_wsgi, необходимый для увязки Apache с Python. Ограничение тут такое, что этот модуль может работать только с одной версией Python, т.е. заставить его обрабатывать проекты, работающие на Python2.6 и также проекты на Python2.7 не получится, нужно выбрать что-то одно. При неправильном выборе версии будете получать ошибку No module named django.core.handlers.wsgi. Об этом чуть позже.
    • Второе - прожорливость Apache. Nginx частично решит проблему, тем не менее, без тонкой настройки Apache ест ОЧЕНЬ много памяти и создаёт отдельный процесс на КАЖДОЕ подключение, что не есть хорошо.

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

    Первым делом нужно добавить репозиторий nginx и ключ для него в список пакетов apt. Многие рекомендуют отредактировать файл /etc/apt/sources.list. Мне это решение не по душе, т.к. я считаю, что там должны быть только те репозитории, которые относятся непосредственно к системе, поэтому делаем так:

    nano /etc/apt/sources.list.d/nginx.list

    В этот файл нужно написать:

    deb http://nginx.org/packages/debian/ wheezy nginx
    deb-src http://nginx.org/packages/debian/ wheezy nginx

    Соответственно, если используется другая версия, нужно написать её имя. Для Ubuntu нужно также заменить /debian/ на /ubuntu/.

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

    wget http://nginx.org/keys/nginx_signing.key
    apt-key add nginx_signing.key

    Если wget откажется скачивать ключ (во всяком случае, такое было у меня при добавлении репозитория PostgreSQL), необходимо также добавить параметр --no-check-certificate.

    Делаем обновление списка доступных для установки пакетов, обновление устаревших и установку nginx

    apt-get update && apt-get dist-upgrade && apt-get install nginx -y

    Когда поставится, можно будет увидеть сообщение об успешной установке.

    Теперь установка остальных пакетов и Apache2:

    apt-get install apache2 libapache2-mod-wsgi libapache2-mod-rpaf python-dev libxml2-dev libxslt1-dev libpq-dev python-pip python-setuptools  python-virtualenv virtualenvwrapper -y

    По шагам:

    • apache2 - сам сервер, обязательно
    • libapache2-mod-wsgi - обязательно, без него не получится увязать Apache с Python'ом
    • libapache2-mod-rpaf - обязательно, иначе при работе в связке с nginx Apache не будет отличать одно подключение от другого
    • python2.6-dev - обязательно, нужен, если расширения для Python будут собираться в виртуальном окружении (а они будут!)
    • libxml2-dev, libxslt1-dev - нужны для сборки и корректной работы библиотек lxml, xmlsec и других в виртуальном окружении, ставить по желанию (если не поставить, собрать, например, пакет lxml в виртуальном окружении не получится)
    • libpq-dev - опционально, если используется модуль psycopg2
    • python-pip - опционально, для установки пакетов Python, распространяемых в исходных кодах. Рекомендую в систему ставить пакет из репозиториев Debian, а в виртуальные окружения через easy_install pip
    • python-setuptools - обязательно, для установки пакетов в формате egg (вообще setuptools может ставить и другие форматы пакетов, но он всё равно нужен для последующей установки pip)
    • python-virtualenv, virtualenvwrapper - рекомендуется, первый для создания виртуальных окружений, второй для удобного управления ими

    Помимо этих пакетов apt сам подтянет кучу зависимостей, нужно их все поставить.

    Если Вы не стали ставить системный pip, его можно поставить следующей командой:

    easy_install pip

    По команде pip freeze можно увидеть список установленных пакетов и их версий, либо сообщение об ошибке. Ничего страшного, работе это не помешает. Если установка прошла успешно, можно приступать ко второму этапу.

    Настройка виртуальных окружений

    Перед тем, как приступать к настройке виртуальных окружений, желательно добавить в начало своего .bashrc следующее:

    export WORKON_HOME=/var/www/.virtualenvs
    export VIRTUALENVWRAPPER_LOG_DIR=/var/www/.virtualenvs
    export VIRTUALENVWRAPPER_HOOK_DIR=/var/www/.virtualenvs
    Естественно, папка /var/www/.virtualenvs должна существовать и быть доступной для записи для текущего пользователя. Если определения этих переменных не будет, virtualenv создаст папку .virtualenvs в домашнем каталоге пользователя. Итак, переопределили папку для хранения виртуальных окружений и где их должен искать workon (команда будет доступна после установки virtualenvwrapper, без параметров выдаёт список существующих окружений). Нужно перелогиниться либо перечитать файл настроек .bashrc:
    source .bashrc

    Примечание: не надо редактировать /etc/bash.bashrc, начнутся конфликты из-за того, что после root никто не сможет писать в указанную папку виртуальных окружений, эти настройки делаются в файле пользователя, который будет с этими окружениями работать.

    Создадим виртуальное окружение командой (под обычным пользователем):

    mkvirtualenv new_env --python=/usr/bin/python2.6

    Этой командой мы создали в каталоге /var/www/.virtualenvs каталог new_env, куда поместили одноимённое окружение, которое будет работать с Python2.6. Если не указывать ключ --python, окружение будет работать с тем интерпретатором, на который указывает мягкая ссылка в каталоге /usr/bin (в Debian 7 по умолчанию это Python2.7). Некоторые пишут, что обязательно нужно также указать --no-system-site-packages. У меня встречный вопрос: зачем, если по умолчанию все окружения и так создаются с этим ключом? Переопределить ссылку на интерпретатор Python можно так (под root):

    cd /usr/bin
    rm -f python
    ln -s python2.6 python

    Теперь команда workon выдаст нам список наших окружений, а команда workon %имя_окружения% его активирует.

    workon new_env

    Слева от интерпретатора появится подсказка, например, такая:

    (new_env)xphoenix@debian:/etc/apt/sources.list.d$

    Тут команда pip freeze выведет сразу:

    argparse==1.2.1
    distribute==0.6.24
    wsgiref==0.1.2

    Не надо удалять эти пакеты! Без них окружение работать НЕ БУДЕТ!

    Можно немного допилить окружение, для этого в каталоге bin нужно отредактировать файл activate
    nano /var/www/.virtualenv/new_end/bin/activate

    Добавим в самое начало строку:

    export DJANGO_SETTINGS_MODULE='project.settings'

    Те, кто пользуется Django, сразу поняли, что произошло. Если Вы не поняли, объясняю: эта переменная указывает, где искать файл настроек для Django-проекта. После этого нужно выйти из окружения командой

    deactivate

    и активировать снова.

    Настройка Apache 2

    Апач поставлен, но т.к. он ставился после Nginx, он не сможет запуститься, т.к. порт 80 уже занят для прослушивания. Делаем так:

    nano /etc/apache2/ports

    Тут должны присутствовать вот такие строки:

    ServerName debian.lo
    NameVirtualHost 127.0.0.1:8080
    Listen 127.0.0.1:8080

    Вместо debian.lo можно написать хоть ololoyakrutoj, ни на что не влияет, но избавляет от сообщения об отсутствии имени при перезапуске Apache. Следующие две строки говорят, что будут использоваться виртуальные хосты, находящиеся на локалхосте на порту 8080, и слушать Apache будет только его. Тем самым убираем конфликт с Nginx.

    Включаем модуль rpaf (wsgi включает себя сам):

    a2enmod rpaf

    Также следует немного подредактировать его настройки:

    nano /etc/apache2/mods-available/rpaf.conf
    <IfModule rpaf_module>
        RPAFenable On
        RPAFsethostname Off
        RPAFproxy_ips 127.0.0.1 ::1
        RPAFheader X-Real-IP
    </IfModule>
    Можно включить модуль сжатия данных deflate (если выключен) и настоятельно рекомендуется включить модуль rewrite (по умолчанию выключен):
    a2enmod deflate
    a2enmod rewrite

    Следует подкрутить также загрузчик модуля wsgi:

    nano /etc/apache2/mods-avaliable/wsgi.load

    Там будет написано нечто подобное:

    LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi_2.7.so

    Если используется Python2.6, следует изменить 7 на 6, иначе опять будем получать ошибку no module named django.core.handlers.wsgi. Добавим виртуальный хост Apache:

    nano /etc/apache2/sites-available/new_site.ru

    В этот файл записываем примерно следующее:

    <VirtualHost 127.0.0.1:8080>
      ServerName new_site.ru
      ServerAlias www.new_site.ru
      ServerAdmin mymail@domen.ru
    
      WSGIScriptAlias / /var/www/my_project/my_project.wsgi
      WSGIProcessGroup my_project
      WSGIDaemonProcess my_project user=xphoenix group=xphoenix threads=2 maximum-requests=1000
    
      DocumentRoot "/var/www/my_project"
      <Directory "/var/www/my_project">
        Order allow,deny
        Allow from all
      </Directory>
    
      FileETag none
    
      <Location "/">
        Order deny,allow
        Allow from all
      </Location>
    
      # Сжатие трафика
      <Location />
        SetOutputFilter DEFLATE
        BrowserMatch ^Mozilla/4 gzip-only-text/html
        BrowserMatch ^Mozilla/4\.0[678] no-gzip
        BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
        SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
        SetEnvIfNoCase Request_URI \.(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
        SetEnvIfNoCase Request_URI \.pdf$ no-gzip dont-vary
      </Location>
    </VirtualHost>

    Этот виртуальный хост будет обрабатывать запросы к сайтам new_site.ru и www.new_site.ru (что одно и то же), для запуска проекта будет использоваться файл /var/www/my_project/my_project.wsgi

    Для обработки запросов будет использоваться два потока (рекомендуется ставить по числу ядер), процесс будет работать в группе my_project, запускаться от имени пользователя xphoenix. Дальнейшие директивы, в принципе, понятны.

    Включаем сайт в apache:

    a2ensite new_site.ru

    Создадим вышеуказанный файл запуска приложения:

    nano /var/www/my_project/my_project.wsgi

    со следующим содержимым

    #!encoding=utf-8
    import os
    
    activate_this = '/var/www/.virtualenvs/new_env/bin/activate_this.py'
    execfile(activate_this, dict(__file__=activate_this))
    
    os.environ['DJANGO_SETTINGS_MODULE'] = 'project.settings'
    
    import django.core.handlers.wsgi
    application = django.core.handlers.wsgi.WSGIHandler()

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

    service apache2 restart

    Настройка Nginx

    Nginx будет проксировать запросы, приходящие на 80-й порт с любого интерфейса. Если это будет запрос статики, он сам его обработает, если нет, переадресует Apache. Создадим настройки для нашего сайта:

    nano /etc/nginx/conf.d/my_site.conf

    со следующим содержимым:

    server {
            listen *:80; #слушать порт 80 на всех интерфейсах
            server_name new_site.ru www.new_site.lo; #этот сервер будет обрабатывать эти сайты
            access_log off; #не хочу собирать общую статистику доступа
            error_log /var/www/my_project/log/nginx_error.log; #А вот ошибки - да
    
            location / {
                    proxy_pass http://127.0.0.1:8080/; #Переадресация запросов к Apache
                    proxy_set_header Host $http_host;  #Подкорректируем заголовки, чтобы отличать одного клиента от другого
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $remote_addr;
                    proxy_connect_timeout 120;
                    proxy_send_timeout 120;
                    proxy_read_timeout 180;
            }
    
            location ~* \.(jpg|jpeg|gif|png|ico|css|bmp|swf|js|html|txt|exe|pdf|djvu|doc|docx|xls|xlsx)$ {
                    root /var/www/my_project;
                    access_log /var/www/my_project/log/nginx_static.log;#Статистика по статике здесь
                    expires max; #Срок жизни кеша - максимальный
            }
    }

    Вот теперь можно перезапустить и nginx

    service nginx restart

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