Django, JSON и формы
Очень интересные результаты выдает Google при поиске по словам django json forms
. Большая часть ссылок ведет на Stack Overflow, но там всё одно и то же. Как правило, всё сводится к тому, чтобы: 1) сменить стек технологий; 2) разобрать request.POST
как словарь (это не работает); 3) передать данные каким-то другим способом.
А вот рабочее решение, проверенное в Django 1.4.22 (из-за некоторых особенностей мне сейчас приходится пользоваться именно таким старьем).
# -*- 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
тоже надо пару слов сказать. В старых версиях такого класса нет, но он отлично портируется из новых. Вот код:
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. Это довольно мощная таблица, однако, у нее есть ряд недостатков, которые решаются обходными путями, например, через редактирование стилей. Вот так можно задать автоматическое растягивание таблицы по размеру родительского блока:
.dgrid {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: auto;
}
Решение проблемы взято со StackOverflow.
Но есть другая проблема. Допустим, мы решили разместить DGrid
внутри компонента типа ContentPane
. При отображении заголовок будет сжат в 0 и наедет на данные. Можно изменить размер окна браузера, чтобы отображалось нормально, а можно:
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
:
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
перенесена в модуль 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 для его хранения.
<!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
Установка необходимых модулей 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/
.
<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. Допустим, у нас есть вот такой код:
# -*- 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 (регистр важен) со следующим содержимым:
(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
(директивы и фильтры пока не успел написать, поэтому и каталогов нет).
Так выглядит главный файл приложения:
(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
Ниже идёт код, который позволяет прописать в шаблон 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. В нём нужно будет создать каталоги для размещения статичных файлов, файлов, загружаемых пользователями, и логов.
cd /var/www
mkdir talos
cd talos/
mkdir static media log
Сам проект будет расположен в виртуальном окружении, которое будет создано чуть позже.
Создание файла запуска
Здесь же, в каталоге /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 ещё не установлен, пришло время это сделать:
apt-get install supervisor -y
Настройки хранятся в каталоге /etc/supervisor. Главный файл называется supervisor.conf, файлы для запуска приложений следует расположить в каталоге /etc/supervisor/conf.d/, указав расширение .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:
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:
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";
Возможно, данные настройки придётся доработать исходя из реальной конфигурации сервера. Здесь я на этом останавливаться не буду. Переходим к созданию файла конфигурации для нашего проекта:
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, тобы он подхватил новый конфигурационный файл. На этом всё.
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, но я написал так:
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
, не сложно.
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, следует публиковать хотя бы небольшие заметки. Начать следует с прописных истин.
- 1. Дуб - дерево.
- 2. Олень - животное.
- 3. Смерь - неизбежна.
- 4.
api
- отдельное приложение в нашем проекте - 5. Следует придерживаться общепринятых правил именования частей API
- 6. API должен быть версионным
- 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
в самом начале. Я сделал так:
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
Сначала хочу сказать, как делать НЕ НАДО:
http://example.org/api/
Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.
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
Разница, как говорится, налицо.
В Django REST Framework обновление записи может быть произведено двумя способами - полностью или частично. В первом случае вызывается запрос с заголовком PUT
, во втором - PATCH
.
При полном обновлении сериализатор проверяет заполнение всех полей модели, у которых не указаны свойства null=True
. Не будем забывать, что для модели пользователя обязательными для заполнения являются поля "Пароль" и "Имя пользователя". Но мы же всего лишь хотели указать новую дату рождения! Почему DRF проверяет все поля? Потому что надо было вызывать метод PATCH
.
Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).
/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 - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.
Итоги
- Разработчики хотят работать с сущностями, а не с 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.pyfrom 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()
, например, так:
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
можно не вполне очевидным способом:
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)
Введение
На данную тему в Интернете уже написано огромное множество статей, и моя станет лишь очередной попыткой описать то, что уже и так широко известно. Не претендуя на оригинальность, я попробую описать тот способ, которым пользуюсь сам. В статье пойдёт речь о расширении стандартной модели 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
является обязательным. Если отправить запрос на указание, например, нового отчества, будет возвращена ошибка, т.к. поле пароля должно быть обязательно заполнено. Дополнительная форма решает эту проблему.
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. Стало сразу две проблемы.
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 приходится всё допиливать под себя.
- Огромное количество пакетов. Я не знаю, есть ли где-то ещё больше. Если есть - напишите мне.
- Python 2.6 в официальном репозитории. Да, Debian 7.1 не требует возни с дохлыми змеями.
- Под него есть официальные сборки PostgreSQL и Nginx. Я раньше тоже думал, что MySQL наше всё, а сейчас вообще не понимаю, как можно пользоваться этой поделкой, не соблюдающей 90% мировых SQL-стандартов. Да, если вы пишете скрипты, руководствуясь SQL-92 или SQL-99, можете быть уверены, что в PostgreSQL они будут работать без каких-либо изменений, в то время как в MySQL их придётся переписывать, и весьма основательно (к слову, в разработке PostgreSQL принимают участие несколько профессоров математики и информатики, что как бы говорит о качестве получающегося продукта).
- Стабильность. Пакеты старые, но если они есть в репозитории, то можно не сомневаться, что они будут работать так, как то ожидается.
Немного про виртуальные окружения
Пакет 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
- опционально, если используется модуль psycopg2python-pip
- опционально, для установки пакетов Python, распространяемых в исходных кодах. Рекомендую в систему ставить пакет из репозиториев Debian, а в виртуальные окружения через easy_install pippython-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, значительно разгружая нагрузку на него. Буду рад любой критике в комментариях.