1. Введение в Django

Что такое среда разработки?

Шаблон MVC разработки

История Django

Как читать эту книгу

Необходимые знания в программировании

Необходимые знания языка Python

Требуемая версия Django

Получение помощи

Стиль исходного кода

Шаблоны

Представления

Модели

2. Начинаем

Установка Python

Версии Python

Установка

Установка Django

Установка официального релиза

Установка версии из транка

Проверка установки Django

Настройка базы данных

Использование Django с PostgreSQL

Использование Django с SQLite 3

Использование Django с MySQL

Использование Django без базы данных

Новый проект

Запуск сервера разработки

Отладка

Журналирование на консоль сервера разработки

Использование отладчика

Обработка ошибок в «боевом» режиме

Ещё о полезном ПО

Тестовый клиент

Быстрый старт

3. Представления и привязки URL

Ваша первая страница на Django: Привет, Мир

Ваше первое представление

Ваша первая привязка URL

Кратко об ошибках 404

Кратко о корне сайта

Как Django обрабатывает запрос

Ваше второе представление: Динамическое содержимое

Схема URL и свободное связывание

Ваше третье представление: Динамические URL

Красивые страницы об ошибке

4. Шаблоны

Основы шаблонной системы

Использование шаблонной системы

Создание шаблонных объектов

Обработка шаблона

Множество контекстов и единственный шаблон

Поиск контекстной переменной

Играем с контекстными объектами

Основные шаблонные теги и фильтры

Теги

Фильтры

Философия и ограничения

Использование шаблонов в представлениях

Загрузка шаблонов

render_to_response()

Трюк с locals()

Подкаталоги в get_template()

Шаблонный тег include

Наследование шаблонов

5. Модели

«Тупой» способ выполнения запросов в представлениях

Методика MTV (или MVC)

Настройка базы данных

Ваше первое приложение

Определение моделей в Python

Ваша первая модель

Установка модели

Доступ к данным

Добавляем строковое представление модели

Вставка и изменение данных

Выборка объектов

Фильтрация данных

Получение одного объекта

Сортировка данных

Формирование цепочки запросов

Выборка части результатов

Изменение множества объектов одним запросом

Удаление объектов

6. Интерфейс администратора Django

Пакеты django.contrib

Активация интерфейса администратора

Использование интерфейса администратора

Добавление ваших моделей на интерфейс администратора

Как работает интерфейс администратора

Делаем поля необязательными

Необязательные числовые поля и поля с датой

Настройка меток полей

Собственные классы ModelAdmin

Настройка списка редактирования

Настройка форм редактирования

Пользователи, группы и права

Когда и почему надо использовать интерфейс администратора, а когда не надо

Что дальше?

7. Формы

Получение данных из объекта запроса

Информация об URL

Остальная информация о запросе

Информация о переданных данных

Пример обработки простой формы

Усовершенствование примера обработки простой формы

Простая проверка данных

Создание формы для контактной информации

Ваш первый класс формы

Используем формы в представлениях

Изменения способа отображения полей

Установка максимального размера поля

Установка начальных значений

Собственные правила проверки

Определение меток

Настройка дизайна формы

Что дальше?

8. Усовершенствованные представления и схемы URL

Схема URL: полезные приёмы

Упорядочивание функций импорта

Использование множества префиксов для представлений

Специальные URL в режиме отладки

Использование именованных групп

Понимание алгоритма совпадения/группировки

Передача дополнительных аргументов в функции представления

Использование стандартных аргументов для представления

Представления для особых случаев

Обработка совпадающего текста

Определение того, с чем совпадёт шаблон URL

Высокий уровень абстракции в функциях представления

Wrapping View Functions

Подключение других схем URL

Как совпавшие параметры работают совместно с include()

Как работают дополнительные параметры совместно с include()

9. Базовые представления

Использование базовых представлений

Базовые представления объектов

Расширение базовых представлений

Создаём дружественные контексты для шаблонов

Добавление дополнительного контекста

Просмотр поднабора объектов

Сложная фильтрация с помощью функций-обработчиков

Выполнение дополнительной работы

10. Расширения для шаблонной системы

Обзор языка шаблонов

Процессоры RequestContext и Context

Процессор django.core.context_processors.auth

django.core.context_processors.debug

django.core.context_processors.i18n

django.core.context_processors.request

Как написать свой процессор?

Загрузка шаблонов

Расширение шаблонной системы

Создание шаблонной библиотеки

Разработка своих шаблонных фильтров

Разработка своих шаблонных тегов

Сокращения для простых тегов

Теги включения

Реализация своих шаблонных загрузчиков

Использование встроенного шаблонного справочника

Работа в автономном режиме

11. Генерация данных, отличных от HTML

Основы: представление и MIME-типы

Генерация CSV

Генерация PDF

Установка ReportLab

Создание представления

Сложные PDF документы

Другие возможности

Средства трансляции

Инициализация

Простая трансляция

Более сложная трансляция

Указание типа трансляции

Вложения

Язык

URL

Одновременная трансляция Atom и RSS

Среда генерации карты сайта

Установка

Активация

Классы

Полезное

Создание индекса для карты сайта

Уведомление Google

12. Сессии, пользователи и регистрация

Cookie

Получение и установка

Всеобщее счастье

Сессии

Активация

Использование в функциях представления

Установка тестовых cookie

Использование сессий вне функций представления

Когда сессии сохраняются

Время жизни сессий

Другие параметры сессии

Аутентификация пользователей

Активация

Использование объекта User

Авторизация и выход

Ограничение доступа авторизованным пользователям

Ограничение доступа определённым пользователям

Управление пользователями, правами и группами

Использование данных аутентификации в шаблонах

Остальное: права, группы, сообщения и профайлы

Права

Группы

Сообщения

Профайлы

13. Кэширование

Настройка кэша

Memcached

Кэширование в базу данных

Кэширование на файловую систему

Кэширование в оперативную память

Простое кэширование (для разработки)

Фиктивное кэширование (для разработки)

Аргументы CACHE_BACKEND

Кэширование отдельного сайта

Кэширование отдельного представления

Определение кэша для отдельного представления в файле со схемой URL

API низкого уровня

Кэши верхнего уровня

Использование заголовков Vary

Другие заголовки для механизма кэширования

Оптимизация

Порядок в MIDDLEWARE_CLASSES

14. Средства от других разработчиков

Стандартная библиотека Django

Среда управления комментариями

Сайты

Сценарий 1: Использование данных на множестве сайтов

Сценарий 2: Хранение информации о сайте в одном месте

Как это использовать

Возможности

CurrentSiteManager

Как Django использует среду управления сайтами

Статические страницы

Использование

Добавление, изменение и удаление

Использование шаблонов

Перенаправления

Использование

Добавление, изменение и удаление

Защита от CSRF

Простой пример

Более сложный пример

Предотвращение

Очеловечивание данных

apnumber

intcomma

intword

ordinal

Фильтры разметки

15. Компоненты

Что такое компоненты?

Установка

Методы

Конструктор

Препроцессор запроса

Препроцессор представления

Постпроцессор отклика

Постпроцессор исключения

Встроенные компоненты

Компонент поддержки аутентификации

Стандартный компонент

Компонент компрессии

Компонент условного GET

Компонент поддержки обратного прокси

Компонент поддержки сессий

Компонент для кэширования сайта

Компонент поддержки транзакций

Компонент X-View

16. Интеграция с унаследованным

Интеграция с унаследованной базой данных

Использование inspectdb

Очистка сгенерированных моделей

Интеграция с системой аутентификации

Описание источников аутентификации

Реализация источников аутентификации

Интеграция с унаследованными веб приложениями

17. Расширение интерфейса администратора

Дзен администрирования

Доверенные пользователи ...

... редактируют ...

... структурированную информацию

Full Stop

Настройка шаблонов интерфейса

Настройка шаблонов для отдельных моделей

Свой JavaScript

Создание своих представлений

Переопределение встроенных представлений

18. Интернационализация

Определение переводимых строк в коде

Стандартные функции для перевода

Пометка строк для позднего перевода

Отложенный перевод

Образование множественного числа

Определение переводимых строк в шаблоне

Создание файлов с переводами

Создание файлов с сообщениями

Компилирования файлов сообщений

Как Django получает языковые настройки

Представление set_language

Использование переводов в ваших проектах

Переводы и JavaScript

Представление javascript_catalog

Использование каталога с переводами

Создание каталога

Замечание для тех, кто уже работал с gettext

19. Безопасность

Тема безопасности в вебе

Внедрение SQL

Решение

Межсайтовый скриптинг (XSS)

Решение

Подделка HTTP запросов

Подделка сессий

Решение

Внедрение E-mail заголовка

Решение

Directory Traversal

Решение

Открытые сообщения об ошибках

Решение

Заключительное слово о безопасности

20. Развёртывание Django

Ничего общего

Что предпочитают разработчики Django?

Использование Django с Apache и mod_python

Базовые настройки

Несколько проектов Django на одном сервере Apache

Запуск тестового сервера под mod_python

Работа с Django и статическим контентом на одном сервере Apache

Обработка ошибок

Устранение ошибок, приводящих к падению Apache

Использование Django с Apache и mod_wsgi

Поддерживаемые версии Apache

Поддерживаемые версии Python

Установка модуля

Интерфейс к модулю

Конфигурация сервера

A. Учебные примеры

B. Справочник определений модели

C. Справочник по API взаимодействия с базой данных

D. Справочник по базовым представлениям

E. Параметры конфигурации

F. Встроенные шаблонные теги и фильтры

Справочник по стандартным тегам

autoescape

block

comment

csrf_token

cycle

debug

extends

filter

firstof

for

for ... empty

if

Фильтры

Составные выражения

ifchanged

ifequal

ifnotequal

include

load

now

regroup

spaceless

ssi

templatetag

url

widthratio

with

Справочник по стандартным фильтрам

add

addslashes

capfirst

center

cut

date

default

default_if_none

dictsort

dictsortreversed

divisibleby

escape

escapejs

filesizeformat

first

fix_ampersands

floatformat

force_escape

get_digit

iriencode

join

last

length

length_is

linebreaks

linebreaksbr

linenumbers

ljust

lower

make_list

phone2numeric

pluralize

pprint

random

removetags

rjust

safe

safeseq

slice

slugify

stringformat

striptags

time

timesince

timeuntil

title

truncatewords

truncatewords_html

unordered_list

upper

urlencode

urlize

urlizetrunc

wordcount

wordwrap

yesno

Дополнительные библиотеки тегов и фильтров

django.contrib.humanize

django.contrib.markup

django.contrib.webdesign

G. Утилита django-admin

H. Объекты HttpRequest и HttpResponse

DjangoBook

Глава 1. Введение в Django

Перевод © Попов Руслан <ruslan.popov • gmail>

Данная книга посвящена Django, среде[1] WEB[2] разработки, которая позволяет сэкономить время и превращает работу в развлечение. Используя Django, вы можете с минимальными усилиями создавать и обслуживать высококачественные приложения.

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

Цель данной книги — сделать вас экспертом по Django. Книга сфокусирована на двух вещах. Первое, мы подробно объясним, как работает Django и как создавать приложения с её помощью. Второе, там где это необходимо, мы обсудим высокоуровневую концепцию, отвечая на вопрос «Как я могу эффективно применять эти инструменты в своих проектах?» Читая эту книгу, вы получите навыки необходимые для разработки мощных приложений с простым поддерживаемым кодом.

Что такое среда разработки?

Django является видным представителем нового поколения сред разработки, но что же конкретно подразумевают под этим термином?

Для того, чтобы ответить на этот вопрос давайте рассмотрим дизайн приложения написанного на языке Python без использования среды разработки. На протяжении всей книги мы будем использовать данный подход, чтобы показать вам основные способы выполнить работу без вспомогательных средств, в надежде, что вы разберётесь почему эти средства так полезны. Также важно знать как можно решить задачу на низком уровне, так как высокий уровень (вспомогательные средства) не всегда доступен. И очень важно то, что знание почему что-то работает именно так, а не иначе, делает вас опытнее.

Одним из простых, наиболее прямолинейных способов создать с нуля вебприложение на языке Python является использование стандарта Common Gateway Interface (CGI), который был популярен, начиная с 1998 года. Кратко рассмотрим как это работает: создаётся скрипт на языке Python, который выводит HTML, скрипт сохраняется на веб сервере в файле с расширением .cgi, затем к скрипту можно обращаться через веб браузер. Вот и всё.

Ниже приведён простой CGI скрипт, написанный на языке Python, который отображает десять наиболее свежих книг из базы данных. Не беспокойтесь о деталях, просто рассмотрите как он работает:

#!/usr/bin/env python

import MySQLdb

print "Content-Type: text/html"
print
print "<html><head><title>Книги</title></head>"
print "<body>"
print "<h1>Книги</h1>"
print "<ul>"

connection = MySQLdb.connect(user='me', passwd='letmein', db='my_db')
cursor = connection.cursor()
cursor.execute("SELECT name FROM books ORDER BY pub_date DESC LIMIT 10")
for row in cursor.fetchall():
    print "<li>%s</li>" % row[0]

print "</ul>"
print "</body></html>"

connection.close()

Во-первых, согласно требованиям CGI, код выводит строку «Content-Type», затем пустую строку. Скрипт выводит необходимые HTML теги, подключается к базе данных и выполняет запрос для получения имён последних десяти книг. Обрабатывая результат запроса, скрипт создаёт ненумерованный список имён. Наконец, скрипт закрывает HTML теги и закрывает соединение с базой данных.

Для одиночной динамической страницы, подобной этой, методика «написать всё с нуля» не всегда плоха. Во-первых, этот код прост для понимания даже неопытным разработчиком и его легко установить на «боевую»[3] систему — просто сохранить в файл с расширением .cgi и зайди браузером.

Но несмотря на свою простоту, данный подход имеет ряд проблем и недостатков. Задайте себе эти вопросы:

Что произойдёт, если потребуется обеспечить работу множества компонентов вашего приложения с базой данных? Несомненно, что код для подключения к базе не должен быть скопирован в каждый отдельный CGI скрипт. Придётся вынести этот код в общую библиотеку функций.

Действительно ли разработчик должен беспокоиться о выводе строки «Content-Type» и помнить о необходимости закрывать соединение с базой данных? Такие вещи снижают производительность разработчика и увеличивают вероятность появления ошибок в коде. Такими задачами должна заниматься некая общая инфраструктура.

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

Что произойдёт, если дизайнер, который не имеет опыта разработки на языке Python, пожелает поменять вид страницы? Один неверный символ может уронить целое приложение. В идеале, при отображении страницы, логика страницы — получение названий книг из базы данных — должна быть отделена от вывода HTML. В этом случае дизайнер может вносить необходимые изменения в HTML без влияния на всё остальное.

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

[1] Здесь и далее слово Django будет рассматриваться как слово женского рода, от «среда».

[2] Здесь и далее речь будет идти о приложениях для Интернет, т.е., о сайтах.

[3] Здесь и далее этот термин подразумевает production (FIXME) WEB-сервер.

Шаблон MVC разработки

Давайте рассмотрим небольшой пример, который демонстрирует разницу между предыдущим подходом и тем, который предлагает среда разработки. Покажем как вы могли написать предыдущий CGI код, воспользовавшись возможностями Django. Мы разделим этот код на четыре файла:

Файл models.py описывает таблицу базы данных.

from django.db import models

class Book(models.Model):
    name = models.CharField(max_length=50)
    pub_date = models.DateField()

Файл views.py описывает логику приложения.

from django.shortcuts import render_to_response
from models import Book

def latest_books(request):
    book_list = Book.objects.order_by('-pub_date')[:10]
    return render_to_response('latest_books.html', {'book_list': book_list})

Файл urls.py описывает соответствие URL логике приложения.

from django.conf.urls.defaults import *
import views

urlpatterns = patterns('',
    (r'^latest/$', views.latest_books),
)

Файл latest_books.html описывает HTML шаблон, используемый при выводе страницы.

<html><head><title>Книги</title></head>
<body>
<h1>Книги</h1>
<ul>
{% for book in book_list %}
<li>{{ book.name }}</li>
{% endfor %}
</ul>
</body></html>

Не пытайтесь пока разобраться как это всё работает. Мы желаем лишь показать общую концепцию. Главной идеей в Django является разделение задач:

Файл models.py содержит описание таблицы базы данных, представленное в виде класса Python. Такой класс называется моделью. С помощью данного класса вы можете создавать, получать, обновлять и удалять записи в таблице вашей базы данных, используя простой код на языке Python вместо использования повторяющихся SQL команд.

Файл views.py содержит логику отображения страницы в функции latest_books(). Такая функция называется представлением.

Файл urls.py определяет какое именно представление будет вызвано для URL, заданного в виде шаблона. В данном случае URL /latest/ будет обработано функцией latest_books(). Другими словами, если имя вашего домена example.com, то любой доступ к http://example.com/latest/ будет обработан функцией latest_books().

Файл latest_books.html является HTML шаблоном, который описывает дизайн страницы. Он использует шаблонный язык с основными логическими операторами — {% for book in book_list %}.

Объединённые вместе, эти компоненты приложения следуют шаблону Модель-Представление-Контроллёр (Model-View-Controller, MVC). Примем, что MVC определяет способ разработки программного обеспечения при котором код для определения и доступа к данным (модель) отделён от логики приложения (управление), которая в свою очередь отделена от интерфейса пользователя (представление).

Основное преимущество такого подхода заключается в свободе объединения этих компонентов. Следовательно, каждая отдельная часть приложения, созданного с помощью Django, имеет одно назначение и может быть изменена независимо, т.е., без влияния на остальные компоненты. Например, разработчик может изменить URL для данной части приложения без изменения остального кода. Дизайнер может изменить HTML страницы без внесения изменений в код, который отображает страницу. Администратор базы данных может переименовать таблицу и определить эти изменения в одном месте, вместо того, чтобы искать и вносить изменения во множество файлов.

В данной книге каждый компонент MVC описан в своей собственной главе.

История Django

Прежде чем мы погрузимся в изучение кода, следует уделить немного времени истории развития Django. Мы отметили, что мы будем рассказывать вам как всё работает без вспомогательных средств, чтобы помочь вам разобраться с внутренней кухней процессов. Аналогично, будет полезно для понимания того, почему была создана среда Django, почему Django работает именно так, а не иначе.

Если у вас есть достаточный опыт разработки приложений, возможно вы сталкивались с проблемами, показанными на примере CGI, который мы продемонстрировали ранее. Классический процесс разработки приложения выглядит так:

Создать приложение с нуля.

Создать другое приложение с нуля.

Реализовать работу первого приложения со вторым.

Переработать код так, чтобы оба приложения использовали общие функции.

Повторить шаги 2-4 несколько раз.

Вы изобрели среду разработки.

Именно так и была создана Django!

Django органично росла во время разработки реальных приложений, созданных командой разработки в Лоуренсе, штат Канзас, США. Она родилась в конце 2003 когда программисты газеты «Lawrence Journal-World», Эдриан Холовати (Adrian Holovaty) и Симон Виллисон (Simon Willison), начали использовать язык Python для разработки своих приложений. Команда World Online, ответственная за разработку и поддержку нескольких локальных новостных сайтов, процветала в среде разработчиков, диктуемой срочностью работы журналиста. Для сайтов, включая LJWorld.com, Lawrence.com и KUsports.com, журналисты и менеджеры требовали, чтобы новые возможности и целые приложения были разработаны максимально быстро, часто счёт шёл на дни или на часы. Таким образом, Симон и Эдриан создали среду разработки экономящую время, исходя из своих потребностей — это был единственный способ, с помощью которого они могли создавать управляемые приложения в жёстких рамках сроков.

Замечание

Рекомендуем ознакомиться с:

Руководство по стилю кода на языке Python.

Туториал.

Летом 2005, развив эту среду до такого состояния, что она поддерживала большинство сайтов World Online, команда разработчиков, включая Якоба Каплан-Мосса (Jacob Kaplan-Moss), решила выпустить среду в виде программного обеспечения с открытым исходным кодом. Они выпустили его в июле 2005 под именем Django в честь джазового гитариста Джанго Рэйнхарда (Django Reinhardt).

Теперь, по прошествии нескольких лет, Django является грамотным проектом с открытым исходным кодом с десятками тысяч пользователей, над которым работают разработчики по всей планете. Два автора из команды World Online (взаимозаменяемые диктаторы Эдриан и Якоб) всё ещё осуществляют общее руководство ростом среды разработки, но приоритет сдвигается к управлению ростом командой разработки. World Online обеспечивает другие важные аспекты, такие как время разработчиков, маркетинговые материалы и хостинг/канал для сайта среды (http://www.djangoproject.com/).

Эта история уместна, потому что помогает объяснить два ключевых аспекта. Первый аспект — основное назначение Django. Так как Django родился в новостной среде, он предоставляет некоторые средства (такие как административный интерфейс, описанные в главе « Интерфейс администратора Django »), которые хорошо подойдут для контент-ориентированных сайтов, таких как Amazon.com, craigslist.org и washingtonpost.com, которые предоставляют динамическую информацию из базы данных. Существует разница между быть особенно эффективным в чём-то и быть неэффективным в другом.

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

Как читать эту книгу

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

Учитывая это, мы рекомендуем прочитать следующие семь глав по порядку. Они формируют основу использования Django, после их прочтения вы будете способны создавать сайты с помощью среды разработки. Остальные главы, которые сфокусированы на особенных возможностях Django, можно читать в любом порядке.

Приложения даны в качестве справочника. Они, вместе со свободной документацией на http://www.djangoproject.com/, являются тем, что вы будете периодически просматривать при разработке.

Необходимые знания в программировании

Читатели должны понимать основы процедурного и объекто-ориентированного программирования: управляющие структуры (if, while, for), структуры данных (списки, хэши/словари), переменные, классы и объекты.

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

Необходимые знания языка Python

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

Если у вас есть опыт разработки на языке Python, вы не должны будете испытывать проблемы. Более того, код Django не делает чего-то магического (т.е. с помощью хитрых приёмов программирования, которые трудно объяснить или понять). Для вас изучение Django будет означать изучение соглашений и API среды разработки.

Если у вас нет опыта разработки на языке Python, ничего страшного. Его легко изучить и прикольно использовать! Несмотря на то, что эта книга не включает в себя обучение языку Python, она подчёркивает возможности языка и его функциональность. Мы рекомендуем прочитать официальные документы по языку на http://docs.python.org/tut/. Мы также рекомендуем прочитать книгу Марка Пилгрима Вглубь языка Python, опубликованную издательством Apress и переведённую Денисом Откидач.

Требуемая версия Django

Данная книга описывает серию релизов 1.х, т.е. версии 1.0, 1.0.2 или 1.1.

Разработчики Django обеспечивают обратную совместимость с основными версиями. Это означает, что если вы пишете приложение для Django 1.0, оно будет работать с версиями 1.1, 1.2, 1.9 и со всеми другими версиями, которые начинаются с «1».

Когда Django достигнет версии 2.0, может потребоваться переписать ваше приложение — но до этого ещё далеко. Например, путь до версии 1.0 занял три года. (Это очень похоже на политику совместимости принятую для языка Python — код, написанный для Python 2.0, работает в 2.6, но это не гарантируется для Python 3.0).

Принимая описанное, данная книга охватывается все версии Django 1.x и должна прослужить вам приличное время.

Получение помощи

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

Список рассылки django-users является местом где тысячи пользователей задают и отвечают на вопросы. Зарегистрируйтесь свободно на http://www.djangoproject.com/r/django-users.

IRC канал является местом где пользователи могут общаться друг с другом, помогая в реальном времени. Присоединяйтесь к каналу #django в FreeNode IRC.

Стиль исходного кода

Перевод оригинальной статьи на http://www.djangoproject.com/documentation/contributing/#coding-style.

Пожалуйста, следуйте следующим правилам при разработке своих приложений:

Если особо не оговорено иное, надо использовать стандарт PEP 8.

Можно использовать утилиту аналогичную pep8.py для проверки исходного кода.

Для отступа следует использовать четыре пробела.

Пользуйтесь символами подчёркивания (_), а не выделениемРегистра при указании имён переменных, функций и методов (т.е., poll.get_unique_voters(), а не poll.getUniqueVoters()).

Используйте заглавные буквы в определении класса или функций, которые возвращают классы (например, InitialCaps).

Подготовьте все строки к переводу. За дополнительной информацией обратитесь к главе « Интернационализация ».

В строках документации используйте «активные слова», например:

def foo():
    """
    Calculates something and returns the result.
    """
    pass

а не:

def foo():
    """
    Calculate something and return the result.
    """
    pass

Не указывайте своё имя в коде. Политика Django предусматривает хранение имён авторов кода в файле AUTHORS.

Шаблоны

В шаблонах используйте только один пробел между фигурными скобками и содержимым тега.

Делайте так:

{{ foo }}

а не так:

{{foo}}

Представления

В представлениях первым параметром всегда должен быть request.

Делайте так:

def my_view(request, foo):
    # ...

а не так:

def my_view(req, foo):
    # ...

Модели

Имена полей должны определяться в нижнем регистре с использованием символа подчёркивания.

Делайте так:

class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

а не так:

class Person(models.Model):
    FirstName = models.CharField(max_length=20)
    Last_Name = models.CharField(max_length=40)

Класс Meta должен описываться после определения полей, отделённый от них пустой строкой.

Делайте так:

class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

    class Meta:
        verbose_name_plural = 'people'

а не так:

class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)
    class Meta:
        verbose_name_plural = 'people'

и не так:

class Person(models.Model):
    class Meta:
        verbose_name_plural = 'people'

    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

Порядок внутренних классов модели и стандартных методов должен быть таким (ничто из нижеперечисленного не является обязательным):

Определение всех полей класса.

class Meta.

class Admin.

def __unicode__().

def __str__().

def save().

def get_absolute_url().

Любые свои методы.

Если для модели определена опция choices, определите выбор в виде кортежа кортежей, использовав заглавные буквы для его имени. Определение лучше помещать в начале файла с моделями или перед классом. Пример:

GENDER_CHOICES = (
    ('M', 'Male'),
    ('F', 'Female'),
)

Глава 2. Начинаем

Перевод © Попов Руслан <ruslan.popov • gmail>

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

Из-за того, что Django является «просто» кодом на языке Python, она работает везде, где есть интерпретатор Python, даже на некоторых мобильных телефонах! Но данная глава описывает только общие сценарии установки Django. Мы предполагаем, что вы устанавливаете её на свой компьютер или на сервер.

Позднее, в главе « Развёртывание Django », мы расскажем как установить Django на боевой сервер.

Установка Python

Сама среда Django написана на чистом Python, следовательно, первым шагом при установке среды надо удостовериться, что интерпретатор языка Python установлен.

Версии Python

Ядро среды Django работает с любыми версиями Python в диапазоне от 2.3 до 2.7 включительно. Необязательный компонент GIS может работать с версиями от 2.4 до 2.7.

Если вы не уверены в том, какая именно версия интерпретатора языка Python следует установить и у вас есть полная свобода выбора, возьмите последнюю версию из ветки 2.х: т.е. версию 2.7. Несмотря на то, что Django работает одинаково хорошо с любой из вышеперечисленных версий, последняя версия интерпретатора Python обладает лучшей производительностью и дополнительными возможностями диалекта языка, которыми вы сможете воспользоваться при разработке своих приложений. Дополнительно, некоторые компоненты среды от сторонних разработчиков, которыми вы возможно захотите воспользоваться, могут потребовать более новую версию, чем Python 2.3, таким образом, использование последней версии интерпретатора даст вам больший выбор.

Django и Python 3.0

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

Если вы новичок в мире Python и раздумываете, что изучать — Python 2.x или Python 3.x, мы посоветуем остановиться на Python 2.x.

Установка

Если вы используете Linux или Mac OS X, вероятно у вас уже установлен Python. Наберите python в командной строке или в терминале (Application/Utilities/Terminal) на OS X). Если вы увидите нечто подобное, значит Python установлен:

Python 2.4.1 (#2, Mar 31 2005, 00:05:10)
[GCC 3.3 20030304 (Apple Computer, Inc. build 1666)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

В противном случае вам потребуется скачать и установить Python. Это легко и просто, а подробные инструкции доступны на http://www.python.org/download/.

Установка Django

В любое время вам доступны две различные версии Django: последний официальный релиз и версия из «транка». Версия, которую вы решите установить, зависит от ваших приоритетов. Вы желаете использовать стабильную и проверенную версию Django или вы желаете использовать версию, содержащую последние наработки, возможно вы будете совершенствовать саму Django, в ущерб стабильности?

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

Установка официального релиза

Официальные релизы имеют имена версий подобные 1.0.3 или 1.1, а последный релиз всегда доступен на http://www.djangoproject.com/download/. Для его установки, сначала надо скачать архив, который будет иметь имя подобное Django-1.0.2-final.tar.gz. Затем, надо развернуть архив и запустить setup.py install, как вы это делаете с большинством библиотек Python.

Ниже показано как этот процесс выглядит на Unix системах:

tar xzvf Django-1.0.2-final.tar.gz

cd Django-*

sudo python setup.py install

Если вы используете дистрибутив Linux, который содержит пакет с Django, будет хорошей идеей воспользоваться этим пакетом. Это позволит вам получать критические обновления совместно с остальными пакетами системы.

На Windows мы рекомендуем использовать 7-Zip (http://www.djangoproject.com/r/7zip/) для распаковки .tar.gz файлов. После распаковки запустите DOS оболочку («Command Prompt») с административными привелегиями и выполните следующую команду в каталоге, имя которого начинается с Django-:

python setup.py install

Установка версии из транка

Самая последняя версия Django называется стволом (trunk) и она доступна из Subversion репозитория Django. Вы должны рассмотреть возможность установки этой версии, если вам требуется работать с новыми возможностями или если вы желаете совершенствовать Django.

Subversion — это свободная система управления версиями с открытым исходным кодом, подобная CVS. Команда разработчиков Django использует её для управления базой разрабатываемого кода. Вы можете использовать клиент Subversion для получения свежего исходного кода Django и в любое время вы можете обновлять свою версию кода для получения свежих обновлений от других разработчиков.

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

Для получения кода со ствола делайте так:

Удостоверьтесь, что у вас установлен клиент Subversion. Вы можете свободно получить его на http://subversion.tigris.org/ и вы можете найти отличную документацию на http://svnbook.red-bean.com/.

Получите код с помощью команды:

svn co http://code.djangoproject.com/svn/django/trunk djtrunk

Создайте файл site-packages/django.pth и добавьте в него каталог djtrunk или обновите ваш PYTHONPATH, чтобы он указывал на djtrunk.

Поместите путь djtrunk/django/bin в ваш системный PATH. Этот каталог содержит утилиты управления, такие как django-admin.py.

Совет

Если вы не знакомы с файлами .pth, вы можете почитать о них на http://www.djangoproject.com/r/python/site-module/.

После получения кода и выполнения вышеприведённых шагов нет никакой необходимости выполнять команду:

python setup.py install

Все необходимые действия вы уже проделали вручную!

Из-за того, что ствол Django обновляется часто, для исправления ошибок и добавления новых возможностей, вероятно вы пожелаете обновлять его на регулярной основе. Для обновления исходного кода надо просто в каталоге djtrunk выполнить команду:

svn update

При выполнении этой команды, Subversion подключается к http://code.djangoproject.com, определяет наличие изменений кода Django и обновляет вашу локальную версию кода, скачивая только изменения. Это достаточно быстро.

Наконец, если вы используете код из ствола, вы должны знать как определить номер текущей версии кода, на которым вы работаете. Знание номера версия имеет ключевое значение, если вам понадобится помощь от сообщества пользователей Django или если вы соберётесь усовершенствовать среду. В этих случаях вам потребуется указать версию кода, так же известную как «номер ревизии», которую вы используете. Для того, чтобы узнать номер ревизии, наберите svn info каталоге с кодом Django и посмотрите на строчку с текстом «Revision:». Этот номер увеличивается при каждом изменении в коде Django, будь то исправлением ошибки, добавлением новых возможностей, изменением в документации или чем-то ещё. Некоторые пользователи Django измеряют свою крутость, говоря: «Я пользуюсь Django с версии [вставьте тут номер ревизии поменьше].».

Проверка установки Django

После установки Django потратьте немного времени для проверки её работоспособности. В терминале перейдите в другой каталог (т.е., не в тот, который содержит каталог django) и запустите интерпретатор Python, набрав python. Если всё установлено правильно, вы должны иметь возможность импортировать модуль django:

>>> import django
>>> django.VERSION
(1, 1, 0, 'alpha', 0)

Примеры интерактивного интерпретатора

Интерактивный интерпретатор языка Python является программой командной строки, которая позволяет вам выполнять программы. Для запуска интерпретатора просто выполните в командной строке команду python.

Во всей книге мы будем приводить код на языке Python как если бы он вводился в интерактивный интерпретатор. Вы сможете распознать эти примеры по трём знакам «больше» (>>>), которые являются приглашением интерпретатора. Если вы будете копировать примеры из книги, не копируйте эти знаки.

Многострочные команды в интерактивном интерпретаторе выравниваются с помощью трёх точек (...), вот так:

>>> print """This is a
... string that spans
... three lines."""
This is a
string that spans
three lines.
>>> def my_function(value):
...     print value
>>> my_function('hello')
hello

Эти три точки в начале дополнительных строк добавляются интерпретатором, они не являются частью нашего ввода. Мы не убираем их, чтобы сымитировать вывод интерпретатора. Если вы будете копировать наши примеры, не копируйте эти точки.

Настройка базы данных

На данный момент вы можете начать разработку веб приложения на Django, потому что единственное требование Django — работающий экземпляр интерпретатора Python. Тем не менее, эта книга сфокусирована на одной из возможностей среды разработки, а именно на разработке сайтов с поддержкой базы данных. Следовательно, вам потребуется установить сервер базы данных, который будет хранить ваши данные.

Если вы просто желаете посмотреть Django, пропустите всё до раздела « Новый проект ». Но имейте в виду, что все примеры в книге предполагают, что у вас есть база данных.

Django поддерживает четыре системы управления базами данных:

PostgreSQL (http://www.postgresql.org/).

SQLite 3 (http://www.sqlite.org/).

MySQL (http://www.mysql.com/).

Oracle (http://www.oracle.com/).

По большей части движки для перечисленных баз данных хорошо работают с ядром среды Django. Заметным исключением является необязательная поддержка GIS, которая лучше работает совместно с PostgreSQL, чем с другими базами данных. Если вы не привязаны к какой-либо унаследованной системе и у вас есть свобода выбора базы данных, мы рекомендуем PostgreSQL, который имеет отличный баланс между стоимостью, возможностями, скоростью и стабильностью.

Настройка базы данных происходит в два шага:

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

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

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

На Windows получение драйвера для работы с базой данных является не таким простым процессом. Таким образом, мы рекомендуем начинать работу с Django, используя Python версии 2.5 и его поддержку SQLite.

Использование Django с PostgreSQL

Если вы планируете использовать PostgreSQL, вам потребуется пакет psycopg или psycopg2, которые можно получить с http://www.djangoproject.com/r/python-pgsql/. Мы рекомендуем использовать psycopg2, так как она новее и более активно разрабатывается и может быть легко установлена. В любом случае следует запомнить, какую версию вы используете, 1-ую или 2-ую, информация об этом потребуется далее.

Если вы используете PostgreSQL на Windows, вы можете найти собранную версию пакета psycopg на http://www.djangoproject.com/r/python-pgsql/windows/.

Если вы используете Linux, проверьте через пакетную систему вашего дистрибутива наличие пакета с именем python-psycopg2, psycopg2-python, python-postgresql или аналогичным им.

Использование Django с SQLite 3

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

Если вы используете Python 2.4 или старее, вам потребуется SQLite версии 3 (не 2-ой) с http://www.djangoproject.com/r/sqlite/ и пакет pysqlite с http://www.djangoproject.com/r/python-sqlite/. Удостоверьтесь, что у вас установлен пакет pysqlite версии не ниже 2.0.3.

На Windows вы можете пропустить установку отдельных исполняемых файлов SQLite, так как они статически слинкованы с pysqlite.

Если вы используете Linux, проверьте через пакетную систему вашего дистрибутива наличие пакета с именем python-sqlite3, sqlite-python, pysqlite или аналогичным им.

Например, на Debian Linux, Ubuntu и т.д. этот пакет можно поставить так:

apt-get install python-pysqlite2

Использование Django с MySQL

Django работает с MySQL, начиная с версии 4.0. Версии MySQL ниже 4.0 не поддерживают вложенные запросы и некоторые стандартные SQL операторы.

Вам также потребуется установить пакет MySQLdb с http://www.djangoproject.com/r/python-mysql/.

Если вы используете Linux, проверьте через пакетную систему вашего дистрибутива наличие пакета с именем python-mysql, python-mysqldb, mysql-python или аналогичным им.

Использование Django без базы данных

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

Следует принять во внимание, что некоторые дополнительные инструменты поставляемые с Django требуют наличия базы данных. Соответственно, если вы отказываетесь от использования базы данных, вы также отказываетесь от использования этих инструментов. Мы отдельно выделим их в книге.

Новый проект

После того как вы установили Python, Django и (возможно) сервер баз данных и библиотеку Python для него, вы можете сделать первый шаг в разработке веб приложения, создав проект.

Проектом является набор настроек для экземпляра Django, включая конфигурацию базы данных, параметров для Django и настроек приложения.

Если вы используете Django в первый раз, вам потребуется позаботиться об инициализации проекта. Создайте новый каталог в котором вы будете работать, например, /home/username/djcode/.

Где должен располагаться этот каталог

Если вы раньше использовали PHP, вы вероятно размещали код в корневом каталоге веб сервера (например, в /var/www). При использовании Django так делать не следует. Это плохая идея размещать код в таком месте, потому что есть риск доступа сторонних лиц к вашему коду. Это плохо.

Разместите ваш код в каком-нибудь каталоге вне корневого каталога веб сервера.

Перейдите в созданный каталог и выполните команду django-admin.py startproject mysite. Она создаст каталог mysite в текущем каталоге.

Замечание

Скрипт django-admin.py должен быть в вашем системном пути, если вы устанавливали Django с помощью утилиты setup.py.

Если же вы используете код из ствола, то он должен быть в каталоге djtrunk/django/bin. Так как вы часто будете использовать скрипт django-admin.py, рассмотрите его добавление в системный путь. На Unix вы можете воспользоваться символической ссылкой, например:

sudo ln -s /path/to/django/bin/django-admin.py \
           /usr/local/bin/django-admin.py

На Windows вам потребуется внести изменение в переменную среды PATH.

Если вы установили Django из пакета вашего дистрибутива Linux, то django-admin.py может называться просто django-admin.

Если вы увидите сообщение «permission denied[4] » при запуске django-admin.py startproject, вам потребуется изменить права доступа к файлу. Чтобы сделать это перейдите в каталог где установлен файл django-admin.py (например, в /usr/local/bin) и выполните команду chmod +x django-admin.py.

Посмотрим на результаты работы команды:

mysite/
    __init__.py
    manage.py
    settings.py
    urls.py

Опишем назначение каждого файла:

__init__.py: Файл необходим для того, чтобы Python рассматривал данный каталог как пакет, т.е., как группу модулей. Это пустой файл и обычно вам не требуется добавлять что-либо в него.

manage.py: Это утилита командной строки, которая позволяет вам взаимодействовать с проектом различными методами. Наберите python manage.py help для получения информации о возможностях утилиты. Вы не должны изменять содержимое данного файла, он создан в данном каталоге в целях удобства.

settings.py: Настройки для текущего проекта Django. Посмотрите на содержимое файла, чтобы иметь представление о типах доступных параметров и их значениях по умолчанию.

urls.py: Описания URL для текущего проекта Django, так сказать «оглавление» для вашего сайта. На момент создания должен быть пустым (FIXME: по-моему авторы гонят.)

Несмотря на свой небольшой размер, эти файлы формируют работоспособное приложение Django.

Запуск сервера разработки

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

Сервер разработки Django (также называемый «runserver», по имени команды, которая его запускает) — это встроенный лёгкий веб сервер, который вы можете использовать в процессе разработки вашего сайта. Он включен в Django для того, чтобы вы могли быстро приступить к разработке вашего сайта без траты времени на конфигурирование вашего боевого веб сервера (т.е., Apache) раньше времени. Этот сервер разработки отслеживает изменения в вашем коде и автоматически перезагружает его, помогая видеть вносимые вами изменения без перезагрузки веб сервера.

Для запуска сервера перейдите в каталог mysite (cd mysite), если вы ещё не сделали этого, и выполните команду:

python manage.py runserver

Вы увидите нечто подобное этому:

Validating models...
0 errors found.

Django version 1.0, using settings 'mysite.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Команда запускает сервер локально на порту 8000. Сервер принимает только локальные соединения с вашего компьютера. Теперь он запущен, посетите страницу http://127.0.0.1:8000/ с помощью браузера. Вы увидите страницу «Welcome to Django[5]». Всё работает!

Ещё об одном обязательно надо сказать. Несмотря на то, что сервер разработки очень удобен во время разработки, сопротивляйтесь искушению использовать этот сервер в боевом режиме. Этот сервер может обрабатывать только один запрос в единицу времени и он не проходил никакого аудита на предмет безопасности. Когда придёт время поднять ваш сайт, обратитесь к главе « Развёртывание Django » для информации о том, как установить туда проект Django.

Как изменить адрес и порт на котором работает сервер

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

python manage.py runserver 8000

Указав IP адрес, вы разрешаете веб серверу обрабатывать внешние подключения. Это особенно полезно, если требуется разделить разрабатываемый сайт с другими членами команды. IP адрес 0.0.0.0 указывает серверу принимать подключения со всех имеющихся сетевых интерфейсов:

python manage.py runserver 0.0.0.0:8000

Когда вы сделаете это, другие компьютеры в вашей локальной сети смогут получить доступ к вашему сайту на Django по IP адресу его компьютера, например, http://192.168.1.103:8000/. Следует отметить, что вам потребуется посмотреть свои сетевые настройки для определения вашего IP адреса в локальной сети. Пользователи Unix могут воспользоваться утилитой ifconfig. Пользователи Windows — утилитой ipconfig.

После запуска сервера, посетите http://127.0.0.1:8000/ с помощью вашего веб браузера. Вы увидите страницу «Welcome to Django». Оно работает!

[4] Доступ запрещён.

[5] Добро пожаловать в Django.

Отладка

По материалам сайта http://simonwillison.net/2008/May/22/debugging/.

Стандартная страница ошибки Django — великолепна. Она предоставляет детальную трассировку, предоставляя локальные переменные, позволяет вам разворачивать строки кода вокруг строки с ошибкой, предоставляет текстовое описание исключения, удобное для отправки по почте коллегам и даже возможность отправить одной кнопкой детали ошибки на http://dpaste.com/, чтобы вы могли обсудить её на IRC канале. Страница предоставляет информацию о настройках приложения, о содержимом переменных запроса GET, POST и COOKIE и о всех значительных полях META собранных из среды HTTP сервера (отличный метод вспомнить, как правильно писать HTTP_REFERER).

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

assert False

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

assert False, request.GET

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

Журналирование на консоль сервера разработки

Если необходимо иметь информацию о том, что происходит при выполнении функции представления, самым быстрым способом будет поместить в представление оператор print. Сервер разработки выводит результат обработки оператора print напрямую на терминал. Это альтернатива со стороны сервера JavaScript функции alert().

Если требуется более сложный подход к журналированию, лучше использовать соответствующий модуль языка Python, который является частью стандартной библиотеки. Вы можете настроить его в файле настроек проекта — settings.py:

import logging
logging.basicConfig(
    level = logging.DEBUG,
    format = '%(asctime)s %(levelname)s %(message)s',
)

Затем можно осуществлять журналирование из любой функции представления:

def my_view(request):
    import logging
    logging.debug("A log message")
    ...

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

logging.basicConfig(
    level = logging.DEBUG,
    format = '%(asctime)s %(levelname)s %(message)s',
    filename = '/tmp/myapp.log',
    filemode = 'w'
)

После этого вы можете использовать tail -f /tmp/myapp.log для отслеживания отладочной информации в реальном времени. Этот способ может использоваться как во время отладки приложения, так и в боевом режиме.

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

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

import logging, traceback, pprint

def my_buggy_function(arg):
    ...
    if error_condition:
        stack = pprint.pformat(traceback.extract_stack())
        logging.debug('An error occurred: %s' % stack)

Кортеж, полученный в результате выполнения traceback.extract_stack(), включает в себя номера строк, имена функций и пути к файлам Python, т.е. вы можете реконструировать достаточный объём информации о работе своей программы.

Использование отладчика

Наиболее мощным оружием в процессе отладки является соответствующий отладчик — pdb. Он поставляется в стандартной библиотеке языка Python и является отладчиком командной строки. Существует множество способов активации отладчика, но наиболее прямым способом является добавление нижеприведённой строки в функцию представления:

import pdb; pdb.set_trace()

При посещении соответствующей страницы браузером, последний «подвиснет» — будет казаться, что страница загружается крайне медленно. На самом деле сервер разработки прекратит выполнение функции представления и передаст управление отладчику, вы получаете к нему доступ на консоли и можете взаимодействовать с кодом представления.

Я упомянул, что вы никогда, ещё раз, никогда не должны оставлять эту строчку при установке проекта на боевой сервер?

Таким образом, вы получили подвисший сервер разработки и приглашение отладчика на консоли. Что же со всем этим делать? Да всё, что угодно. Сначала следовало бы почитать документацию на отладчик, но далее приведены несколько полезных команд:

list — Отображает строки исходного кода вокруг текущей точки выполнения. Вы можете выполнять эту команду многократно для увеличения объёма отображаемого кода.

n — Выполняет следующую строку.

s — Аналогично предыдущей команде, но заходит в любую вызываемую функцию.

r — Выполняет текущую функцию до конца.

u — Переходит на один уровень вверх по стеку, т.е., можно перейти в родительскую функцию.

locals() — Это не команда отладчика, но очень полезна для получения текущей области видимости.

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

Но следует помнить, что всё это время браузер будет пытаться получить страницу. Если вы нажмёте c, то ваше приложение продолжит работу, запрос будет обработан и ваш браузер вздохнёт с облегчением.

К счастью, нет необходимости в «замораживании» сервера разработки во время работы с отладчиком. Отладчик отлично работает в интерактивной оболочке Django. Просто запустите проблемную функцию и затем делайте так:

>>> def function_that_raises_an_exception():
...   assert False
...
>>> function_that_raises_an_exception()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in function_that_raises_an_exception
AssertionError
>>> import pdb; pdb.pm()
> <stdin>(2)function_that_raises_an_exception()
(Pdb)

Метод pdb.pm() позволяет вам вернуться для отладки самого последнего исключения, даже если вы ещё не произвели импорт модуля отладчика на время вызова исключения.

И последний совет по использованию отладчика: вы можете использовать его для отладки скриптов командной строки, подобных ./manage.py. Запустите его так:

python -i manage.py buggy_command

Аргумент -i указывает интерпретатору, что надо перейти в интерактивный режим после запуска скрипта. Если скрипт вызывает исключение, вы можете использовать метод pdb.pm() для отладки.

Обработка ошибок в «боевом» режиме

Стандартным поведением Django в «боевом» режиме (т.е., когда параметр DEBUG установлен в False) является отправка на почту отчёта о вызванном исключении всем, кто перечислен в секции ADMINS. Также можно включить отправку отчётов на каждую ошибку 404 с помощью параметра SEND_BROKEN_LINK_EMAILS, отправка будет осуществлена всем, кто перечислен в секции MANAGERS. Больше эти настройки ничего не делают — это древний кусочек Django.

При работе сайта под высокой нагрузкой, вероятно, вам пригодится решение django-db-log от Дэвида Крамера (David Cramer), которое записывает исключения в таблицу базы данных. Оно по-хитрому использует MD5 хэш от трассировки для объединения множества сообщений об одной ошибке. Так же оно является отличным примером того, как использовать обработчик process_exception Django для своих целей.

Ещё о полезном ПО

ProfilerMiddleware — позволяет получить вывод модуля cProfile для конкретного URL, если добавить к нему ?prof.

DebugFooter — добавляет снизу страницы информацию о загруженных шаблонах и о выполненных SQL запросах.

Тестовый клиент

Завершим этот раздел по интерактивному исследованию вашего приложения советом изучить использование TestClient. Несмотря на то, что он был разработан для юнит-тестов Django, он подойдёт и для нашей цели. Этот инструмент позволяет имитировать запрос к вашему приложению из вашего кода. Пример:

>>> from django.test.client import Client
>>> c = Client()
>>> response = c.get("/") # The homepage
>>> response
<django.http.HttpResponse object at 0x2300470>
>>> print response
Vary: Cookie
Content-Type: text/html; charset=utf-8

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

В ответ вы получите объект HttpResponse, который можно интерактивно исследовать.

Существует ещё одна функция, которая может помочь в интерактивной отладке приложения — setup_test_environment(). Пример:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test.client import Client
>>> c = Client()
>>> response = c.get("/")
>>> response.template
[<django.template.Template object at 0x2723dd0>,
 <django.template.Template object at 0x2723f30>,
 <django.template.Template object at 0x273ee10>]
>>> response.context
[ list of Context objects ]

Это позволяет вам исследовать не только HTML, возвращённый функцией представления, но также шаблоны и контексты, которые были использованы для его рендеринга.

Быстрый старт

Каждый проект начинается с создания каталога:

django-admin.py startproject megaproject

Планируется, что проект должен работать с базой данных. Подразумевается, что база данных уже создана в соответствии с разделом « Настройка базы данных ». Для работы с базой данных необходимо создать в проекте приложение, причины этого описаны в разделе « Ваше первое приложение ». Переходим в каталог ./megaproject и приступаем к созданию:

python manage.py startapp gigaapp

Переходим в каталог ./gigaapp и принимаемся за наполнение файла models.py, в соответствии с разделом « Ваша первая модель ».

Глава 3. Представления и привязки URL

Перевод © Попов Руслан <ruslan.popov • gmail>

В предыдущей главе мы объяснили как настроить проект и запустить сервер разработки. В этой главе вы изучите основы создания динамических страниц с помощью Django.

Ваша первая страница на Django: Привет, Мир

В качестве вашего первого задания, давайте создадим страницу, которая будет выводить знаменитое сообщение: «Здравствуй, Мир».

Если бы вы создавали обычную страницу с текстом «Здравствуй, Мир» без использования Django, то достаточно было бы создать файл с таким текстом, дать ему имя hello.html и загрузить в каталог на веб сервере. Следует отметить, что в этом случае, вы определяете два ключевых параметра страницы: её содержимое (строка «Здравствуй, Мир») и её URL (http://www.example.com/hello.html/).

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

Ваше первое представление

Внутри каталога mysite, который был создан с помощью команды django-admin.py startproject в предыдущей главе, создайте пустой файл с именем views.py. Этот файл будет содержать представления, описываемые в данной главе. Следует отметить, что нет ничего особенного в выбранном нами имени файла. Django нет никакой разницы как он называется и вы убедитесь в этом сами. Но, назвать так файл — это хорошая идея, другим разработчикам будет проще ориентироваться в вашем коде.

Наше представление простое, оно состоит из кода функции и оператора импорта. Вот содержимое файла views.py:

# -*- coding: utf-8 -*-
from django.http import HttpResponse

def hello(request):
    return HttpResponse("Здравствуй, Мир")

Пройдёмся по коду, строчка за строчкой:

Сначала мы импортируем класс HttpResponse, который определён в модуле django.http. Нам необходимо импортировать этот класс, так как он используется в нашем коде.

Затем, мы определяем функцию hello — функцию представления.

Каждая функция представления принимает как минимум один параметр, который для удобства называют request. Это объект, который содержит информацию об обрабатываемом запросе, который привёл к вызову данной функции представления. Объект является экземпляром класса django.http.HttpRequest. В данном примере, мы ничего не делаем с этим объектом, но не смотря ни на что, он всегда должен быть первым параметром функции представления.

Следует отметить, что имя функции представления несёт только смысловую нагрузку для разработчика. Мы дали функции имя hello, так как такое имя явно определяет назначение функции, но мы могли бы её назвать и так hello_wonderful_beautiful_world и так далее. Следующий раздел, « Ваша первая привязка URL », проливает свет на то, как Django находит функцию.

Функция является однострочной. Она просто возвращает HttpResponse объект, который был создан с текстом «Здравствуй, мир».

Основная идея в том, что представление является функцией языка Python, которая принимает HttpRequest в качестве первого параметра и возвращает экземпляр HttpResponse. Для того, чтобы функция языка Python стала функцией представления Django, он должен соответствовать этим двум правилам. Есть исключения из этих правил, но мы рассмотрим их позже.

Ваша первая привязка URL

Если в этот момент вы бы запустили снова команду python manage.py runserver, то вы бы увидели сообщение «Welcome to Django» без единого следа нашего представления. Так происходит потому, что наш проект mysite ещё не знает о представлении hello. Нам потребуется явно указать Django, что мы активируем данное представление для конкретного URL. (Продолжая нашу аналогию с публикацией статичных HTML файлов, как будто бы мы создали HTML файл, но ещё не закачали его в каталог на сервере.) Для привязки функции представления к конкретному URL в Django используются файлы привязки URL.

Файл привязки URL можно рассматривать как таблицу с содержанием вашего сайта. Проще говоря, этот файл определяет соответствие между URL и функциями представления, которые должны быть вызваны для этих URL. Именно так вы указываете Django: «Для данного URL, вызывай этот код, а для этого URL вызывай вот этот код.» Например, «Если кто-нибудь посетит URL /foo/, вызывай функцию представления foo_view(), которая расположена в модуле views.py.»

Когда вы запустили django-admin.py startproject в предыдущей главе, скрипт автоматически создал для вас файл привязки: urls.py. По-умолчанию, он выглядит примерно так:

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Example:
    # (r'^mysite/', include('mysite.foo.urls')),

    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
    # to INSTALLED_APPS to enable admin documentation:
    # (r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # (r'^admin/', include(admin.site.urls)),
)

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

from django.conf.urls.defaults import *

urlpatterns = patterns('',
)

Давайте пройдём по каждой строчке этого кода:

Первая строка импортирует все объекты из модуля django.conf.urls.defaults, который содержит всю инфраструктуру для работы с файлами привязки. Там же находится функция patterns.

Вторая строка вызывает функцию patterns и сохраняет результат в переменную urlpatterns. Функция patterns принимает единственный аргумент — пустую строку. (Эта строка может быть использована для передачи общего префикса для функций представления, эта особенность будет описана в главе « Усовершенствованные представления и схемы URL ».)

Основным элементом данного кода является переменная urlpatterns, которую Django ожидает найти в вашем файле привязки. Эта переменная определяет соответствие между списком URL и кодом, который обрабатывает данные URL. По-умолчанию, как вы можете видеть, файл привязки пуст — ваше Django приложение ещё не настроено. (Именно так Django определяет, что следует показывать страницу «Welcome to Django». Если ваш файл привязки пуст, Django предполагает, что вы только что начали новый проект и, следовательно, отображает данное сообщение.)

Для того, чтобы добавить URL и представление в файл привязки, просто добавьте кортеж, объединяющий URL и функцию представления. Ниже показана привязка для нашего представления hello:

from django.conf.urls.defaults import *
from mysite.views import hello

urlpatterns = patterns('',
    ('^hello/$', hello),
)

(Следует отметить, что мы убрали закомментированные строчки для ясности. Вы можете оставить эти строки, если вам захочется.)

Мы сделали следующие изменения:

Сначала мы проимпортировали представление hello из его модуля — mysite/views.py, которое преобразовалось в mysite.views.

Затем, мы добавили строку ('^hello/$', hello) в urlpatterns. Такая строка известна как шаблон URL. Это обычный кортеж языка Python, в котором первый элемент является шаблоном регулярного выражения, а второй элемент — функция представления, которая должна использоваться при совпадении данного шаблона.

В двух словах, мы просто указали Django, что любой запрос к URL /hello/ должен быть обработан с помощью функции представления hello.

Ваш PYTHONPATH

Ваш PYTHONPATH является списком каталогов вашей системы, где интерпретатор Python производит поиск модулей в момент когда вы используете оператор import.

Например, допустим, что путь имеет следующее значение:

['', '/usr/lib/python2.4/site-packages', '/home/username/djcode/']

При выполнении кода:

from foo import bar

интерпретатор сначала проверит наличие файла foo.py в текущем каталоге. (Первым элементом списка идёт пустая строка, которая означает «текущий каталог».) Если такого файла там нет, то интерпретатор ищет файл в каталоге /usr/lib/python2.4/site-packages, затем в каталоге /home/username/djcode/. Наконец, если файл всё ещё не найден, интерпретатор возвращает ImportError.

Если вам интересно посмотреть на значения пути, запустите интерпретатор и выполните следующие команды:

>>> import sys
>>> print sys.path

В общем случае вам не требуется беспокоиться о настройке пути. Это задача Python и Django, они сами об этом позаботятся. (Настройка пути, это одна из задач, которыми занимается скрипт manage.py.)

Стоит обсудить синтаксис данного шаблона URL, так как он может быть не совсем очевиден. Несмотря на то, что шаблон должен совпадать с /hello/, он выглядит немного не так. Объясним почему:

Django удаляет слеш вначале каждого URL, перед его проверкой относительно шаблонов. Это означает, что наш шаблон не включает ведущего слеша в /hello/. (Сперва, это может показаться запутанным, но данное требование всё упрощает — например, включение одного файла привязок в другой, которое мы рассмотрим в главе « Усовершенствованные представления и схемы URL ».)

Шаблон включает в себя символы ^ и $. Они являются символами регулярного выражения, которые имеют особое значение: ^ означает «требовать, чтобы шаблон совпадал с начала строки», а $ означает «требовать, чтобы шаблон совпадал до конца строки».

Данный подход лучше объяснять на примере. Если бы мы использовали шаблон '^hello/' (без завершающего $), тогда любой URL, начинающийся с /hello/ совпадал бы с шаблоном. Например, такие как /hello/foo и hello/bar, а не только /hello/. Аналогично, если бы мы опустили начальный ^ (т.е., 'hello/$'), Django будет обрабатывать любой URL, который оканчивается на hello/. Например, такой как foo/bar/hello/. Если бы мы просто указали в качестве шаблона 'hello/', без символов ^ и $, тогда шаблону бы соответствовал любой URL, содержащий hello/. Например, такой как /foo/hello/bar. Следовательно, мы используем оба этих специальных символа для точного определения нашего шаблона. Он должен совпадать только с /hello/, не больше и не меньше.

Большинство ваших шаблонов для URL будут соответствовать вышеописанному правилу. Но неплохо было бы иметь больше гибкости для выполнения более изощрённых проверок.

Вы можете удивиться тому, что произойдёт, если кто-нибудь запросит страницу по URL /hello (то есть, без завершающего слеша). Так как наш шаблон требует наличие завершающего слеша, этот URL не будет распознан. Тем не менее, по умолчанию, любой запрос к URL, который не совпадает с шаблоном и не завершается слешом, перенаправляется на тот же URL с добавленным завершающем слешом. (Это поведение регулируется параметром конфигурации Django APPEND_SLASH, который описан в приложении D FIXME.)

Если вы относитесь к той группе людей, которые предпочитают URL с завершающими слешами (таких большинство среди разработчиков Django), всё, что вам потребуется сделать — добавить завершающий слеш в каждый шаблон URL и установить значение переменной APPEND_SLASH в True. Если вы предпочитаете иной вариант (по той или иной причине), установите APPEND_SLASH в False.

Следует отметить ещё один важный момент. В шаблоне URL мы передаём функцию представления hello в виде объекта, без его вызова. Это возможно благодаря ключевой особенности языка Python (и любого другого динамического языка): функции являются объектами первого рода, это означает, что вы можете передавать их в другие объекты аналогично переменным. Прикольно, не так ли?

Для того, чтобы протестировать наши изменения внесённые в файл привязок, запустите тестовый сервер, как это было показано в главе « Начинаем », выполнив команду python manage.py runserver. (Если вы не останавливали его работу, это тоже неплохо. Тестовый сервер автоматически обнаруживает изменения в вашем коде и выполняет его перезагрузку по необходимости, таким образом, вам не надо делать это вручную.) Сервер привязывается к адресу http://127.0.0.1:8000/, так что откройте браузер и пройдите по ссылке http://127.0.0.1:8000/hello/. Вы должны увидеть текст «Здравствуй, Мир» — результат работы вашего представления.

Ура! Вы создали вашу первую страницу с помощью Django.

Регулярные выражения

Регулярные выражения[6] — компактный метод определения шаблонов в тексте. В то время как схема URL Django позволяет использование определённых регулярных выражений для работы с URL, вероятно на практике вы будете использовать не так много регулярных выражений для шаблонов. В таблице « Выборка наиболее используемых шаблонов » приведена выборка наиболее используемых шаблонов.

Таблица 3.1. Выборка наиболее используемых шаблонов

Символ | Совпадает
. (точка) | Любой символ
\d | Любая цифра
[A-Z] | Любая буква (верхний регистр)
[a-z] | Любая буква (нижний регистр)
[A-Za-z] | Любая буква (любой регистр)
+ | Один или более символов предыдущего выражения, т.е. \d+ совпадает с одной или более цифрами
[^/]+ | Все символы подряд, кроме слэша
? | Наличие или отсутствие предыдущего выражения, т.е. \d? описывает возможное наличие одной цифры
{1, 3} | От одного до трёх (символов) предыдущего выражения

Кратко об ошибках 404

В текущий момент наш файл привязок определяет единственный шаблон: тот, который обрабатывает запросы по URL URL. Что произойдёт при запросе другого URL?

Чтобы узнать это запустите тестовый сервер и посетите страницу такую как http://127.0.0.1:8000/goodbye/ или http://127.0.0.1:8000/hello/subdirectory/, или даже такую http://127.0.0.1:8000/ (корень сайта). Вы должны увидеть сообщение «Страница не найдена» (см. картинку FIXME). Django отображает это сообщение, потому что вы запросили URL, который не был определён в файле привязок.

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

Обычно, эта информация предназначена только для вас, для разработчика. В случае боевого сайта, выставленного в Интернет, врядли вы пожелаете показывать такую информацию неизвестным людям. По этой причине страница с подробностями об ошибке 404 отображается только в том случае, когда ваш проект находится в режиме отладки. Мы расскажем как деактивировать режим отладки позже. Сейчас просто запомните, что новый проект Django всегда создаётся со включенным режимом отладки, и что когда проект не находится в этом режиме, Django показывает совсем другое сообщение об ошибке 404.

Кратко о корне сайта

Как было описано в последнем разделе, вы увидите сообщение об ошибке 404 в случае, если вы перейдёте в корень сайта — по URL http://127.0.0.1:8000/. Django ничего не добавляет к корню сайта. Данный URL никоим образом не является чем-то особенным. Назначить корню сайта соответствующий шаблон — это целиком ваша задача, как и всё остальное в вашем файле привязки.

Шаблон для корня сайта не совсем очевиден, наверное стоит это отметить. Когда вы будете готовы реализовать представление для корня сайта, используйте шаблон '^$', который соответствует пустой строке. Например:

from mysite.views import hello, my_homepage_view

urlpatterns = patterns('',
    ('^$', my_homepage_view),
    # ...
)

[6] Для информации по регулярным выражениям обращайтесь на http://www.djangoproject.com/r/python/re-module/ и на http://docs.python.org/lib/re-syntax.html.

Как Django обрабатывает запрос

Перед тем как приняться за вторую функцию представления, давайте изучим немного подробнее то, как работает Django. Конкретно, что делает Django в тот момент, когда вы запрашиваете страницу через http://127.0.0.1:8000/hello/?

Всё начинается с файла конфигурации. Когда вы запускаете python manage.py runserver, скрипт ищет файл с именем settings.py в том же каталоге, в котором находится файл manage.py. Данный файл содержит всю информацию о конфигурации конкретного проекта Django, все параметры указаны большими буквами: TEMPLATE_DIRS, DATABASE_NAME и так далее. Самый главный параметр называется ROOT_URLCONF. Этот параметр указывает Django какой модуль Python следует использовать в качестве файла привязки для данного сайта.

Запомните, что django-admin.py startproject создаёт файлы settings.py и urls.py. Параметр ROOT_URLCONF созданного файла настроек указывает на созданный файл привязок. Откройте settings.py и удостоверьтесь сами. Там должны быть строчка:

ROOT_URLCONF = 'mysite.urls'

Она соответствует файлу mysite/urls.py.

Когда приходит запрос на определённый URL — скажем, на /hello/ — Django загружает файл привязок, указанный параметром ROOT_URLCONF. Затем проверяет каждый шаблон этого файла, по-порядку, сравнивая запрошенный URL с шаблонами, пока не найдёт подходящий. Если совпадение найдено, Django вызывает функцию представления, ассоциированную с данным шаблоном, передавая ей объект HttpRequest в качестве первого аргумента. (Мы рассмотрим особенности HttpRequest позже.)

Как мы видели в нашем первом примере представления, функция представления должна возвращать HttpResponse. После того как она это сделала, Django делает всё остальное: преобразовывает объект Python в соответствующий отклик с необходимыми HTTP заголовками и телом (т.е., содержимым страницы).

В общем:

Приходит запрос к /hello/.

Django определяет корневой файл привязки, с помощью параметра конфигурации ROOT_URLCONF.

Django просматривает файл привязки в поисках первого шаблона, который совпадёт с запрошенным URL.

Если такой шаблон найден, Django вызывает ассоциированную с ним функцию представления.

Функция представления возвращает HttpResponse.

Django преобразовывает HttpResponse в соответствующий HTTP отклик, который реализует страницу.

Теперь вы знаете основы создания страниц с помощью Django. Это несложно — просто напишите функции представления и ассоциируйте их с шаблонами через файл привязки.

Ваше второе представление: Динамическое содержимое

Наше представление «Здравствуй, Мир» было поучительным с точки зрения основ использования Django, но оно не было примером динамического создания страниц, так как содержимое страницы не изменялось. Каждый раз, посещая эту страницу, вы видели одно и тоже. То же самое достигается и с помощью статического HTML файла.

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

Данное представление должно делать две вещи: вычислять текущую дату и время и возвращать HttpResponse, содержащий это значение. Если у вас был опыт работы с Python, вы знаете, что он предоставляет модуль datetime для работы с датой и временем. Покажем как его можно использовать:

>>> import datetime
>>> now = datetime.datetime.now()
>>> now
datetime.datetime(2008, 12, 13, 14, 9, 39, 2731)
>>> print now
2008-12-13 14:09:39.002731

Это достаточно просто и не требует наличия Django. Это просто код на языке Python. (Мы хотели бы подчеркнуть, что вы должны знать различия между «просто Python» и кодом, который относится к Django. После того, как вы изучите Django, мы хотим, чтобы вы могли применить свои знания в других Python проектах, которые не обязательно бы использовали Django.)

Для того, чтобы создать представление, которое отображает текущую дату и время, необходимо добавить вызов datetime.datetime.now() в представление и возвратить HttpResponse. Вот так это выглядит:

from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

Аналогично функции представления hello, данная функция должна располагаться в views.py. Следует отметить, что мы спрятали функцию hello из этого примера для краткости, но весь файл будет выглядеть так:

from django.http import HttpResponse
import datetime

def hello(request):
    return HttpResponse("Hello world")

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

(Начиная с этого места, мы больше не будем показывать предыдущий код в примерах, кроме моментов, когда это действительно необходимо. У вас будет возможность понять из контекста, какая часть кода новая, а какая — старая.)

Пройдёмся по изменениям, которые мы сделали в views.py при создании представления current_datetime:

Мы добавили import datetime в начало модуля, теперь мы можем работать с датами.

Новая функция current_datetime возвращает текущую дату и время, в виде объекта datetime.datetime, и сохраняет его в локальной переменной now.

Вторая строка кода внутри представления создаёт HTML отклик, используя возможности Python для форматирования строк. Элемент %s внутри строки является подстановкой, а знак процента после строки означает «Замени %s в предыдущей строке на значение переменной now.» Переменная now технически является объектом datetime.datetime, не строкой, но подстановка %s преобразовывает его в строковое представление, подобное "2008-12-13 14:09:39.002731". Всё это выражается в виде HTML строки "<html><body>It is now 2008-12-13 14:09:39.002731.</body></html>".

(Да, наш HTML некорректен, но мы стараемся сделать наш пример простым и коротким.)

Наконец, представление возвращает объект HttpResponse, который содержит сгенерированный отклик — точно так же, как и в случае с hello.

После добавления новой функции в views.py, следует добавить шаблон в urls.py, чтобы указать Django какой именно URL должен обрабатываться с помощью этой функции представления. Подойдёт что-нибудь подобное /time/:

from django.conf.urls.defaults import *
from mysite.views import hello, current_datetime

urlpatterns = patterns('',
    ('^hello/$', hello),
    ('^time/$', current_datetime),
)

Здесь мы сделали два изменения. Сначала мы проимпортировли функцию current_datetime в начале модуля. Затем, и это самое главное, мы добавили шаблон URL /time/, ассоциированный с этим новым представлением.

После всех этих действий можно запустить runserver и посетите http://127.0.0.1:8000/time/ с помощью браузера. Вы должны увидеть текущую дату и время.

Временная зона в Django

Время на вашем компьютере может отличаться на несколько часов. Всё потому, что Django обращает внимание на временные зоны, а по умолчанию Django настроен на зону America/Chicago. (Она должна быть установлена по-умолчанию, вот разработчики Django и выбрали в качестве таковой свою временную зону.) Если вы живёте не рядом с ними, укажите свою зону в settings.py. Обратите внимание на комментарий в этом файле, там указана ссылка с актуальным списком мировых временных зон.

Схема URL и свободное связывание

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

Схема URL Django является хорошим примером применения такого принципа на практике. В Django-приложении определения URL и функций представления, которые они вызывают связаны свободно. Следовательно, решение о том, какой URL должен соответствовать данной функции и реализация самой функции располагаются в двух разных местах. Это позволяет вам отключать один компонент приложения без влияния на остальные.

Для примера рассмотрим наше представление current_datetime. Если нам потребуется изменить URL в приложении, скажем, переименовать его из /time/ в /current-time/, нам достаточно будет сделать быстрое изменение в файле привязок, не беспокоясь о самом представлении. Аналогично, если нам потребуется изменить функцию представления — как-то изменить логику её работы — мы можем сделать это без внесения изменений в URL, к которому привязана данная функция.

Продолжая, если нам потребуется разделить функционал данного представления между несколькими URL, мы сможем легко реализовать это, отредактировав схему URL, не внося изменений в код представления. В данном примере, наше представление current_datetime доступно по двум URL:

urlpatterns = patterns('',
    ('^hello/$', hello),
    ('^time/$', current_datetime),
    ('^another-time-page/$', current_datetime),
)

Схема URL и представления являются примером свободного связывания в действии. Мы продолжим рассматривать на примерах этот важный подход в течение всей книги.

Ваше третье представление: Динамические URL

В нашем представлении current_datetime содержимое страницы — текущая дата и время — были динамическими, но URL оставался статическим. В большинстве динамических web-приложений URL содержат параметры, которые влияют на генерацию страницы. Например, онлайн магазин может отображать книгу по её собственному URL, таким как /books/243/ и /books/81196/.

Давайте создадим третье представление, которое отображает текущую дату и время со смещением на указанное количество часов. Цель — реализовать функциональность, с помощью которой сайт будет по URL /time/plus/1/ отдавать текущее время сдвинутое на час вперёд, а по URL /time/plus/2/ — на два часа вперёд и так далее.

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

urlpatterns = patterns('',
    ('^time/$', current_datetime),
    ('^time/plus/1/$', one_hour_ahead),
    ('^time/plus/2/$', two_hours_ahead),
    ('^time/plus/3/$', three_hours_ahead),
    ('^time/plus/4/$', four_hours_ahead),
)

Очевидно, это направление мысли является ущербным. Такой подход не только приведёт к избыточности функций представления, но и к тому, что приложение будет явно ограничено заранее заданным диапазоном смещений — один, два, три или четыре часа. Если мы решим создать страницу, которая отображает текущее время со смещением на пять часов вперёд, нам потребуется создать отдельное представление и внести дополнительную строчку в схему URL, продолжая создавать избыточность. Нам потребуется немного окунуться в теорию.

Слово о красивых URL

Если вы работали с другими платформами разработки web-приложения, такими как PHP или Java, вы можете подумать: «Эй, давайте использовать параметр строки запроса!» — что-нибудь аналогичное /time/plus?hours=3, в котором смещение будет указано через параметр hours в URL.

Вы можете сделать так и с Django (и мы расскажем об этом в главе 7 FIXME), но Django пропагандирует философию использования красивых URL. URL /time/plus/3/ гораздо яснее, проще, более читаемое, его проще сказать кому-нибудь вслух и ... просто проще, чем его оппонент. Красивые URL являются характеристикой качества web-приложения.

Система схемы URL Django поощряет создание красивых URL, упрощая использование таких URL, в отличие от других.

Как же мы должны спроектировать наше приложение для обработки соответствующих смещений времени? Ответ лежит в использовании шаблонов подстановки. Как мы упоминали ранее, шаблон URL является регулярным выражением. Следовательно, мы можем использовать шаблон \d+ для выделения одной или более цифр:

urlpatterns = patterns('',
    # ...
    (r'^time/plus/\d+/$', hours_ahead),
    # ...
)

(Мы используем #..., чтобы показать, что здесь могут быть другие шаблоны, которые мы временно исключили из данного примера.)

Новый шаблон схемы URL будет соответствовать любому URL, подобному /time/plus/2/, /time/plus/25/ и даже /time/plus/100000000000/. Давайте ограничим смещение 99-ю часами. Это означает, что мы разрешаем только одно- или двухзначиные числа. На языке регулярных выражений это преобразовывается в \d{1,2}:

(r'^time/plus/\d{1,2}/$', hours_ahead),

Замечание

При разработке web-приложения, важно всегда рассматривать наиболее диковинные варианты ввода данных и решать должно или нет приложение обрабатывать такой ввод. Мы ограничили здесь возможности ввода смещения 99-ю часами.

Ещё одной важной деталью, которую мы ввели здесь, является символ r в начале строки регулярного выражения. Он указывает Python, что строка является «сырой» — в её содержимом не следует интерпретировать обратные слеши. В обычной строке Python, обратные слеши используются для экранирования особых символов, например, \n — односимвольная строка. Если предварить её символом r, сделав её «сырой», Python не будет выполнять экранирование — таким образом, r'\n' станет двухсимвольной строкой, содержащей обратный слеш и символ n. Существует естественная коллизия между обычными обратными слешами и используемыми в регулярных выражениях. Крайне рекомендуется всегда использовать сырые строки при определении регулярных выражений в Python. С этого момента следует считать, что все шаблоны URL, показанные в этой книге, будут представлены сырыми строками.

После применения символов подстановки в URL необходим способ передачи выделенных подстановкой данных в функцию представления, это позволит нам использовать единственную функцию представления для любого разрешённого смещения часов. Мы осуществляем это с помощью установки скобок вокруг интересующей нас части шаблона URL. В рамках нашего примера, это будет любое число в URL, таким образом, надо расположить скобки вокруг выражения \d{1,2}, например:

(r'^time/plus/(\d{1,2})/$', hours_ahead),

Если у вас уже есть опыт работы с регулярными выражениями, то вам будет легко. Мы используем скобки для выделения данных из совпавшего текста.

В итоге схема URL будет выглядеть так:

from django.conf.urls.defaults import *
from mysite.views import hello, current_datetime, hours_ahead

urlpatterns = patterns('',
    (r'^hello/$', hello),
    (r'^time/$', current_datetime),
    (r'^time/plus/(\d{1,2})/$', hours_ahead),
)

Теперь напишем представление hours_ahead.

Порядок разработки

В данном примере мы сначала создали шаблон URL, а затем представление. Но в предыдущих примерах, мы сначала создавали представление, а затем — шаблон URL. Какой подход лучше?

Хорошо, каждый разработчик уникален.

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

Если вам комфортно разрабатывать проект, начиная с небольших компонентов, может быть вы предпочтёте сначала реализовать представления, а затем ассоциировать их с URL. Такой подход тоже имеет право на жизнь.

В конце концов, не важно какой из подходов лучше или хуже. Они оба правильные.

Представление hours_ahead очень похоже на представление current_datetime, которое мы написали ранее, с одним только отличием: оно принимает дополнительный аргумент — количество часов смещения. Вот его код:

from django.http import Http404, HttpResponse
import datetime

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

Рассмотрим каждую строчку этого кода:

Функция представления, hours_ahead, принимает два параметра: request и offset.

Параметр request является объектом HttpRequest, аналогичным используемому в представлениях hello и current_datetime. Мы повторим ещё раз: каждое представление всегда принимает объект HttpRequest в качестве первого аргумента.

Параметр offset является строкой, выделенной скобками в шаблоне URL. Например, если запрошенный URL был /time/plus/3/, тогда offset будет содержать строку '3'. Если /time/plus/21/ — строка '21'. Следует отметить, что выделенные значения всегда будут строками, не целыми, даже если строка составлена только из цифр.

(Технически, захваченные значения всегда будут объектами Unicode, а не простыми строками Python, но пока об этом беспокоиться не следует.)

Мы решили назвать данный параметр offset, но вы можете дать ему любое другое имя, главное, чтобы оно соответствовало требованиям языка Python для идентификаторов. Имя переменной само по себе ничего не означает. Имеет значение только то, что переменная является вторым аргументом функции, после request. (Также можно использовать ключевые параметры в схеме URL вместо позиционных. Мы расскажем об этом в главе 8 FIXME.)

Первое, что мы делаем в функции — вызываем int() для offset. Данная функция преобразовывает строковое значение в целое.

Следует отметить, что Python вызывет исключение ValueError, если в процессе вызова int() данная функция не сможет осуществить конвертацию, например, для строки 'foo'. В данном примере, если мы получим ValueError, мы вызовем исключение django.http.Http404, которое, как вы можете представить, возвратит страницу с ошибкой 404 — «Страница не найдена»

Проницательному читателю будет интересно: как мы сможем попасть в ветку с ValueError, учитывая то, что регулярное выражение в нашем шаблоне — (\d{1,2}) — принимает только цифры и, следовательно, offset всегда будет состоять из строки цифр? Ответ заключается в том, что мы и не рассчитываем на это, так как шаблон URL предоставляет простой, но достаточный уровень проверки, но мы попрежнему проверяем значение на случай, если данная функция представления будет вызвана каким-либо другим способом. Это хороший подход — реализация функций представления так, как будто им в параметрах может передаваться всё что угодно. Помните про свободное связывание?

На следующей строке функции мы вычисляем текущую дату и время, а затем добавляем соответствующее число к часам. Мы уже встречались с datetime.datetime.now() в представлении current_datetime. Разница лишь в том, что здесь мы выполняем арифметические операции, создав объект datetime.timedelta и прибавив его к объекту datetime.datetime. Результат операции сохраняется в переменной dt.

Данная строка также показывает почему мы вызываем int() для offset — функция datetime.timedelta требует, чтобы параметр hours содержал целое число.

Затем мы создаём HTML документ для данного представления, аналогично тому, как это делалось в представлении current_datetime. Есть небольшое отличие в этой строке, от предыдущей — здесь используется строка форматирования с двумя подстановками, а не одной. Следовательно, строка содержит две подстановки %s и кортеж, содержащий значения: (offset, dt)..

Наконец, мы возвращаем HttpResponse. Вот и всё.

Реализовав данную функцию представления и привязав её к URL, запустите тестовый сервер Django (если он ещё не запущен) и посетите http://127.0.0.1:8000/time/plus/3/ для проверки работы функции. Затем попробуйте http://127.0.0.1:8000/time/plus/5/. Затем http://127.0.0.1:8000/time/plus/24/. Наконец, посетите http://127.0.0.1:8000/time/plus/100/, чтобы проверить, что шаблон URL принимает только одно- или двухсимвольные числа. Django должна отобразить «Страница не найдена» в этом случае. http://127.0.0.1:8000/time/plus/ (без указания часов) также должен вызвать ошибку 404.

Красивые страницы об ошибке

Восхититесь созданным вами web-приложением, а теперь давайте сломаем его. Умышленно внесём ошибку в код представления hours_ahead, закомментировав строки offset = int(offset):

def hours_ahead(request, offset):
    # try:
    #     offset = int(offset)
    # except ValueError:
    #     raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

Запустите тестовый сервер и перейдите по ссылке http://127.0.0.1:8000/time/plus/3/. Вы увидите страницу с ошибкой, которая будет содержать значительный объём информации, включая TypeError сообщение на самом верху:

"unsupported type for timedelta hours component: unicode"

Что же случилось? Как мы говорили функция datetime.timedelta ожидает, что параметр hours является целым числом, а мы закомментировали кусок кода, который выполняет это преобразование для offset. Это привело к вызову TypeError функцией datetime.timedelta. Это типичная ошибка.

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

Отметим следующее:

Наверху страницы отображается ключевая информации об исключении: тип исключения; любой параметр, вызвавший его (в данном случае это "unsupported type"); имя файла, в котором произошло исключение и номер соответствующей строки в файле.

Чуть ниже отображается полный трейс для данного исключения. Он аналогичен стандартному трейсу, который вы получаете при работе с интерпретатором командной строки, только этот более интерактивен. Для каждого уровня (кадра) стека Django отображает имя файла и имя функции/метода, номер строки и исходный код данной строки.

Если кликнуть на строку с исходным кодом, то вы увидите несколько строк выше и ниже данной строки, это даст вам контекст ошибки.

Кликните на «Local vars» под любым кадром стека для того, чтобы получить таблицу всех локальных переменных и их значения именно в этой точке кода. Эта отладочная информация бывает очень полезна.

Обратите внимание на текст «Switch to copy-and-paste view» под заголовком «Traceback». Нажмите на эти слова и трейс переключится в альтернативный вид, который может быть легко скопирован. Используйте эту возможность когда вам необходимо поделиться информацией об ошибке с другими разработчиками для получения технической поддержки — например с людьми с канала djangobook@conference.jabber.ru или в списке рассылки.

Ниже расположенная кнопка Share this traceback on a public Web site сделает эту работу за вас. Она отправляет информацию об ошибке на сервис http://www.dpaste.com/, вы получаете точный URL с текстом ошибки и можете передавать его другим разработчикам.

Секция «Request information» содержит информацию о пришедшем web-запросе, который вызвал ошибку: GET и POST информация, значения cookie и такая мета информация, как CGI заголовки.

Под секцией «Request information» расположена секция «Settings» со списком всех настроек для данного проекта. (Мы уже упоминали ROOT_URLCONF и мы покажем различные параметры в течение книги.)

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

Вы являетесь разработчиком, которому нравится производить отладку кода с помощью оператора print? Вы можете использовать для этого страницу ошибки Django — без всяких print. Вставьте на время в любой точке вашего представления оператор assert False — это вызовет отображение ошибки. Затем, вы можете просмотреть локальные переменные и состояние программ. Ниже дан пример использования этого подхода в представлении hours_ahead:

def hours_ahead(request, offset):
    try:
        offset = int(offset)
    except ValueError:
        raise Http404()
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    assert False
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

Очевидно, что большая часть данной информации не стоит показывать всем — она рассказывает о вашем коде и настройках Django — будет глупо отображать её всему Internet. Какой-нибудь отрицательный персонаж может воспользоваться ей для изучения вашего web-приложения и осуществления своих зловредных планов. По этой причине Django отображает страницу с ошибкой только когда находится в режиме отладки. Мы объясним позже как деактивировать отладочный режим в главе « Сессии, пользователи и регистрация ». Просто запомните, что после создания проект находится в режиме отладки.

Глава 4. Шаблоны

Перевод © Попов Руслан <ruslan.popov • gmail>

В предыдущей главе вы могли заметить что-то особенное в том, как мы возвращали текст в наших представлениях. А именно, HTML был напрямую вбит в код на Python, примерно так:

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

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

Любое изменение в дизайне страницы требует изменения в коде. Дизайн сайта изменяется гораздо чаще, чем сам код сайта. Удобнее будет иметь возможность вносить изменения в дизайн, не затрагивая код.

Разработка кода на языке Python и работа с HTML являются двумя различными дисциплинам и наиболее профессиональные среды web-разработки разделяют такую ответственность между отдельными людьми (или даже отдельными подразделениями). От дизайнеров и HTML/CSS кодеров не следует требовать редактирования Python кода для выполнения их работы.

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

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

Основы шаблонной системы

Шаблон Django — это строка текста, которая предназначена для разделения представления документа от его данных. Шаблон определяет места подстановки и различные виды основной логики (шаблонные теги), которая управляет отображением документа. Обычно, шаблоны используются для создания HTML, но шаблоны Django также способны участвовать в генерации любого текстового формата.

Давайте начнём с простого примера. Данный шаблон описывает HTML страницу, которая благодарит посетителя за заказ, отправленный компании. Рассматривайте его как вид письма:

<html>
<head><title>Ordering notice</title></head>

<body>

<h1>Ordering notice</h1>

<p>Dear {{ person_name }},</p>

<p>Thanks for placing an order from {{ company }}. It's scheduled to
ship on {{ ship_date|date:"F j, Y" }}.</p>

<p>Here are the items you've ordered:</p>

<ul>
{% for item in item_list %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

{% if ordered_warranty %}
    <p>Your warranty information will be included in the packaging.</p>
{% else %}
    <p>You didn't order a warranty, so you're on your own when
    the products inevitably stop working.</p>
{% endif %}

<p>Sincerely,<br />{{ company }}</p>

</body>
</html>

Данный шаблон представляет собой обычный HTML с несколькими переменными и шаблонными тегами. Рассмотрим его подробнее:

Любой текст взятый в скобки (т.е., {{ person_name }}) является переменной. Это означает «вставить значение переменной с указанным именем.» (Как же мы определяем значения переменных? Скоро мы это рассмотрим.)

Любой текст взятый в фигурные скобки и проценты (т.е., {% if ordered_warranty %}) является шаблонным тегом. Определение тега достаточно простое: тег просто указывает шаблонной системе «сделать что-то.»

Вышеприведёный шаблон содержит тег for ({% for item in item_list %}) и тег if ({% if ordered_warranty %}).

Тег for работает почти так же как и оператор for языка Python, позволяя вам циклически обрабатывать каждый элемент последовательности. Тег if, как вы можете ожидать, работает аналогично логическому оператору if. В данном примере, тег проверяет является ли True значением ordered_warranty. Если это так, шаблонная система отобразит всё, что находится между {% if ordered_warranty %} и {% else %}. В противном случае, шаблонная система отобразит всё между {% else %} и {% endif %}. Следует отметить, что элемент {% else %} является необязательным.

Наконец, второй параграф данного шаблона содержит пример использования фильтра, который предоставляет наиболее удобный способ изменения формата переменной. В данном примере, {{ ship_date|date:"F j, Y" }}, мы передаём значение переменной ship_date фильтру date, указывая для него аргументы "F j, Y". Фильтр date форматирует дату в соответствии с указанным форматом. Фильтры подключаются к переменным с помощью символа |, аналогично Unix потокам.

Каждый шаблон Django имеет доступ к некоторому количеству встроенных тегов и фильтров, многие из которых будут рассмотрены в последующих разделах главы. Приложение F FIXME содержит полный список тегов и фильтров, будет неплохо, если вы ознакомитесь с этим списком. Также есть возможность создавать свои собственный фильтры и теги, мы рассмотрим это в главе 9 FIXME.

Использование шаблонной системы

Давайте рассмотрим шаблонную систему Django подробнее, вы сможете увидеть как она работает, но пока мы не будем интегрировать её с представлениями, которые мы создали в прошлой главе. Наша цель в данный момент — показать вам, как шаблонная система работает независимо от остальной части Django. (Предположим другой способ: обычно вы будете использовать шаблонную систему совместно с представлениями, но мы желаем показать то, что шаблонная система является обычной библиотекой Python, которую вы можете использовать где угодно, не только с представлениями Django.)

Ниже указан самый основной способ использования шаблонной системы Django в коде Python:

Создать объект Template, передав ему шаблон в виде строки.

Вызвать метод render() объекта Template с набором переменных (контекст). Метод возвратит полностью обработанный шаблон в виде строки, все переменные и шаблонные теги будут вычислены в соответствии с контекстом.

Пример использования:

>>> from django import template
>>> t = template.Template('My name is {{ name }}.')
>>> c = template.Context({'name': 'Adrian'})
>>> print t.render(c)
My name is Adrian.
>>> c = template.Context({'name': 'Fred'})
>>> print t.render(c)
My name is Fred.

Следующие разделы описывают каждый шаг более детально.

Создание шаблонных объектов

Простейшим способом создания объекта Template является его прямое порождение. Класс Template расположен в модуле django.template, конструктор принимает единственный аргумент — строку с кодом шаблона. Давайте рассмотрим как это всё работает с помощью интерактивного интерпретатора языка Python.

В каталоге проекта mysite, созданном с помощью команды django-admin.py startproject (это было описано в главе « Начинаем »), выполните команды python manage.py shell для запуска интерактивного интерпретатора.

Пример интерактивной интерпретации

Если вы использовали Python ранее, вас может заинтересовать, почему мы используем python manage.py shell, а не просто python. Обе команды запускают интерпретатор, но наш вариант имеет одну ключевую особенность: перед запуском интерпретатора, он указывает Django какой файл конфигурации следует использовать. Многие компоненты Django, включая шаблонную систему, зависят от ваших настроек и вы не сможете их использовать до тех пор, пока среда разработки не будет знать где их брать.

Если вам это интересно, расскажем как всё это работает внутри. Django ищет переменную среды с именем DJANGO_SETTINGS_MODULE, которая должна содержать путь к вашему файлу settings.py. Например, DJANGO_SETTINGS_MODULE может быть установлено в mysite.settings, подразумевая, что каталог mysite находится в пути Python.

При запуске python manage.py shell настройка DJANGO_SETTINGS_MODULE производится автоматически. Мы рекомендуем использовать данный метод для работы с приведёнными ниже примерами.

После того, как вы лучше изучите Django, вероятно вы перестанете использовать данный способ и будете устанавливать DJANGO_SETTINGS_MODULE вручную в вашем .bash_profile или в другом конфигурационном файле вашей среды.

Рассмотрим основы шаблонной системы:

>>> from django.template import Template
>>> t = Template('My name is {{ name }}.')
>>> print t

Если вы попробовали запустить данный код в интерпретаторе, то вы увидели нечто подобное:

<django.template.Template object at 0xb7d5f24c>

Шестнадцатеричный адрес объекта — 0xb7d5f24c будет всякий раз разным, не обращайте на это внимание. Это внутренняя информация Python, так называемый «идентификатор» созданного объекта Template.

При создании объекта Template шаблонная система компилирует строку с кодом шаблона во внутренний оптимизированный формат, готовый для обработки. Но если ваш шаблонный код содержит синтаксические ошибки, вызов Template() приведёт к исключению TemplateSyntaxError:

>>> from django.template import Template
>>> t = Template('{% notatag %}')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  ...
django.template.TemplateSyntaxError: Invalid block tag: 'notatag'

Термин «block tag[7]» в данном случае относится к {% notatag %}. «Блочный тег» и «шаблонный тег» являются синонимами.

Система вызывает исключение TemplateSyntaxError в любом из нижеприведённых случаев:

Неверные теги;

Неверные аргументы у верных тегов;

Неверные фильтры;

Неверные аргументы у верных фильтров;

Неверный синтаксис шаблона;

Незакрытые теги (для тегов, которые требуют наличия завершающих тегов).

Обработка шаблона

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

Контекст в Django представлен классом Context, который определён в модуле django.template. Конструктор этого класса принимает один необязательный аргумент — словарь, содержащий имена переменных и ассоциированные им значения. Вызовите метод render() объекта Template для заполнения шаблона контекстными значениями:

>>> from django.template import Context, Template
>>> t = Template('My name is {{ name }}.')
>>> c = Context({'name': 'Stephane'})
>>> t.render(c)
u'My name is Stephane.'

Единственное, о чём следует упомянуть — то, что значение t.render(c) является объектом Unicode, а не обычной строкой Python. Вы можете выразить это, добавив u перед строкой. Django везде использует объекты Unicode вместо обычных строк. Если вы не понимаете последствия этого подхода, не беспокойтесь. Просто запомните, что поддержка Unicode позволяет упростить для вашего приложения поддержку национальных языков.

Словари и контексты

Словарь в языке Python является отображением между известным набором ключей и их значений. Context подобен словарю, но он также предоставляет дополнительный функционал, который описан в главе 9 FIXME.

Имена переменных должны начинаться с буквы (A-Z или a-z) и могут состоять из множества букв, цифр, символов подчёркивания и точек. (Точки являются особым случаем, мы скоро рассмотрим их.) Имена переменных регистрозависимы.

Ниже показан пример компиляции и обработки шаблона, который похож на тот, что мы рассмотрели в начале главы:

>>> from django.template import Template, Context
>>> raw_template = """<p>Dear {{ person_name }},</p>
...
... <p>Thanks for placing an order from {{ company }}. It's scheduled to
... ship on {{ ship_date|date:"F j, Y" }}.</p>
...
... {% if ordered_warranty %}
... <p>Your warranty information will be included in the packaging.</p>
... {% else %}
... <p>You didn't order a warranty, so you're on your own when
... the products inevitably stop working.</p>
... {% endif %}
...
... <p>Sincerely,<br />{{ company }}</p>"""
>>> t = Template(raw_template)
>>> import datetime
>>> c = Context({'person_name': 'John Smith',
...     'company': 'Outdoor Equipment',
...     'ship_date': datetime.date(2009, 4, 2),
...     'ordered_warranty': False})
>>> t.render(c)
u"<p>Dear John Smith,</p>\n\n<p>Thanks for placing an order from Outdoor
Equipment. It's scheduled to\nship on April 2, 2009.</p>\n\n\n<p>You
didn't order a warranty, so you're on your own when\nthe products
inevitably stop working.</p>\n\n\n<p>Sincerely,<br />Outdoor Equipment
</p>"

Рассмотрим каждую строку кода:

Сначала мы импортируем классы Template и Context, которые определены в модуле django.template.

Мы сохраняем оригинальный текст нашего шаблона в переменной raw_template. Следует отметить, что мы используем тройные кавычки для выделения строки, так как мы имеем дело с многострочной строкой. Дело в том, что строки, заключённые в одинарные кавычки, не могут быть многострочными.

Затем мы создаём шаблонный объект, t, передавая raw_template в конструктор класса Template.

Мы импортируем модуль datetime из стандартной библиотеки Python, так как нам он понадобится в дальнейшем.

Затем мы создаём контекстный объект, c. Конструктор класса Context принимает словарь, который содержит ассоциированные именам значения. В нашем случае, мы определили, что person_name имеет значение 'John Smith', company — 'Outdoor Equipment' и так далее.

В конце мы вызвали метод render() для нашего шаблонного объекта, передав ему контекст. Нам был возвращён обработанный шаблон, т.е., метод заменил переменные их действительными значениями и вычислил все шаблонные теги.

Следует отметить, что параграф «You didn’t order a warranty» был отображён, потому что переменная ordered_warranty вычислилась в False. Обратите внимание на дату, April 2, 2009, которая отобразилась в соответствии с заданным форматом — 'F j, Y'. (Мы расскажем об аргументах фильтра date немного позже.)

Если вы новичок в Python, вы можете заинтересоваться почему результат содержит символы перевода строки (\n), вместо того, чтобы отображать реальные переводы строк. Так происходит из-за тонкостей работы интерактивного интерпретатора Python: вызов t.render(c) возвращает строку и, по умолчанию, интерактивный интерпретатор отображает представление строки, вместо печатного варианта строки. Если вы желаете получить печатную строку, используйте оператор print: print t.render(c).

Таковы основы использования шаблонной системы Django: просто напишите шаблонную строку, создайте объект Template, создайте Context и вызовите метод render().

Множество контекстов и единственный шаблон

После того, как вы создали объект Template, вы можете с его помощью обработать множество контекстов. Например:

# Так делать не стоит
for name in ('John', 'Julie', 'Pat'):
    t = Template('Hello, {{ name }}')
    print t.render(Context({'name': name}))

# Правильный подход
t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat'):
    print t.render(Context({'name': name}))

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

Поиск контекстной переменной

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

Ключом к такой всеядности является символ . (точка). Используйте точку для получения доступа к ключам словаря, атрибутам, методам и индексам объекта.

Это хорошо проиллюстрировано в нескольких примерах. Например, предположим, что вы передали словарь в шаблон. Для получения доступа к значениям этого словаря по ключу следует использовать точку:

>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
u'Sally is 43 years old.'

Аналогично, точка также позволяет получить доступ к атрибутам объекта. Например, объект datetime.date содержит атрибуты year, month и day, и вы можете получить к ним доступ:

>>> from django.template import Template, Context
>>> import datetime
>>> d = datetime.date(1993, 5, 2)
>>> d.year
1993
>>> d.month
5
>>> d.day
2
>>> t = Template('The month is {{ date.month }} and the year is {{ date.year }}.')
>>> c = Context({'date': d})
>>> t.render(c)
u'The month is 5 and the year is 1993.'

Следующий пример использует класс, демонстрируя методику получения доступа к атрибутам объекта:

>>> from django.template import Template, Context
>>> class Person(object):
...     def __init__(self, first_name, last_name):
...         self.first_name, self.last_name = first_name, last_name
>>> t = Template('Hello, {{ person.first_name }} {{ person.last_name }}.')
>>> c = Context({'person': Person('John', 'Smith')})
>>> t.render(c)
u'Hello, John Smith.'

С помощью точек можно получать доступ к методам объектов. Например, каждая строка Python обладает методами upper() и isdigit(), и вы можете использовать их в шаблонной системе Django с помощью «точечного» синтаксиса:

>>> from django.template import Template, Context
>>> t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}')
>>> t.render(Context({'var': 'hello'}))
u'hello -- HELLO -- False'
>>> t.render(Context({'var': '123'}))
u'123 -- 123 -- True'

Следует отметить, что не следует завершать имя метода скобками. Также нет никакой возможности передать аргументы в методы, вы можете использовать только те методы, которые не требуют аргументов. (Мы объясним такой подход немного позже в этой главе.)

Наконец, точки используются для доступа к элементам списка по индексу, например:

>>> from django.template import Template, Context
>>> t = Template('Item 2 is {{ items.2 }}.')
>>> c = Context({'items': ['apples', 'bananas', 'carrots']})
>>> t.render(c)
u'Item 2 is carrots.'

Отрицательные значения индекса не допускаются. Например, шаблонная переменная {{ items.-1 }} вызовет исключение TemplateSyntaxError.

Списки в Python

Запомните: индексирование элементов списка в Python начинается с нуля. Первый элемент имеет индекс 0, второй — 1 и так далее.

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

Поиск в словаре (т.е., foo["bar"]);

Поиск атрибута (т.е., foo.bar);

Вызов метода (т.е., foo.bar());

Поиск в списке по индексу (т.е., foo[2]).

Система использует первое, что сработает.

«Точечный» поиск может быть многоуровневым. Например, в следующем примере используется переменная {{ person.name.upper }}, которая преобразовывается в поиск по словарю (person['name']) и затем вызывается метод (upper()):

>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name.upper }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
u'SALLY is 43 years old.'

Вызов методов

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

Если в процессе «точечного» поиска метод вызывает исключение, оно будет обработано. Но только если исключение не будет иметь атрибута silent_variable_failure установленный в True. Если исключение имеет атрибут silent_variable_failure, переменная будет преобразовываться в пустую строку, например:

>>> t = Template("My name is {{ person.first_name }}.")
>>> class PersonClass3:
...     def first_name(self):
...         raise AssertionError, "foo"
>>> p = PersonClass3()
>>> t.render(Context({"person": p}))
Traceback (most recent call last):
...
AssertionError: foo

>>> class SilentAssertionError(AssertionError):
...     silent_variable_failure = True
>>> class PersonClass4:
...     def first_name(self):
...         raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
u'My name is .'

Вызов метода срабатывает только в случае, если метод не требует наличия аргументов. В противном случае, система попробует следующий тип поиска (поиск в списке по индексу).

Очевидно, что некоторые методы будут иметь побочные эффекты и, в лучшем случае, было бы глупо давать шаблонной системе доступ к ним.

Например, у вас есть объект BankAccount, у которого есть метод delete(). Если шаблон содержит что-то аналогичное {{ account.delete }}, где account — это объект BankAccount, то объект будет удалён во время обработки шаблона!

Чтобы сделать такое невозможным, установите для этого метода атрибут alters_data:

def delete(self):
    # Delete the account
delete.alters_data = True

Шаблонная система не будет выполнять метод, помеченный таким образом. Продолжая обсуждение вышеприведённого примера, если шаблон содержит {{ account.delete }} и метод delete() имеет alters_data=True, то метод delete() не будет выполнен при обработке шаблона, он будет просто проигнорирован.

Обработка неправильных переменных

По умолчанию, если переменная не существует, шаблонная система отобразит такую переменную как пустую строку. Например:

>>> from django.template import Template, Context
>>> t = Template('Your name is {{ name }}.')
>>> t.render(Context())
u'Your name is .'
>>> t.render(Context({'var': 'hello'}))
u'Your name is .'
>>> t.render(Context({'NAME': 'hello'}))
u'Your name is .'
>>> t.render(Context({'Name': 'hello'}))
u'Your name is .'

Система просто игнорирует такие шаблонные переменные, не вызывая исключение, потому что она должна быть устойчивой к человеческим ошибкам. В данном случае все «точечные» проверки будут неудачными. В случае настоящего приложения недопустимо, чтобы web-сайт стал недоступным из-за маленькой ошибки в шаблоне.

Играем с контекстными объектами

Большую часть времени, вы будете создавать объекты Context передавая готовый словарь в Context(). Но вы можете добавлять и удалять элементы из объекта Context после его создания, используя стандартный синтаксис словаря Python:

>>> from django.template import Context
>>> c = Context({"foo": "bar"})
>>> c['foo']
'bar'
>>> del c['foo']
>>> c['foo']
Traceback (most recent call last):
  ...
KeyError: 'foo'
>>> c['newvariable'] = 'hello'
>>> c['newvariable']
'hello'

[7] Блочный тег

Основные шаблонные теги и фильтры

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

Теги

if/else

Тег {% if %} вычисляет переменную и если результатом вычисления является True (т.е., она существует, не пустая и не равна False), система отображает всё между {% if %} и {% endif %}, например:

{% if today_is_weekend %}
    <p>Welcome to the weekend!</p>
{% endif %}

Тег {% else %} является необязательным:

{% if today_is_weekend %}
    <p>Welcome to the weekend!</p>
{% else %}
    <p>Get back to work.</p>
{% endif %}

«Истина» в Python

В Python и в шаблонной системе Django нижеприведённые объекты вычисляются в False в контексте Boolean:

Пустой список ([]);

Пустой кортеж (());

Пустой словарь ({});

Ноль (0);

Особый объект None;

Объект False (это очевидно);

Особые объекты, которые определяют их собственное поведение Boolean (это питонские навороты).

Всё остальное вычисляется в True.

Тег {% if %} принимает and, or или not для тестирования множества переменных или инвертирования. Например:

{% if athlete_list and coach_list %}
    Both athletes and coaches are available.
{% endif %}

{% if not athlete_list %}
    There are no athletes.
{% endif %}

{% if athlete_list or coach_list %}
    There are some athletes or some coaches.
{% endif %}

{% if not athlete_list or coach_list %}
    There are no athletes or there are some coaches.
{% endif %}

{% if athlete_list and not coach_list %}
    There are some athletes and absolutely no coaches.
{% endif %}

Теги {% if %} не позволяют объединять and и or в одном теге, потому что порядок логики будет двусмысленным. Пример с ошибкой:

{% if athlete_list and coach_list or cheerleader_list %}

Использование скобок для управления порядком операций не поддерживается. Если вам потребуются скобки, рассмотрите применение логики вне шаблона и передачу результата вычисления в виде выделенной шаблонной переменной. Или просто используйте вложенные теги {% if %}, например:

{% if athlete_list %}
    {% if coach_list or cheerleader_list %}
        We have athletes, and either coaches or cheerleaders!
    {% endif %}
{% endif %}

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

{% if athlete_list or coach_list or parent_list or teacher_list %}

Тега {% elif %} не существует. Используйте вложенные теги {% if %} для решения такой задачи:

{% if athlete_list %}
    <p>Here are the athletes: {{ athlete_list }}.</p>
{% else %}
    <p>No athletes are available.</p>
    {% if coach_list %}
        <p>Here are the coaches: {{ coach_list }}.</p>
    {% endif %}
{% endif %}

Не забывайте закрывать каждый тег {% if %} с помощью {% endif %}. В противном случае Django будет вызывать исключение TemplateSyntaxError.

for

Тег {% for %} позволяет обработать каждый элемент последовательности. Аналогично оператору for языка Python, синтаксис таков for X in Y, где Y — это последовательность, а X — это имя переменной, в которую помещается значение текущего элемента. На каждом шагу цикла, шаблонная система выводит всё, что находится между тегами {% for %} и {% endfor %}.

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

<ul>
{% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
{% endfor %}
</ul>

Добавьте reversed к тегу для обработки списка в обратном порядке:

{% for athlete in athlete_list reversed %}
...
{% endfor %}

Теги {% for %} можно вкладывать друг в друга:

{% for athlete in athlete_list %}
    <h1>{{ athlete.name }}</h1>
    <ul>
    {% for sport in athlete.sports_played %}
        <li>{{ sport }}</li>
    {% endfor %}
    </ul>
{% endfor %}

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

{% if athlete_list %}
    {% for athlete in athlete_list %}
        <p>{{ athlete.name }}</p>
    {% endfor %}
{% else %}
    <p>There are no athletes. Only computer programmers.</p>
{% endif %}

Так как этот способ часто употребляется на практике, тег {% for %} поддерживает необязательный оператор {% empty %}, который позволяет выводить сообщение, в случае пустого списка. Этот пример эквивалентен предыдущему:

{% for athlete in athlete_list %}
    <p>{{ athlete.name }}</p>
{% empty %}
    <p>There are no athletes. Only computer programmers.</p>
{% endfor %}

Нет стандартной возможности прекратить цикл до окончания обработки списка. Если вам надо это сделать, создайте переменную, которая будет содержать список только с теми значениями, которые нужны. Аналогично, нет поддержки оператора continue, который бы позволил перейти к обработке следующего элемента списка. (Обратитесь к разделу « Философия и ограничения » для описания причин, которые привели к такому поведению тега.)

Во время каждой итерации {% for %} вы получаете доступ к шаблонной переменной forloop. Эта переменная обладает несколькими атрибутами, которые предоставляют информацию о текущем цикле:

forloop.counter всегда содержит целое число, представляющее собой количество выполненных итераций, включая текущую. Отсчёт начинается с единицы, таким образом, во время первой итерации данный атрибут будет иметь значение 1. Пример:

{% for item in todo_list %}
    <p>{{ forloop.counter }}: {{ item }}</p>
{% endfor %}

forloop.counter0 аналогичен forloop.counter, только отсчёт начинается с нуля. То есть, во время первой итерации данный атрибут будет иметь значение 0.

forloop.revcounter всегда содержит целое число, представляющее собой количество оставшихся итераций. Во время первой итерации данный атрибут содержит число, равное количеству итераций цикла, а во время последней итерации — атрибут содержит значение 1.

forloop.revcounter0 аналогичен атрибуту forloop.revcounter, только отсчёт начинается с нуля. Во время первой итерации данный атрибут содержит число, равное количеству итераций цикла минус единица, а время последней итерации — атрибут содержит значение 0.

forloop.first является Boolean и установлен в True только во время первой итерации цикла. Пример использования:

{% for object in objects %}
    {% if forloop.first %}<li class="first">{% else %}<li>{% endif %}
    {{ object }}
    </li>
{% endfor %}

forloop.last является Boolean и установлен в True только во время последней итерации цикла. Пример использования:

{% for link in links %}{{ link }}
  {% if not forloop.last %}|{% endif %}
{% endfor %}

После обработки данного тега получается примерно такой результат:

Link1 | Link2 | Link3 | Link4

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

Favorite places:
{% for p in places %}{{ p }}{% if not forloop.last %}, {% endif %}{% endfor %}

forloop.parentloop является ссылкой на объект forloop родительского цикла, в случае вложенных циклов. Пример:

{% for country in countries %}
    <table>
    {% for city in country.city_list %}
        <tr>
        <td>Country #{{ forloop.parentloop.counter }}</td>
        <td>City #{{ forloop.counter }}</td>
        <td>{{ city }}</td>
        </tr>
    {% endfor %}
    </table>
{% endfor %}

Эта волшебная переменная forloop доступна только внутри циклов. После того, как парсер достигнет тега {% endfor %}, переменная forloop уничтожается.

Контекст и переменная forloop

Inside the {% for %} block, the existing variables are moved out of the way to avoid overwriting the magic forloop variable. Django exposes this moved context in forloop.parentloop FIXME. В общем случае вам не надо беспокоиться об этом, но если вы будете использовать в блоке переменную forloop (не смотря на наши рекомендации не делать так), она будет переименована в forloop.parentloop на время обработки блока {% for %}.

ifequal/ifnotequal

Шаблонная система Django умышленно создана в виде неполноценного языка программирования и, следовательно, не позволит вам выполнить операторы языка Python. (Более подробно данный аспект рассмотрен в разделе « Философия и ограничения » данной главы.) Тем не менее, сравнение двух значений и отображение некой информации в случае равенства является достаточно общим требованием к шаблонам. И Django предоставляет тег {% ifequal %} для решения данной задачи.

Тег {% ifequal %} сравнивает два значения и отображает всё, что находится между {% ifequal %} и {% endifequal %}, если эти значения равны.

Данный пример сравнивает шаблонные переменные user и currentuser:

{% ifequal user currentuser %}
    <h1>Welcome!</h1>
{% endifequal %}

Аргументами могут служить строки (вбитые в шаблон), ограниченные одинарными или двойными кавычками, вот пример:

{% ifequal section 'sitenews' %}
    <h1>Site News</h1>
{% endifequal %}

{% ifequal section "community" %}
    <h1>Community</h1>
{% endifequal %}

Аналогично {% if %}, тег {% ifequal %} поддерживает необязательный тег {% else %}:

{% ifequal section 'sitenews' %}
    <h1>Site News</h1>
{% else %}
    <h1>No News Here</h1>
{% endifequal %}

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

{% ifequal variable 1 %}
{% ifequal variable 1.23 %}
{% ifequal variable 'foo' %}
{% ifequal variable "foo" %}

Любые другие типы переменных, подобных словарям, спискам или логическим значениям не могут быть использованы в {% ifequal %}. Ниже показаны примеры неправильного использования:

{% ifequal variable True %}
{% ifequal variable [1, 2, 3] %}
{% ifequal variable {'key': 'value'} %}

Если вам требуется проверить соответствие чего-либо логическому значению, используйте тег {% if %} вместо {% ifequal %}.

Комментарии

Аналогично принятому в HTML или Python, шаблонный язык Django позволяет использовать комментарии. Для определения комментария используйте {# #}:

{# This is a comment #}

Комментарий не будет отображен при обработке шаблона.

Комментарии, использующие данный синтаксис, не могут быть многострочными. Данное ограничение повышает производительность обработки шаблонов. При обработке следующего шаблона комментарий будет отображён как есть (т.е., тег не будет распознан как комментарий):

This is a {# this is not
a comment #}
test.

Если вам необходимо использовать многострочные комментарии, используйте шаблонный тег {% comment %}, вот так:

{% comment %}
This is a
multi-line comment.
{% endcomment %}

Фильтры

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

{{ name|lower }}

Этот код выведет значение переменной {{ name }}, после его обработки фильтром lower, который преобразует буквы текста в нижний регистр.

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

{{ my_list|first|upper }}

Некоторые фильтры принимают аргументы. Аргумент фильтра указывается после двоеточия и всегда в двойных кавычках. Например:

{{ bio|truncatewords:"30" }}

Этот код отобразит первые тридцать слов из переменной bio.

Далее представлены несколько наиболее важных фильтров. Приложение F FIXME описывает все остальные.

addslashes: Добавляет обратные слеш перед символами: обратный слеш, одинарная или двойная кавычки. Это удобно при включении созданного текста в строку JavaScript.

date: Форматирует вывод объекта date или datetime в соответствии с форматной строкой, переданной в параметре, например:

{{ pub_date|date:"F j, Y" }}

Формат аргумента определён в приложении F FIXME.

length: Возвращает длину значения. Для списка возвращается количество его элементов. Для строки — количество символов. (Следует отметить, что этот фильтр работает с любым объектом Python, который знает как определять свою длину, т.е., с любым объектом обладающим методом __len__().)

Философия и ограничения

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

Мнения разработчиков о шаблонной системе варьируются очень широко по сравнению с другими компонентами веб приложений. Поддерживает это утверждение тот факт, что сам Python содержит их десятки, если не сотни. Каждая шаблонная система вероятно была создана потому что её разработчик счёл все существующие системы неподходящими. (В действительности, существует своеобразный экзамен для разработчика на Python — написать свой собственный язык шаблонной системы! Если вы не сделали такого, попробуйте. Это интересный опыт.)

Помня это, вам возможно будет интересно узнать, что Django не требует, чтобы вы использовали именно её язык шаблонов. По причине того, что Django создана как полноценная система веб разработки, которая предоставляет все необходимые компоненты для продуктивной работы, будет гораздо удобнее использовать её шаблонную систему, вместо других шаблонных библиотек Python. Но это не накладывает никаких ограничений на вас. Как вы увидите в разделе « Использование шаблонов в представлениях », очень легко использовать другой шаблонный язык совместно с Django.

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

Бизнес-логика должна быть отделена от логики представления. Мы рассматриваем шаблонную систему как инструмент управления представлением и логикой для этого представления, не более. Шаблонная система не должна поддерживать функциональность, которая выходит за рамки этой основополагающей цели.

По этой причине нет возможности вызвать напрямую код на языке Python из шаблонов Django. Всё «программирование» принципиально ограничено рамками возможностей тегов. Можно создать пользовательские шаблонные теги для выполнения определённых задач, но намеренно не позволяется выполнять код на языке Python вне тегов шаблона.

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

Предполагается, что дизайнеры способны работать с чистым HTML. Шаблонная система не разработана так, чтобы шаблоны всегда отображались красиво в WYSIWYG редакторах, подобных Dreamweaver. Поддержка таких редакторов накладывает слишком серьёзные ограничения на синтаксис шаблона. Django ожидает, что авторы шаблонов работают с HTML напрямую.

Предполагается, что дизайнеры не являются Python программистами. Авторы шаблонной системы признают, что чаще всего шаблоны для страниц разрабатываются дизайнерами, не разработчиками, и, следовательно, это не должно предполагать наличия знания языка Python.

Тем не менее, шаблонная система старается поддерживать небольшие команды, в которых шаблоны создаются Python программистами. Она предлагает способ расширения синтаксиса системы с помощью кода на языке Python (подробности в главе 9 FIXME).

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

Использование шаблонов в представлениях

Вы изучили основы использования шаблонной системы. Теперь давайте используем эти знания для создания представления. Вспомните представление current_datetime из модуля mysite.views, с которого мы начали предыдущую главу. Вот как оно выглядит:

from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

Внесём изменения в это представление, чтобы оно стало использовать шаблонную систему Django. Сначала вы можете подумать о чём-то подобном:

from django.template import Template, Context
from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    t = Template("<html><body>It is now {{ current_date }}.</body></html>")
    html = t.render(Context({'current_date': now}))
    return HttpResponse(html)

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

Сначала вы можете рассмотреть вариант сохранения шаблона в каком-нибудь файле на файловой системе и затем использовать стандартную для Python функциональность для загрузки содержимого этого шаблона. Ниже показан такой вариант, предполагается, что шаблон находится в файле /home/djangouser/templates/mytemplate.html:

from django.template import Template, Context
from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    # Simple way of using templates from the filesystem.
    # This doesn't account for missing files!
    fp = open('/home/djangouser/templates/mytemplate.html')
    t = Template(fp.read())
    fp.close()
    html = t.render(Context({'current_date': now}))
    return HttpResponse(html)

Такой подход, тем не менее, не элегантен по следующим причинам:

Он не обрабатывает случай отсутствия файла. Если файл /home/djangouser/templates/mytemplate.html не существует или недоступен для чтения, вызов open() приведёт к исключению IOError.

Он ищет шаблон по жёстко указанному пути. Если вы используете такой метод для каждой функции представления, вы будете размножать путь до шаблона. Мы даже не говорим, как это надо будет набирать!

Он включает в себя много одинакового кода. Многое можно сделать вместо вызова open(), fp.read() и fp.close() при каждой загрузке шаблона.

Для решения подобных проблем мы будем использовать загрузку шаблонов и каталоги шаблонов, которые описаны в этой главе далее.

Загрузка шаблонов

Django предоставляет удобный и мощный API для загрузки шаблонов с диска с целью удаления избыточности в вызовах подгрузки шаблонов и в самих шаблонах.

Для использования этого API сначала требуется указать среде разработки где располагаются шаблоны. Это делать надо в файле конфигурации проекта — settings.py, как мы показывали в прошлой главе, когда рассказывали о параметре ROOT_URLCONF.

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

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

Этот параметр указывает механизму шаблонной системы Django пути, по которым следует искать шаблоны. Укажите требуемый каталог, в котором вы будете хранить шаблоны, например так:

TEMPLATE_DIRS = (
    '/home/django/mysite/templates',
)

Надо отметить несколько моментов:

Вы можете указать любой каталог, который доступен на чтение пользователю, от которого работает ваш веб сервер. Если вы не можете придумать такое место, мы рекомендуем создать каталог templates в каталоге вашего проекта (т.е. в каталоге mysite, который вы создали в главе « Начинаем »).

Если ваш параметр TEMPLATE_DIRS содержит только один каталог, не забудьте запятую в конце строки с каталогом шаблонов!

Неправильно:

# Missing comma!
TEMPLATE_DIRS = (
    '/home/django/mysite/templates'
)

Правильно:

# Comma correctly in place.
TEMPLATE_DIRS = (
    '/home/django/mysite/templates',
)

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

Если вы используете Windows, включите букву диска и используйте Unix-стиль с прямыми слэшами вместо обратных, вот так:

TEMPLATE_DIRS = (
    'C:/www/django/templates',
)

Проще всего использовать абсолютные пути (т.е. пути до каталогов, которые начинаются от корня файловой системы). Если вы желаете быть более гибким и независимым в данном вопросе, вы можете воспользоваться тем, что файлы настроек в Django — это всего лишь код на языке Python и содержимое TEMPLATE_DIRS можно изменять динамически, например:

import os.path

TEMPLATE_DIRS = (
    os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'),
)

Этот пример использует «волшебную» переменную __file__, которая автоматически заменяется именем файла модуля Python, в котором располагается данный код. Она получает имя каталога, в котором содержится settings.py (os.path.dirname), соединяет его с templates кроссплатформенным способом (os.path.join) и затем проверяет, что используются только прямые слеши, а обратных нет (в случае Windows).

Работая с динамическим Python кодом в файле настроек, следует отметить, что очень важно избегать ошибок в этом коде. Если вы сделаете синтаксическую ошибку или ошибку, которая возникнет во время исполнения кода, ваш сайт не будет работать.

После установки TEMPLATE_DIRS, следующим шагом будет изменение кода представления для использования функциональности Django для загрузки шаблонов, вместо указания путей к шаблонам. Возвращаясь к нашему представлению current_datetime, давайте изменим его следующим образом:

from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    t = get_template('current_datetime.html')
    html = t.render(Context({'current_date': now}))
    return HttpResponse(html)

В данном примере, мы использовали функцию django.template.loader.get_template() вместо ручной загрузки шаблона из файловой системы. Функция get_template() принимает имя шаблона в качестве аргумента, производит поиск такого шаблона на файловой системе, открывает этот файл и возвращает скомпилированный объект Template.

В данном примере наш шаблон имеет имя current_datetime.html, но в расширении .html нет ничего особенного. Вы можете давать вашим шаблонам любое расширение или вообще не использовать его.

Для того, чтобы определить расположение шаблонов на файловой системе, get_template() использует информацию из TEMPLATE_DIRS и переданное имя шаблона. Например, если параметр TEMPLATE_DIRS содержит '/home/django/mysite/templates', функция get_template() будет искать шаблон по пути /home/django/mysite/templates/current_datetime.html.

Если get_template() не может найти шаблон с указанным именем, она возвращает исключение TemplateDoesNotExist. Чтобы посмотреть на него, снова запустите сервер разработки Django с помощью команды python manage.py runserver, выполненной в каталоге проекта. Затем запустите с помощью браузера представление current_datetime (т.е. перейдите по ссылке http://127.0.0.1:8000/time/). Предполагая что настройка DEBUG установлена True и вы ещё не создали шаблон current_datetime.html, вы должны увидеть страницу с ошибкой, на которой выделено исключение TemplateDoesNotExist.

Рисунок 4.1. Страница с ошибкой показывающей, что шаблон не найден

Эта страница с ошибкой похожа на такую же, которую мы рассматривали в главе « Представления и привязки URL », только одна вещь тут новая — отладочная информация: секция «Template-loader postmortem». Эта информация неоценима при отладке ошибок загрузки шаблона.

Далее, создадим файл /home/django/templates/current_datetime.html в вашем каталоге с шаблонами, с помощью кода:

<html><body>It is now {{ current_date }}.</body></html>

Обновите страницу в вашем браузере и вы увидите результат обработки шаблона.

render_to_response()

Мы показали вам как загружать шаблон, заполнять Context и возвращать объект HttpResponse с обработанным шаблоном. Мы оптимизировали код, используя get_template() для загрузки шаблона. Но вам по-прежнему требуется набирать много кода для выполнения этих действий. По причине частого использования этой последовательности действий Django предоставляет более короткий метод, который позволяет вам выполнять эти действия одной строкой кода.

Речь идёт о функции render_to_response(), которая определена в модуле django.shortcuts. Чаще всего вы будете использовать render_to_response() вместо вышеописанных действий, кроме случая когда ваш работодатель оценивает ваш труд по количеству написанных строк кода.

Ниже представлен старый знакомый пример current_datetime, использующий функцию render_to_response():

from django.shortcuts import render_to_response
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    return render_to_response('current_datetime.html', {'current_date': now})

Какая разница! Пройдёмся по изменениям в коде:

Нам больше не надо импортировать get_template, Template, Context или HttpResponse. Вместо этого, мы импортируем django.shortcuts.render_to_response. Импорт datetime остался без изменений.

В функции current_datetime мы всё ещё вычисляем now, но загрузка шаблона, создание контекста, обработка шаблона и создание объекта HttpResponse — обо всём этом заботится функция render_to_response(). Так как эта функция возвращает объект HttpResponse, мы просто возвращаем это значение в представление.

Первым аргументом функции render_to_response() должно быть имя используемого шаблона. Вторым аргументом, если он есть, должен быть словарь, используемый при создании контекста для этого шаблона. Если вы не предоставите второй аргумент, функция будет использовать пустой словарь.

Трюк с locals()

Рассмотрим последнюю инкарнацию функции current_datetime:

def current_datetime(request):
    now = datetime.datetime.now()
    return render_to_response('current_datetime.html', {'current_date': now})

Постоянно вы будете ловить себя на том, что вы вычисляете значения, сохраняете их в переменных (см. now в предыдущем примере) и отправляете эти переменные в шаблон. Особо ленивые разработчики должны отметить, что это немного излишне назначать имена временным переменным и шаблонным переменным. Это не только излишне, но и требует много набора на клавиатуре.

Таким образом, если вы один из таких ленивых разработчиков и вам нравится краткий код, вы можете воспользоваться встроенной в Python функцией locals(). Она возвращает словарь, хранящий все локальные переменные и их соответствующие значения (речь идёт о всех переменных, определённых в текущей области видимости). Следовательно, предыдущую функцию представления можно переписать так:

def current_datetime(request):
    current_date = datetime.datetime.now()
    return render_to_response('current_datetime.html', locals())

Здесь, вместо ручного ввода контекстного словаря, как это было раньше, вы передали значение функции locals(), которое содержало все переменные определённые в этой точке выполнения функции. Как результат, мы переименовали переменную now в current_date, потому что это то имя, которое шаблон ожидает. В этом примере locals() не предлагает значительного удобства, но эта методика может сохранить вам время, если вы используете несколько шаблонных переменных или если вы ленивы.

Следует обратить внимание при использовании locals() на то, что она включает в результат каждую локальную переменную. Это может быть не совсем то, что вам нужно. В предыдущем примере, locals() также включает request. Важно ли это для вас, зависит от вашего приложения и вашего уровня перфекционизма.

Подкаталоги в get_template()

Может стать неудобно хранить все шаблоны в одном каталоге. Вам может понадобиться хранить шаблоны в подкаталогах вашего каталога шаблонов, это нормально. Действительно, мы рекомендуем делать так. Более продвинутые особенности Django (такие как общая система представлений, которая будет описана в главе « Генерация данных, отличных от HTML ») ожидают такое расположение шаблонов по умолчанию.

Хранение шаблонов в подкаталогах несложное занятие. В вашем вызове get_template(), просто включите имя подкаталога и слэш перед именем шаблона, вот так:

t = get_template('dateapp/current_datetime.html')

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

Нет ограничений на глубину дерева подкаталогов. Используйте столько сколько нужно.

Замечание

Пользователи Windows должны использовать прямой слэш (/) вместо обратного (\). Функция get_template() ожидает прямые слэши в имени файла с шаблоном.

Шаблонный тег include

После того как мы описали механизм загрузки шаблонов, мы можем рассказать о встроенном шаблонном теге, который использует его: {% include %}. Этот тег позволяет вам подключать содержимое другого шаблона. Аргументом тега является путь к подключаемому шаблону. Имя шаблона может быть представлено как переменной, так и строкой, заключённой в двойные или одинарные кавычки. Если вы используете один и тот же код в нескольких шаблонах, рассмотрите возможность использования тега {% include %}, чтобы исключить дублирование кода.

Следующий пример дважды подключает содержимое шаблона nav.html. Строки примера эквивалентны, несмотря на разницу используемых кавычек:

{% include 'nav.html' %}
{% include "nav.html" %}

Следующий пример показывает как следует подключать содержимое шаблона includes/nav.html:

{% include 'includes/nav.html' %}

Следующий пример подключает содержимое шаблона, имя которого содержится в переменной template_name:

{% include template_name %}

Подобно функции get_template(), имя файла для шаблона определяется с использованием каталога для шаблонов из параметра конфигурации TEMPLATE_DIRS.

Подключенные шаблоны обрабатываются в контексте шаблона, который их подключил. Например, рассмотрим два шаблона:

# mypage.html

<html>
<body>
{% include "includes/nav.html" %}
<h1>{{ title }}</h1>
</body>
</html>

# includes/nav.html

<div id="nav">
    You are in: {{ current_section }}
</div>

При обработке шаблона mypage.html с контекстом, в котором определена переменная current_section, тогда переменная будет доступна в «подключенном» шаблоне, как вы могли ожидать.

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

Если параметр конфигурации DEBUG имеет значение True, вы увидите исключение TemplateDoesNotExist на странице с ошибкой.

Если параметр конфигурации DEBUG имеет значение False, вы не увидите никакой ошибки, просто в месте подключаемого шаблона ничего не отобразится.

Наследование шаблонов

До этого момента наши примеры шаблонов содержали крошечные фрагменты HTML, но в реальном проекте, вы будете использовать шаблонную систему Django для создания всех страниц проекта. Это приводит к общей проблеме веб разработки — рассматривая весь сайт, как уменьшить повторение и избыточность общих областей страниц, таких как навигация по сайту?

Классическим методом решения этой задачи является использование кода на серверной стороне — команд, которые вы можете добавлять в ваши HTML страницы, «подключая» одну страницу в другую. Действительно, Django предоставляет поддержку этого метода с помощью вышеописанного тега {% include %}. Но более предпочтительным методом решения такой задачи с помощью Django является использование элегантной стратегии под названием наследование шаблонов.

В сущности, наследование шаблонов позволяет создать основной «скелетный» шаблон, который содержит все общие части вашего сайта и определить «блоки», которые могут быть заменены шаблонами-наследниками.

Давайте рассмотрим такой пример, создав более сложный шаблон для представления current_datetime. Отредактируем его:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
    <title>The current time</title>
</head>
<body>
    <h1>My helpful timestamp site</h1>
    <p>It is now {{ current_date }}.</p>

    <hr>
    <p>Thanks for visiting my site.</p>
</body>
</html>

Выглядит неплохо, но что случится, когда нам понадобится создать другое представление, скажем, hours_ahead из главы « Представления и привязки URL ». Если мы снова желаем получить приятный, правильный, полный HTML шаблон, нам потребуется создать нечто такое:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
    <title>Future time</title>
</head>
<body>
    <h1>My helpful timestamp site</h1>
    <p>In {{ hour_offset }} hour(s), it will be {{ next_time }}.</p>

    <hr>
    <p>Thanks for visiting my site.</p>
</body>
</html>

Очевидно, что мы просто скопировали большую часть HTML. Представьте, что у нас более однородный сайт, включая навигационную панель, несколько стилей, возможно, немного JavaScript — нам потребуется использовать повторяющийся HTML в каждом шаблоне.

Решение этой проблемы с помощью подключения шаблонов на серверной стороне — выделение общих кусков в обоих шаблонах и сохранение их в отдельных шаблонах, которые затем включаются в каждый шаблон. Возможно, вы сохранили верхнюю часть шаблона в файле header.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>

И, возможно, вы сохранили нижнюю часть в файле footer.html:

    <hr>
    <p>Thanks for visiting my site.</p>
</body>
</html>

С помощью стратегии использования включений шаблонов, работать с заголовками и окончаниями страниц легко. Но подход неряшлив. В данном примере, у обеих страниц есть тег <title> — <h1>My helpful timestamp</h1> — но мы не можем поместить его в header.html, т.к. содержимое тегов различно. Если мы включим тег <h1> в заголовок, нам потребуется включить и <title>, что не позволит нам настраивать его содержимое для каждой страницы. Видите, куда это нас ведёт?

Система наследования шаблонов Django решает такие проблемы. Вы можете рассматривать её как версию с «вынесением различий» шаблонов на серверной стороне. Вместо определения общих частей шаблона, вы определяете различающиеся части.

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

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <h1>My helpful timestamp site</h1>
    {% block content %}{% endblock %}
    {% block footer %}
    <hr>
    <p>Thanks for visiting my site.</p>
    {% endblock %}
</body>
</html>

Этот шаблон, который мы назвали base.html, определяет основу HTML документа, которую мы будем использовать для каждой страницы на сайте. Это уже задача дочерних шаблонов заполнить или добавить, или не трогать содержимое этих блоков. (Если вы следуете за нашим рассказом, сохраните этот файл в каталог для шаблонов.)

Здесь мы используем тег, который раньше не определяли: {% block %}. Всё, что такие теги делают — указывают шаблонной системе, что дочерние шаблоны могут переопределять эту часть основного шаблона.

Имея основной шаблон, мы можем внести изменения в существующий current_datetime.html, чтобы использовать его:

{% extends "base.html" %}

{% block title %}The current time{% endblock %}

{% block content %}
<p>It is now {{ current_date }}.</p>
{% endblock %}

Теперь, давайте создадим шаблон для представления hours_ahead из главы « Представления и привязки URL ». (Если вы читаете, изменяя код, мы оставим на вас изменение кода hours_ahead для использования шаблонной системы.) Вот как он должен выглядеть:

{% extends "base.html" %}

{% block title %}Future time{% endblock %}

{% block content %}
<p>In {{ hour_offset }} hour(s), it will be {{ next_time }}.</p>
{% endblock %}

Разве не прекрасно? Каждый шаблон содержит только уникальный код. Нет никакой избыточности. Если вам потребуется внести глобальные изменения в сайт, просто измените содержимое шаблона base.html и все остальные шаблоны немедленно отразят ваши изменения.

Вот так это работает. При загрузке шаблона current_datetime.html, шаблонная система видит тег {% extends %}, что означает, что перед ней дочерний шаблон. Система немедленно загружает базовый шаблон, в данном случае, base.html.

После этого, шаблонная система находит три тега {% block %} в base.html и заменяет эти блоки содержимым дочернего шаблона. Таким образом, заголовок, который мы определили в {% block title %} дочернего шаблона, будет использован вместо блока {% block title %} базового шаблона, а текст, определённый в {% block content %} — вместо блока {% block content %}.

Следует отметить, что раз дочерний шаблон не определяет блок footer, шаблонная система вместо этого использует значение из базового шаблона. Содержимое внутри тега {% block %} в основном шаблоне всегда используется в качестве запасного.

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

Вы можете использовать столько уровней наследования, сколько необходимо. Одним из общих способов использования наследования является следующий трёх-уровневый подход:

Создать шаблон base.html, который содержит основную информацию о дизайне сайта. Эта часть изменяется очень редко, если вообще изменяется.

Создать шаблон base_SECTION.html для каждого «раздела» вашего сайта (т.е. base_photos.html и base_forum.html). Эти шаблоны расширяют base.html и включают стили и дизайн для конкретного раздела.

Создать индивидуальные шаблоны для каждого типа страниц, таких как страница форума или фотогалереи. Эти шаблоны расширяют определённые разделы шаблона.

Этот подход увеличивает повторное использование кода и упрощает добавление элементов к разделяемым частям сайта, например, к навигации.

Ниже приведены некоторые советы для работы с наследованием шаблонов:

Если вы используете {% extends %} в шаблоне, он должен быть первым тегом в этом шаблоне. В противном случае, наследование шаблонов работать не будет.

В общем случае, чем больше тегов {% block %} в основном шаблоне, тем лучше. Запомните, дочерние шаблоны не обязаны определять все блоки основного шаблона. Так что вы можете указать разумные значения по умолчанию в ряде блоков, а затем определить в дочернем шаблоне только те, которые надо изменить. Лучше иметь больше обработчиков.

Если вы заметили, что повторяете код в ряде шаблонов, возможно вам надо перенести этот код в {% block %} в основном шаблоне.

Если вам надо получить содержимое блока из основного шаблона, переменная {{ block.super }} поможет с этим. Это полезно, если вам потребуется лишь добавить данные в блок, вместо его полной замены.

Вы не можете определять множество тегов {% block %} с одинаковым именем в одном шаблоне. Это ограничение существует по причине того, что эти теги работают в «обоих» направлениях. То есть, этот тег не просто предоставляет место для заполнения данными, он также определяет содержимое, которое заполняет место в основном шаблоне. Если бы было два одинаково названных тега {% block %} в шаблоне, то основной шаблон бы не знал, какое содержимое блока использовать.

Шаблон, имя которого вы передаёте в {% extends %} загружается тем же способом, что и при использовании get_template(). То есть, имя шаблона добавляется к содержимому параметра TEMPLATE_DIRS.

В большинстве случаев, аргументом для тега {% extends %} будет строка. Но может быть и переменная, если вы не знаете имя основного шаблона до запуска приложения. Это позволяет вам реализовывать динамические вещи.

Глава 5. Модели

Перевод © Попов Руслан <ruslan.popov • gmail>

В главе « Представления и привязки URL » мы описали основы создания динамических сайтов с помощью Django: настройка представлений и схемы URL для них. Как было рассказано, представление отвечает за некоторую произвольную логику и возвращает отклик. В одном из примеров логикой являлось вычисление текущей даты и времени.

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

Множество сложных сайтов предоставляют некую комбинацию этих двух вариантов. Например, http://www.amazon.com/ является отличным примером такого сайта. Каждая страница продукта, по существу, является запросом в базу данных продуктов Amazon, отформатированном в виде HTML. А когда вы отсылаете свой комментарий, он помещается в базу данных отзывов.

Django отлично подходит для создания сайтов, ориентированных на использование совместно с базой данных, т.к., она поставляется с простыми, но мощными механизмами выполнения запросов к базе данных с помощью Python. Эта глава описывает этот механизм: слой Django для работы с базой данных.

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

«Тупой» способ выполнения запросов в представлениях

Подобно определённому в главе « Представления и привязки URL » «тупому» способу генерации вывода с помощью представлений (вбивание руками текста прямо в код представления), существует «тупой» способ получения в представлениях информации из базы данных. Это просто: используйте любую существующую библиотеку языка Python для выполнения SQL запроса и обрабатывайте его результаты.

В этом примере представления мы используем библиотеку MySQLdb (доступную по адресу http://www.djangoproject.com/r/python-mysql/) для подключения к базе данных MySQL, получения нескольких записей и помещения их в шаблон для отображения на странице сайта:

from django.shortcuts import render_to_response
import MySQLdb

def book_list(request):
    db = MySQLdb.connect(user='me', db='mydb', passwd='secret', host='localhost')
    cursor = db.cursor()
    cursor.execute('SELECT name FROM books ORDER BY name')
    names = [row[0] for row in cursor.fetchall()]
    db.close()
    return render_to_response('book_list.html', {'names': names})

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

Мы жёстко определяем параметры соединения с базой данных. В идеале эти параметры должны храниться в конфигурации проекта Django.

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

Это привязывает нас к MySQL. Если, с течением времени, мы решим перейти с MySQL на PostgreSQL, нам потребуется использовать другой драйвер для базы данных (т.е., psycopg вместо MySQLdb), изменить параметры соединения и, в зависимости от природы SQL операторов, возможно, переписать SQL запросы. В идеале, мы должны рассматривать сервер базы данных абстрактно, т.е., для смены сервера мы должны внести изменения в одно только место проекта. (Эта проблема имеет особое значение в случае, если вы работаете над приложением Django с открытым кодом, которое вы желаете распространить среди максимально возможного количества пользователей.)

Как вы можете ожидать, слой Django для работы с базами данных помогает решать такие проблемы. Далее представлен пример как надо изменить предыдущее представление для использования Django API для работы с базами данных:

from django.shortcuts import render_to_response
from mysite.books.models import Book

def book_list(request):
    books = Book.objects.order_by('name')
    return render_to_response('book_list.html', {'books': books})

Мы разберём этот код немного позже в этой главе.

Методика MTV (или MVC)

Прежде чем мы погрузимся в изучение кода, давайте рассмотрим общий дизайн БД ориентированных веб-приложений Django.

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

Эти три вещи вместе — логика доступа к данным, бизнес-логика и логика отображения — составляют концепцию, которую называют шаблоном Модель-Представление-Управление (Model-View-Controller, MVC) архитектуры программного обеспечения. В этой концепции термин «Модель» относится к логике доступа к данным; термин «Представление» относится к той части системы, которая определяет, что показать и как; а термин «Управление» относится к той части системы, которая определяет какое представление надо использовать, в зависимости от пользовательского ввода, по необходимости получая доступ к модели.

Почему используется сокращение?

Целью чёткого определения сокращений, подобных MVC, является упорядочивание взаимодействия между разработчиками. Вместо того, чтобы сказать вашим сотрудникам: «Давайте использовать абстрактный доступ к данным, затем создадим слой управления отображением данных и тогда создадим слой между ними, который всем этим управляет» можно воспользоваться общим термином: «Давайте здесь использовать подход MVC».

Django следует модели MVC достаточно близко, т.е., может быть назван MVC совместимой средой разработки. Вот примерно как M, V и C используются в Django:

M, доступ к данным, обрабатывается слоем работы с базой данных, который описан в этой главе.

V, эта часть, которая определяет какие данные получать и как их отображать, обрабатывается представлениями и шаблонами.

C, эта часть, которая выбирает представление в зависимости от пользовательского ввода, обрабатывается самой средой разработки, следуя созданной вами схемой URL, и вызывает соответствующую функцию Python для указанного URL.

Так как «C» обрабатывается средой разработки и всё интересное в Django происходит в моделях, шаблонах и представлениях, на Django ссылаются как на MTV-ориентированную среду разработки. В MTV-подходе к разработке:

M определено для «Модели» (Model), слоя доступа к данным. Этот слой знает всё о данных: как получить к ним доступ, как проверить их, как с ними работать и как данные связаны между собой.

T определено для «Шаблона» (Template), слоя представления данных. Этот слой принимает решения относительно представления данных: как и что должно отображаться на странице или в другом типе документа.

V определено для «Представления» (View), слоя бизнес-логики. Этот слой содержит логику, как получать доступ к моделям и применять соответствующий шаблон. Вы можете рассматривать его как мост между моделями и шаблонами.

Если вам приходилось работать с другими MVC ориентированными средами разработки, такими как Ruby on Rails, вы можете рассматривать представления в Django как «контролёры», а шаблоны Django — как «представления». Это печальная путаница возникла в результате различных толкований MVC. В интерпретации Django «представление» описывает данные, которые будут представлены пользователю. Неважно как эти данные будут выглядеть, важно какие данные. Напротив, в Ruby on Rails и подобных ему средах предполагается, что в работу контролёра включено принятие решения, какие данные будут представлены пользователю, в то время как представление точно определяет как эти данные будут выглядеть, а не какие данные будут представлены.

Ни одна интерпретация не имеет преимуществ над другой. Важно понимать основную концепцию.

Настройка базы данных

Учитывая вышеописанную философию, начнём исследовать слой Django для работы с базой данных. Первое, что нам понадобится — необходимо позаботиться о начальной настройке. Необходимо указать Django какой сервер базы данных использовать и как к нему подключаться.

Используем кодировку UTF-8, пример для MySQL (прим. переводчика)

Откроем на редактирование файл /etc/mysql/my.cnf.

В конец секции [client] добавим строчку:

default-character-set=utf8

В конец секции [mysqld] добавим строчки:

default-character-set=utf8
collation_server=utf8_unicode_ci

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

Проверка настроек кодировки (прим. переводчика)

В результате вышеописанных действий вы должны получить:

mysql> show variables like 'coll%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_unicode_ci |
| collation_server     | utf8_unicode_ci |
+----------------------+-----------------+
3 rows in set (0.00 sec)

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

Мы предполагаем, что вы уже установили сервер базы данных, запустили его и создали базу данных внутри. Пример для MySQL:

CREATE USER user@localhost IDENTIFIED BY "topsecret";
CREATE DATABASE mysite;
GRANT ALL ON mysite.* TO user@localhost;

Использование SQLite является особым случаем: не требуется создавать базу данных, так как SQLite использует файлы на файловой системе для хранения своих данных.

Подобно параметру TEMPLATE_DIRS из предыдущей главы, по умолчанию параметры соединения с базой данных определяются в файле конфигурации проекта, settings.py. Отредактируйте этот файл и посмотрите на настройки базы данных:

DATABASE_ENGINE = ''
DATABASE_NAME = ''
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

Опишем каждый параметр:

Параметр DATABASE_ENGINE указывает тип базы данных. Может быть одним из показанных в таблице « Настройки драйвера базы данных ».

Таблица 5.1. Настройки драйвера базы данных

Значение | База данных | Драйвер
postgresql | PostgreSQL | psycopg версия 1.х, http://www.djangoproject.com/r/python-pgsql/1/
postgresql_psycopg2 | PostgreSQL | psycopg версия 2.х, http://www.djangoproject.com/r/python-pgsql/
mysql | MySQL | MySQLdb, http://www.djangoproject.com/r/python-mysql/
sqlite3 | SQLite | При использовании Python 2.5+ драйвер не нужен. В противном случае http://www.djangoproject.com/r/python-sqlite/
oracle | Oracle | cx_Oracle, http://www.djangoproject.com/r/python-oracle/

Следует отметить, что для работы с базой данных потребуется скачать и установить соответствующий драйвер. Каждый описанный драйвер доступен бесплатно, просто проследуйте по ссылке, которая приведена в колонке «Драйвер». Если вы используете Linux, вероятно ваша система управления пакетами может предложить соответствующие пакеты. (Например, ищите пакеты с именами python-postgresql или python-psycopg.)

Пример:

DATABASE_ENGINE = 'postgresql_psycopg2'

Параметр DATABASE_NAME указывает Django имя вашей базы данных. Например:

DATABASE_NAME = 'mydb'

Если вы использует SQLite, укажите полный путь к файлу базы данных на файловой системе. Например:

DATABASE_NAME = '/home/django/mydata.db'

Мы используем каталог /home/django в этом примере, но вам следует выбирать каталог, который более подходит для вашей задачи.

Параметр DATABASE_USER указывает Django какое имя надо использовать при соединении с базой данных. Если вы используете SQLite, оставьте этот параметр пустым.

Параметр DATABASE_PASSWORD указывает Django какой пароль надо использовать при соединении с базой данных. Если вы используете SQLite, оставьте этот параметр пустым.

Параметр DATABASE_HOST указывает Django какое имя сервера надо использовать при соединении с базой данных. Если база данных находится на том же компьютере (т.е., localhost), оставьте параметр пустым. Если вы используете SQLite, оставьте этот параметр пустым.

MySQL является особым случаем. Если значение этого параметра начинается с прямого слэша (/) и вы используете MySQL, то соединение будет произведено через UNIX сокет, например:

DATABASE_HOST = '/var/run/mysql'

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

Параметр DATABASE_PORT указывает Django какой порт надо использовать при соединении с базой данных. Если вы используете SQLite, оставьте этот параметр пустым. В противном случае, соответствующий драйвер базы данных будет использовать стандартный (для конкретной базы данных) порт. В большинстве случаев используется стандартный порт, можете оставить этот параметр пустым.

После заполнения этих параметров, проверьте свою конфигурацию. Чтобы выполнить это, запустите python manage.py shell, как делали это в прошлой главе, в каталоге проекта mysite. (Как мы рассказывали в прошлой главе команда manage.py shell является способом запуска интерпретатора Python с корректной конфигурацией Django проекта. Это необходимо в нашем случае, так как Django должна знать какой файл конфигурации следует использовать для получения учётных данных для работы с базой данных.)

В интерпретаторе выполните нижеприведённые команды для проверки ваших настроек для соединения с базой данных:

>>> from django.db import connection
>>> cursor = connection.cursor()

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

Таблица 5.2. Сообщения об ошибках в конфигурации доступа к базе данных

Сообщение об ошибке | Решение
Вы не определили значение параметра DATABASE_ENGINE. (You haven't set the DATABASE_ENGINE setting yet.) | Установите значение параметра DATABASE_ENGINE. Не оставляйте его пустым.
Не определена переменная среды DJANGO_SETTINGS_MODULE. (Environment variable DJANGO_SETTINGS_MODULE is undefined.) | Запустите команду python manage.py shell вместо python.
Ошибка при загрузке модуля _____. Нет модуля с именем _____. (Error loading _____ module: No module named _____.) | Вы не установили соответствующий драйвер базы данных, т.е. psycopg или MySQLdb.
_____ не является доступным драйвером базы данных. (_____ isn't an available database backend. | Установите параметр DATABASE_ENGINE, указав используемую базу данных, как было описано ранее. Возможно вы опечатались?
База данных _____ не существует. (database _____ does not exist.) | Измените параметр DATABASE_NAME, чтобы он указывал на существующую базу данных, или выполните соответствующую команду CREATE DATABASE для создания базы данных.
Роль _____ не существует. (role _____ does not exist.) | Измените параметр DATABASE_USER, чтобы он указывал на существующего пользователя или создайте соответствующего пользователя в базе данных.
Не могу подключиться к серверу. (could not connect to server.) | Удостоверьтесь, что параметры DATABASE_HOST и DATABASE_PORT установлены правильно, а также, что сервер баз данных работает.

Ваше первое приложение

После того, как вы проверили работоспособность соединения с базой данных, пришло время создать Django-приложение — код, включающий в себя модели и представления, который объединён в один пакет Python и представляет собой цельное Django-приложение.

Удобно согласовать терминологию здесь, это поможет новичкам. В главе « Новый проект » мы создали проект, но в чём же разница между проектом и приложением? Разница в том, что первое является конфигурацией, а второе — кодом:

Проект — это экземпляр определённого набора кода Django-приложений и конфигурация для этих приложений.

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

Приложение — это переносимый набор некой функциональности, обычно включает в себя модели и представления, которые хранятся вместе в едином пакете языка Python.

Например, Django поставляется с рядом приложений, таких как система комментирования и автоматический интерфейс администратора. Важной особенностью этих приложений является то, что они переносимы и их можно использовать во множестве проектов.

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

В действительности нет нужды создавать приложения вообще, как это видно из примера функций представления, которые мы создали ранее. Для тех случаев мы просто создали файл с именем views.py, который содержал код функций представления и создали схему URL для этих функций. Никакое «приложение» нам для этого не понадобилось.

Тем не менее, существует одно требование относящееся к приложениям: если вы используете Django API для работы с базой данных, то вы обязаны создать приложение. Модели должны находиться внутри приложения. Следовательно, для того, чтобы начать описывать наши модели нам потребуется создать новое приложение.

Внутри каталога проекта mysite выполните эту команду для создания приложения books:

python manage.py startapp books

Эта команда ничего не выводит, она просто создаёт каталог books в каталоге mysite. Давайте глянем на содержимое этого каталога:

books/
    __init__.py
    models.py
    views.py

Эти файлы будут содержать модели и представления для приложения.

Посмотрите с помощью вашего текстового редактора файлы models.py и views.py. Оба файла сейчас пустые, исключая импорт в models.py. Это каркас для вашего приложения.

Определение моделей в Python

Как мы рассказывали в этой главе ранее, буква «M» в MTV определяет слово «Модель» (Model). Модель в Django — это описание данных, которые хранятся в базе данных, выполненное в виде кода на языке Python. Это форма ваших данных — эквивалент SQL операторов CREATE TABLE — только описана она на языке Python вместо SQL и включает в себя не только определение столбцов в базе данных. Django использует модель для фонового выполнения SQL и возвращает удобные структуры Python с данными, представляющими записи в таблицах вашей базы данных. Django также использует модели для представления высокоуровневых концепций, которые SQL вряд ли сможет обработать.

Если вы уже работали с базами данных, вы могли подумать: «Зачем дублировать определение моделей данных в Python и в SQL?» Django действует таким образом по нескольким причинам:

Самодиагностика занимает ресурсы и несовершенна. Для того, чтобы предоставить удобный API для доступа к данным, Django должна как-то получить информацию о содержимом базы данных и существует два способа сделать это. Первый: можно явно описать данные в Python. Второй: самодиагностика базы данных при работе приложения для определения моделей данных.

Второй способ кажется более очевидным, так как метаданные в ваших таблицах хранятся в одном месте, но он приводит к нескольким проблемам. Первое, самодиагностика базы данных во время работы приложения потребляет ресурсы. Если среде разработки потребуется проверять структуру базы данных при выполнении каждого запроса, или хотя бы при каждом запуске веб-сервера, вы получите значительную нагрузку на ресурсы сервера. (В то время как некоторые считают такую нагрузку приемлемой, разработчики Django нацелены на максимально возможную скорость работы среды. И этот подход успешно решает задачу обгона конкурентов в тестах.) Во-вторых, некоторые базы данных, особенно старые версии MySQL, не сохраняют достаточно метаинформации для точной и полной самодиагностики.

Разрабатывать с помощью Python прикольно и хранение всего в виде его кода сокращает число «переключений контекста», которые приходится делать вашему мозгу. Если придерживаться единой среды разработки/менталитета, это повысит продуктивность работы. Необходимость писать SQL, затем код Python, а потом опять SQL, отрицательно сказывается на производительности разработчика.

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

SQL позволяет лишь определённый уровень хранения метаинформации о формате данных. Большинство систем управления базами данных не предоставляют специализированных типов данных для представления адресов электронной почты или URL. Модели Django предоставляют. Преимущество высокоуровневых типов данных в улучшении продуктивности и в большем повторном использовании кода.

SQL несовместим между разными платформами баз данных. При распространении веб-приложения, более практично передавать модуль на языке Python, который описывает формат ваших данных, вместо отдельных наборов операторов CREATE TABLE для MySQL, PostgreSQL и SQLite.

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

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

Ваша первая модель

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

Мы предположим следующие концепции, поля и взаимоотношения:

Автор имеет звание (господин, госпожа, товарищ), имя, фамилию, адрес электронной почты и фотографию.

Издатель имеет имя, адрес, город, область, страну и сайт.

Книга имеет название и дату издания. Она также имеет одного или нескольких авторов (с авторами отношение многие-ко-многим) и одного издателя (отношение один-к-многим, как внешний ключ, к издателям).

Первым шагом в использовании этой схемы базы данных будет её выражение в виде кода Python. В файл models.py, который был создан командой startapp, добавьте следующее:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

class Author(models.Model):
    salutation = models.CharField(max_length=10)
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='/tmp')

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

Быстро рассмотрим этот код, чтобы разобраться с основами. Первое, что следует отметить — каждая модель представлена в виде класса Python, который является потомком класса django.db.models.Model. Родительский класс, Model, содержит всю необходимую функциональность для обеспечения работы объектов с базой данных. Таким образом наши модели несут полную ответственность за определение полей в кратком понятном виде. Верите или нет, но это весь код, который необходим для получения простого доступа к данным с помощью Django.

Обычно каждая модель соответствует одной таблице в базе данных и, обычно, каждый атрибут модели соответствует столбцу в таблице. Имя атрибута модели соответствует имени столбца таблицы, а тип атрибута (т.е., CharField) соответствует типу столбца (т.е., varchar). Например, модель Publisher эквивалентна следующей таблице (в синтаксисе для PostgreSQL):

CREATE TABLE "books_publisher" (
    "id" serial NOT NULL PRIMARY KEY,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);

На самом деле Django может сгенерировать такой оператор CREATE TABLE автоматически, мы это скоро покажем.

Исключением из правила «один класс для одной таблицы» является случай отношения «многие-ко-многим». В нашем примере Book имеет такое поле — authors. Это означает, что книга имеет одного или более авторов, но таблица Book не получает такого столбца, Django создаёт дополнительную таблицу, которая применяется для обеспечения этой функциональности.

Полный список типов полей и опций синтаксиса моделей приведены в Приложении « Справочник определений модели ».

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

Установка модели

Мы написали код. Давайте создадим таблицы в нашей базе данных. Чтобы это сделать мы должны активировать модели в нашем проекте. Это делается с помощью добавления приложения books к списку установленных приложений в файле параметров.

Откройте файл settings.py снова и посмотрите на параметр INSTALLED_APPS. Этот параметр указывает Django какие приложения активированы для текущего проекта. По умолчанию, параметр выглядит так:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
)

Временно закомментируйте все четыре строки с помощью символа #. (Эти приложения были активированы для удобства, но мы самостоятельно активируем и обсудим их позже.) Не закрывая файл, измените параметры MIDDLEWARE_CLASSES и TEMPLATE_CONTEXT_PROCESSORS. Эти параметры зависели от приложений, которые мы закомментировали. Теперь добавьте mysite.books в список INSTALLED_APPS:

MIDDLEWARE_CLASSES = (
#    'django.middleware.common.CommonMiddleware',
#    'django.contrib.sessions.middleware.SessionMiddleware',
#    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

INSTALLED_APPS = (
    #'django.contrib.auth',
    #'django.contrib.contenttypes',
    #'django.contrib.sessions',
    #'django.contrib.sites',
    'mysite.books',
)

(Раз мы здесь работаем с одно-элементным кортежем, не забудьте о завершающей запятой. Между прочим, авторы этой книги предпочитают ставить запятую после каждого элемента в кортеже, независимо от того, что кортеж имеет единственный элемент. Это помогает избежать проблемы забытых запятых и никак не влияет на производительность.)

Элемент mysite.books ссылается на приложение books, над которым мы работаем. Каждое приложение, перечисленное в INSTALLED_APPS, представлено полным путём к нему — он, путь к пакетам, разделён точками.

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

python manage.py validate

Команда validate проверяет корректность синтаксиса и логики моделей. Если всё в порядке, вы увидите сообщение «0 errors found». Если нет, удостоверьтесь, что вы корректно набрали код модели. Вывод ошибки должен дать вам полезную информацию об ошибке в коде.

Каждый раз когда вам кажется, что есть проблемы с вашими моделями, используйте команду python manage.py validate. Она поможет выявить все стандартные проблемы.

Если ваши модели не имеют ошибок, выполните следующую команду, чтобы Django сгенерировало операторы CREATE TABLE для моделей приложения books (с полной подсветкой синтаксиса, если вы используете Unix):

python manage.py sqlall books

В данной команде books является именем приложения. Это имя вы указывали при выполнении команды manage.py startapp. После запуска команды вы должны увидеть нечто подобное:

BEGIN;
CREATE TABLE "books_publisher" (
    "id" serial NOT NULL PRIMARY KEY,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);
CREATE TABLE "books_book" (
    "id" serial NOT NULL PRIMARY KEY,
    "title" varchar(100) NOT NULL,
    "publisher_id" integer NOT NULL REFERENCES "books_publisher" ("id"),
    "publication_date" date NOT NULL
);
CREATE TABLE "books_author" (
    "id" serial NOT NULL PRIMARY KEY,
    "salutation" varchar(10) NOT NULL,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(40) NOT NULL,
    "email" varchar(75) NOT NULL,
    "headshot" varchar(100) NOT NULL
);
CREATE TABLE "books_book_authors" (
    "id" serial NOT NULL PRIMARY KEY,
    "book_id" integer NOT NULL REFERENCES "books_book" ("id"),
    "author_id" integer NOT NULL REFERENCES "books_author" ("id"),
    UNIQUE ("book_id", "author_id")
);
CREATE INDEX books_book_publisher_id ON "books_book" ("publisher_id");
COMMIT;

Отметим следующее:

Имена таблиц генерируются автоматически, комбинируется имя приложения (books) и имя модели в нижнем регистре (publisher, book и author). Вы можете изменить это поведение, подробности смотрите в приложении « Справочник определений модели ».

Как мы уже упоминали, Django автоматически добавляет первичный ключ для каждой таблицы — поле id. Вы можете изменить и это.

По правилу Django добавляет «_id» к имени поля внешнего ключа. Как вы можете предположить, вы можете изменить и это поведение.

Привязка внешнего ключа явно определяется с помощью оператора REFERENCES.

Операторы CREATE TABLE адаптируются к используемой базе данных, таким образом специфичные типы полей, такие как auto_increment (MySQL), serial (PostgreSQL) или integer primary key (SQLite) обрабатываются автоматически. То же самое происходит с экранированием имён полей (т.е., используются двойные или одинарные кавычки). В данном примере приведён синтаксис для PostgreSQL.

Команда sqlall не создаёт таблицы и никак не воздействует на базу данных — она просто выводит SQL операторы на экран и вы можете посмотреть, что будет передано в базу данных. Если вы пожелаете, можно вручную скопировать эти операторы в командную строку клиента базы данных или перенаправить данные в клиент напрямую. Тем не менее, Django предоставляет простой способ выполнения SQL в базе данных. Выполните команду syncdb:

python manage.py syncdb

Вы увидите подобное:

Creating table books_publisher
Creating table books_book
Creating table books_author
Installing index for books.Book model

Команда syncdb просто синхронизирует ваши модели в базе данных. Она просматривает все модели каждого приложения, указанного в параметре INSTALLED_APPS, проверяет наличие соответствующих таблиц в базе данных и по необходимости создаёт их. Следует отметить, что syncdb не синхронизирует изменения в моделях или удаления моделей. Если вы изменили модель или удалили её, то команда syncdb не поможет с внесением изменений в базу. (More on this in the “Making Changes to a Database Schema” section toward the end of this chapter. FIXME)

При повторном запуске команды python manage.py syncdb ничего не произойдёт, так как вы не добавили модели в приложение books или не добавили новое приложение в INSTALLED_APPS. Следовательно, безопасно запускать эту команду — она не испортит базу данных.

Если вам интересно, то изучите подробнее клиент вашей базы данных и просмотрите созданную базу данных. Вы можете вручную запустить клиент (т.е., psql для PostgreSQL) или вы можете применить команду python manage.py dbshell, которая запускает клиент, используя параметр DATABASE_ENGINE. Последний вариант более удобен.

Доступ к данным

После создания модели Django автоматически предоставляет высокоуровневый API для работы с этими моделями. Изучите его, запустив команду python manage.py shell и набрав следующее:

>>> from books.models import Publisher
>>> p1 = Publisher(name='Addison-Wesley', address='75 Arlington Street',
...     city='Boston', state_province='MA', country='U.S.A.',
...     website='http://www.apress.com/')
>>> p1.save()
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.',
...     city='Cambridge', state_province='MA', country='U.S.A.',
...     website='http://www.oreilly.com/')
>>> p2.save()
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Publisher object>, <Publisher: Publisher object>]

Эти несколько строк кода выполняют немного. Вот основное:

Во-первых, мы импортируем наш класс модели Publisher. Это позволит нам работать с таблицей базы данных, которая содержит информацию об издателях.

Мы создаём экземпляр этого класса, передав значения для каждого поля — name, address и так далее.

Чтобы сохранить объект в базе данных, воспользуйтесь методом save(). Django выполнит для этого SQL оператор INSERT.

Для того, чтобы получить информацию об издателях из базы данных, используйте атрибут Publisher.objects, который можно рассматривать как список всех издателей. Получаем список всех объектов Publisher из базы данных с помощью метода Publisher.objects.all(). Django выполнит SQL оператор SELECT.

Следует остановиться на одном моменте, если он не очевиден из примера. При создании объекта с помощью модели Django не сохраняет ничего в базе данных до тех пор, пока вы не воспользуетесь методом save():

p1 = Publisher(...)
# At this point, p1 is not saved to the database yet!
p1.save()
# Now it is.

Если вам требуется создать объект и сохранить его в базе данных единой операцией, используйте метод objects.create(). Следующий пример эквивалентен предыдущему:

>>> p1 = Publisher.objects.create(name='Apress',
...     address='2855 Telegraph Avenue',
...     city='Berkeley', state_province='CA', country='U.S.A.',
...     website='http://www.apress.com/')
>>> p2 = Publisher.objects.create(name="O'Reilly",
...     address='10 Fawcett St.', city='Cambridge',
...     state_province='MA', country='U.S.A.',
...     website='http://www.oreilly.com/')
>>> publisher_list = Publisher.objects.all()
>>> publisher_list

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

Добавляем строковое представление модели

Когда мы распечатали список издателей, всё что мы получили — бесполезный вывод, который затрудняет понимание:

[<Publisher: Publisher object>, <Publisher: Publisher object>]

Это можно легко исправить, добавив метод __unicode__() к нашему объекту Publisher. Метод __unicode__() указывает Python как отображать юникодное представление объекта. Вот так это используется в реальном приложении:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    def __unicode__(self):
        return self.name

class Author(models.Model):
    salutation = models.CharField(max_length=10)
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='/tmp')

    def __unicode__(self):
        return '%s %s' % (self.first_name, self.last_name)

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

    def __unicode__(self):
        return self.title

Как видно, метод __unicode__() может делать всё необходимое для того, чтобы возвратить строковое представление. В данном случае, методы __unicode__() для Publisher и Book просто возвращают имя объекта и заголовок, соответственно, но метод __unicode__() для Author более сложный — он объединяет поля first_name и last_name.

Единственное требование к методу __unicode__() — он должен возвращать объект Unicode. В противном случае, например, если метод возвращает целое число, Python вызовет исключение TypeError с сообщением «coercing to Unicode: need string or buffer, int found».

Объекты Unicode

Что представляют собой объекты Unicode?

Вы можете рассматривать объект Unicode в качестве обычной строки, которая может содержать миллион различных типов символов, от акцентированных версий латинских букв до различных символов экзотических языков.

Обычные строки в Python являются кодированными, это означает, что они используют кодировку: ASCII, ISO-8859-1 или UTF-8. Если вы помещаете в обычную строку экзотические символы (таковыми считаются любые символы, не попадающие в кодировку ASCII), вы должны помнить используемую кодировку, иначе на выходе вы получите непонятную мешанину символов. Проблемы возникают при попытках объединить данные в одной кодировке с данными в другой или при попытке отобразить эти данные в приложении, которое работает только с одной определённой кодировкой. Все мы встречались с сайтами или сообщениями электронной почты, которые содержали «??? ??????» или другие символы в нечётных позициях. Всё это показывает на проблему с кодировкой.

Однако объекты Unicode не имеют кодировки. Они используют целостный, универсальный набор символов, называемый «Unicode». При работе с объектами Unicode в Python, вы можете смешивать и сравнивать их, не беспокоясь о кодировке.

Django использует объекты Unicode везде. Объекты моделей выбираются в виде объектов Unicode, представления работают с Unicode данными, а шаблоны обрабатываются как Unicode. В общем, вам не надо беспокоиться о кодировках вообще. Всё должно работать.

Следует отметить то, что мы лишь кратко описали объекты Unicode. Вы можете самостоятельно продолжить их изучение. Хорошей точкой старта будет http://www.joelonsoftware.com/articles/Unicode.html.

Для того, чтобы эти изменения вступили в силу надо выйти из оболочки и запустить её снова командой python manage.py shell. (Это самый простой способ для активации изменений.) Теперь список объектов Publisher более понятен:

>>> from books.models import Publisher
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Addison-Wesley>, <Publisher: O'Reilly>]

Удостоверьтесь, что каждая определённая вами модель имеет метод __unicode__(). Это необходимо не только для вашего удобства, но и потому что Django использует результат вызова этого метода при отображении объектов.

Наконец, следует отметить, что __unicode__() является хорошим примером добавления нового поведения моделям. Модель Django описывает большее чем просто таблицу для объекта, она также описывает всю функциональность этого объекта. Метод __unicode__() является одним из примеров такой функциональности — модель знает как отображать себя.

Вставка и изменение данных

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

>>> p = Publisher(name='Apress',
...         address='2855 Telegraph Ave.',
...         city='Berkeley',
...         state_province='CA',
...         country='U.S.A.',
...         website='http://www.apress.com/')

Этот пример создания экземпляра модели не производит изменений в базе данных. Эта запись не сохраняется в базе данных пока не будет вызван метод save():

>>> p.save()

Вышеприведённые действия с точки зрения SQL операторов выглядят так:

INSERT INTO book_publisher
    (name, address, city, state_province, country, website)
VALUES
    ('Apress', '2855 Telegraph Ave.', 'Berkeley', 'CA',
     'U.S.A.', 'http://www.apress.com/');

Так как модель Publisher использует автоматически увеличивающийся первичный ключ id, первый вызов метода save() выполнит ещё и вычисление значения для первичного ключа и подставит это значение в атрибут id:

>>> p.id
52    # значение может отличаться для ваших данных

Последующие вызовы метода save() будут перезаписывать существующую запись, не создавая новую (т.е., будут выполнять SQL оператор UPDATE вместо INSERT):

>>> p.name = 'Apress Publishing'
>>> p.save()

В SQL это будет выглядеть так:

UPDATE book_publisher SET
    name = 'Apress Publishing',
    address = '2855 Telegraph Ave.',
    city = 'Berkeley',
    state_province = 'CA',
    country = 'U.S.A.',
    website = 'http://www.apress.com'
WHERE id = 52;

Да, следует отметить то, что все поля будут обновлены, а не только те, что были изменены. В зависимости от реализации вашего приложения, такое поведение может вызвать «гонки» (race condition). См « Изменение множества объектов одним запросом » далее о том, как выполнять подобные запросы:

UPDATE books_publisher SET
    name = 'Apress Publishing'
WHERE id=52;

Выборка объектов

Знание о том, как можно создавать и изменять данные достаточно, но есть вероятность, что ваше приложение будет гораздо чаще обращаться к существующим объектам, чем создавать новые. Мы уже знаем способ получения каждой записи определённой модели:

>>> Publisher.objects.all()
[<Publisher: Apress>, <Publisher: O'Reilly>]

В общих чертах в SQL это выглядит так:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher;

Замечание

Следует отметить, что Django не использует SELECT * при просмотре данных, а вместо этого явно перечисляет все поля. Так было задумано изначально: в некоторых случаях использование SELECT * может повлиять на производительность, а главное, перечисление полей более соответствует нормам Искусства программирования на языке Python: «Явное лучше неявного».

Для получения более подробной информации об Искусстве программирования на языке Python выполните в консоли Python команду import this.

Теперь внимательно рассмотрим строку Publisher.objects.all():

Сначала у нас есть определённая нами модель Publisher. Ничего удивительного: при необходимости получить доступ к данным вы используете модель.

Затем мы работаем с objects. Технически этот объект можно назвать менеджером. Менеджеры более подробно будут описаны в главе « Расширения для шаблонной системы ». Сейчас вам надо знать только то, что менеджеры отвечают за операции «табличного уровня» над данными, что самое главное, включая операции выборки данных.

Все модели автоматически получают менеджер objects. Он используется при каждом доступе к данным.

Наконец, у нас есть all(). Это метод объекта objects, который возвращает все записи, относящиеся к этой модели, из базы данных. Несмотря на то, что этот объект выглядит как список, в действительности это набор данных (QuerySet) — объект, который представляет собой набор записей из базы данных. Набор данных описан в приложении « Справочник по API взаимодействия с базой данных ». В оставшейся части этой главы мы будем рассматривать этот набор как список.

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

Фильтрация данных

Естественно, что достаточно редко требуется получать весь набор данных из модели единовременно. В большинстве случаев нам требуется обработать небольшую часть данных. Мы можем осуществить это с помощью метода filter():

>>> Publisher.objects.filter(name='Apress')
[<Publisher: Apress>]

Метод filter() принимает именованные аргументы, которые преобразуются в соответствующие SQL-условия WHERE. Предыдущий пример будет преобразован в нечто подобное:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE name = 'Apress';

Вы можете передать множество аргументов в filter() для сокращения диапазона выборки:

>>> Publisher.objects.filter(country="U.S.A.", state_province="CA")
[<Publisher: Apress>]

Множество аргументов будут объединены с помощью SQL операторов AND. Пример станет таким:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE country = 'U.S.A.'
AND state_province = 'CA';

Следует отметить, что по умолчанию выборка использует SQL оператор = для точного совпадения. Также можно выполнять другие виды запросов:

>>> Publisher.objects.filter(name__contains="press")
[<Publisher: Apress>]

Обратите внимание на двойное подчёркивание между name и contains. Подобно Python это используется Django для выполнения «магических» действий. В данном случае __contains преобразовывается в SQL оператор LIKE:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE name LIKE '%press%';

Доступны множество типов выборок, включая icontains (независящий от регистра LIKE), startswith и endswith, а также range (SQL оператор BETWEEN). В приложении « Справочник по API взаимодействия с базой данных » подробно описаны все типы выборок.

Получение одного объекта

Вышеприведённые примеры использования filter() всегда возвращают QuerySet, который можно использовать как список. Иногда более уместной является выборка единственного объекта. Для этого предназначен метод get():

>>> Publisher.objects.get(name="Apress")
<Publisher: Apress>

Вместо списка (т.е., набора данных QuerySet) возвращается только один объект. По этой причине, запрос ожидающий множество объектов вызовет исключение:

>>> Publisher.objects.get(country="U.S.A.")
Traceback (most recent call last):
    ...
MultipleObjectsReturned: get() returned more than one Publisher --
    it returned 2! Lookup parameters were {'country': 'U.S.A.'}

Запрос не возвращающий ни одного объекта тоже вызовет исключение:

>>> Publisher.objects.get(name="Penguin")
Traceback (most recent call last):
    ...
DoesNotExist: Publisher matching query does not exist.

Исключение DoesNotExist является атрибутом класса модели — Publisher.DoesNotExist. В вашем приложении вы можете перехватывать такие исключения:

try:
    p = Publisher.objects.get(name='Apress')
except Publisher.DoesNotExist:
    print "Apress isn't in the database yet."
else:
    print "Apress is in the database."

Сортировка данных

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

Очевидно, что для вашего приложения потребуется отсортировать результат по какому-нибудь значению, скажем, по алфавиту. Для этого использую метод order_by():

>>> Publisher.objects.order_by("name")
[<Publisher: Apress>, <Publisher: O'Reilly>]

Это не сильно отличается от примера с методом all(), но SQL представление теперь включает в себя оператор сортировки:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
ORDER BY name;

Мы можем производить сортировку по любому полю:

>>> Publisher.objects.order_by("address")
[<Publisher: O'Reilly>, <Publisher: Apress>]

>>> Publisher.objects.order_by("state_province")
[<Publisher: Apress>, <Publisher: O'Reilly>]

Для выполнения сортировки по множеству полей (когда второе поле используется для сортировки элементов, у которых первое поле совпадает), используют множество аргументов:

>>> Publisher.objects.order_by("state_province", "address")
 [<Publisher: Apress>, <Publisher: O'Reilly>]

Также мы можем указать необходимость выполнять обратную сортировку с помощью префикса -(минус) для имени поля:

>>> Publisher.objects.order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress>]

Несмотря на все возможности, постоянное использование order_by() может надоесть. Чаще всего сортировку надо производить по жёстко определённому полю. В этих случаях Django позволяет указать стандартную сортировку для модели:

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ["name"]

Здесь мы ввели новую концепцию: класс Meta, который внедрён в определение класса Publisher (обратите внимание на отступы). Вы можете использовать данный класс Meta с любой моделью для указания различных параметров, влияющих на поведение модели. Полный список данных параметров описан в приложении B FIXME, но сейчас мы рассматривали только параметр ordering. Если вы определите его, то пока не будет использован явный вызов метода order_by(), все объекты Publisher будут выдаваться отсортированными по полю name.

Формирование цепочки запросов

Вы знаете как фильтровать данные и как их сортировать. Вы желаете узнать как это делать одновременно? Для этого следует «сцепить» операторы вместе:

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress>]

Как вы можете предполагать, в SQL представлении это будет выглядеть так:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE country = 'U.S.A'
ORDER BY name DESC;

Количество таких цепочек никак не ограничено[8].

Выборка части результатов

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

>>> Publisher.objects.order_by('name')[0]
<Publisher: Apress>

Это преобразуется в:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
ORDER BY name
LIMIT 1;

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

>>> Publisher.objects.order_by('name')[0:2]

Этот код вернёт два объекта и будет преобразован в:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
ORDER BY name
OFFSET 0 LIMIT 2;

Следует отметить, что отрицательные значения не поддерживаются:

>>> Publisher.objects.order_by('name')[-1]
Traceback (most recent call last):
  ...
AssertionError: Negative indexing is not supported.

Но получить результат всё же возможно. Просто воспользуйтесь методом order_by():

>>> Publisher.objects.order_by('-name')[0]

Изменение множества объектов одним запросом

Мы упоминали в разделе « Вставка и изменение данных » о том, что метод save() обновляет все поля записи. Иногда полезно уметь обновлять только выделенный набор полей.

Например, нам требуется изменить в объекте Publisher поле name с «Apress» на «Apress Publishing». При использовании save() это выглядит так:

>>> p = Publisher.objects.get(name='Apress')
>>> p.name = 'Apress Publishing'
>>> p.save()

Это примерно преобразовывается в следующий SQL:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher
WHERE name = 'Apress';

UPDATE books_publisher SET
    name = 'Apress Publishing',
    address = '2855 Telegraph Ave.',
    city = 'Berkeley',
    state_province = 'CA',
    country = 'U.S.A.',
    website = 'http://www.apress.com'
WHERE id = 52;

(Следует отметить, что данный пример предполагает, что нужный нам объект имеет идентификатор 52.)

Вы можете видеть в данном примере, что метод save() установит значения для всех полей, не только для name. Если в вашем случае, остальные поля могут изменяться другими процессами, правильнее будет изменять только поля, которые этого требуют. Для этого следует использовать метод update() объектов QuerySet. Пример:

>>> Publisher.objects.filter(id=52).update(name='Apress Publishing')

SQL код будет более эффективным и не сможет вызвать «гонки»:

UPDATE books_publisher
SET name = 'Apress Publishing'
WHERE id = 52;

Метод update() имеется у каждого QuerySet и это означает, что вы можете изменять множество записей за раз. Вот так вы можете изменить в поле country значение «U.S.A.» на «USA» для каждой записи Publisher:

>>> Publisher.objects.all().filter(country='U.S.A.').update(country='USA')
2

Метод update() вернул значение — целое, представляющее количество изменённых записей.

[8] Оставил из предыдущей версии (прим. переводчика).

Удаление объектов

Для удаления объектов следует использовать метод delete():

>>> p = Publisher.objects.get(name="O'Reilly")
>>> p.delete()
>>> Publisher.objects.all()
[<Publisher: Apress Publishing>]

Вы также можете удалять сразу несколько объектов, для этого надо вызвать метод delete() объекта QuerySet. Эта методика аналогична использованию метода update(), описанному в предыдущем разделе:

>>> Publisher.objects.filter(country='USA').delete()
>>> Publisher.objects.all().delete()
>>> Publisher.objects.all()
[]

Будьте осторожны во время удаления своих данных! В качестве меры предосторожности против случайного удаления всех данных в таблице Django требует явного использования метода all() для удаления всех записей таблицы. Этот пример не сработает:

>>> Publisher.objects.delete()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Manager' object has no attribute 'delete'

Но сразу станет работать, если вы добавите метод all():

>>> Publisher.objects.all().delete()

Если вам потребуется удалить выделенный набор записей, в таком случае не требуется использовать метод all(). Повторим предыдущий пример:

>>> Publisher.objects.filter(country='USA').delete()

Глава 6. Интерфейс администратора Django

Перевод © Дмитрий aka Alerion <alerion.um • gmail>

Корректура © Попов Руслан <ruslan.popov • gmail>

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

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

Как Django предлагает решать эти скучные и однообразные задачи? Она делает всё за вас — всего несколькими строками кода, не более. С Django создание интерфейса администратора — решённая проблема.

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

Мы рекомендуем прочитать этот раздел, даже если вы не намерены использовать интерфейс администратора Django, потому что мы вводим несколько концепций, которые применимы ко всем компонентам Django.

Пакеты django.contrib

Автоматический интерфейс администратора является частью большой библиотеки кода, называемой django.contrib — часть кода Django, включающего различные полезные дополнения к ядру Django. Рассматривайте django.contrib для Django в качестве аналога стандартных библиотек Python — необязательный, но очень полезный функционал для решения общих задач. Он поставляется в комплекте с Django, поэтому вам не нужно заново «создавать велосипед» в ваших приложениях.

Интерфейс администратора является первой частью django.contrib, которую мы рассмотрим в книге. В общем, она называется django.contrib.admin. Другими частями django.contrib являются система авторизации (django.contrib.auth), поддержка анонимных сессий (django.contrib.sessions), а также систему пользовательских комментариев (django.contrib.comments). Изучая различные возможности django.contrib, вы становитесь экспертом в Django. Более подробно данный пакет будет рассмотрен в главе Интеграция с унаследованным . Просто имейте в виду, что Django поставляется со множеством интересных дополнений, которые в основном находятся в django.contrib.

Активация интерфейса администратора

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

Сначала внесите изменения в файле настроек:

Добавим django.contrib.admin в параметр INSTALLED_APPS. (Порядок элементов в INSTALLED_APPS не имеет значения, но мы советуем содержать их в алфавитном порядке, для удобства чтения.)

Убедитесь, что INSTALLED_APPS содержит django.contrib.auth, django.contrib.sessions и django.contrib.contenttypes. Интерфейс администратора требует подключения этих трёх пакетов. (Если вы следуете нашим инструкциям относительно проекта mysite, вспомните, что мы закомментировали эти три пакета в главе « Модели ». Раскомментируйте их сейчас.)

Убедитесь, что MIDDLEWARE_CLASSES содержит django.middleware.common.CommonMiddleware и django.contrib.sessions.middleware.SessionMiddleware а также django.contrib.auth.middleware.AuthenticationMiddleware. (И снова вам потребуется расскомментировать эти строчки для проекта mysite.)

Теперь выполните команду python manage.py syncdb. Она установит дополнительные таблицы в базу данных, которые будет использовать интерфейс администратора. При первом запуске syncdb при наличии django.contrib.auth в INSTALLED_APPS, у вас спросят о необходимости создания суперпользователя. Если вы его не создадите, то позже для его создания вам потребуется выполнить python manage.py createsuperuser. В противном случае вы не сможете войти в интерфейс администратора.

Теперь добавим административную часть в схему URL (в файл urls.py, помните?). По умолчанию, urls.py созданый с помощью django-admin.py startproject, содержит закомментированный код для административного интерфейса Django, и вам потребуется раскомментировать его. Вот, что там должно быть:

# Include these import statements...
from django.contrib import admin
admin.autodiscover()

# And include this URLpattern...
urlpatterns = patterns('',
    # ...
    (r'^admin/', include(admin.site.urls)),
    # ...
)

После этих несложных настроек, вы можете увидеть интерфейс администратора в действии. Запустите тестовый сервер (python manage.py runserver, как и в предыдущих главах) и перейдите по ссылке http://127.0.0.1:8000/admin/.

Использование интерфейса администратора

Интерфейс администратора предназначен для использования неквалифицированными пользователями и, следовательно, он должен быть самодостаточным. Тем не менее, сделаем несколько замечаний по свойствам интерфейса.

Первым, что вы увидите, будет экран с формой аутентификации, показанный на рисунке « Форма аутентификации администратора ».

Рисунок 6.1. Форма аутентификации администратора

Следует использовать логин и пароль, которые вы указали при создании администратора. Если вы не можете войти, убедитесь что вы на самом деле создали суперпользователя — попробуйте выполнить python manage.py createsuperuser.

После того, как вы войдёте в систему, вы увидите домашнюю страницу административного интерфейса. Эта страница отображает все возможные типы объектов, с которыми вы можете работать. Так как мы не активировали ни одной собственной модели, список выглядит пустым — он содержит только «Группы» и «Пользователей», которые по умолчанию доступны для редактирования.

Рисунок 6.2. Главная страница интерфейса администратора

Каждый тип обьектов в интерфейсе администратора обладает формой редактирования и списком обьектов. Список объектов отображает все доступные обьекты в базе данных, а форма редактирования позволяет добавлять, изменять и удалять конкретные записи.

Прочие языки

Если английский не является вашим основным языком, вы можете легко настроить интерфейс администратора на отображение информации на нужном вам языке. Для это в конфигурационном файле добавьте django.middleware.locale.LocaleMiddleware в MIDDLEWARE_CLASSES. Убедитесь что оно следует после django.contrib.sessions.middleware.SessionMiddleware.

После этого перегрузите главную страницу административного интерфейса. Если присутствует перевод для вашего языка, различные части интерфейса — от названий ссылок «Сменить пароль» и «Выйти» в верхней части страницы до «Группы» и «Пользователи» — отобразятся на вашем языке вместо английского. Django поставляется с переводом интерфейса на десятки языков.

Подробнее многоязычность в Django рассмотрена в главе « Безопасность ».

Нажмите на ссылку «Изменить» в строке «Пользователи», чтобы загрузить страницу со списком пользователей.

Рисунок 6.3. Стандартный вид списка изменений

Эта страница отображает всех пользователей в базе данных. Можно рассматривать этот список в качестве красивого аналога SQL-запроса SELECT * FROM auth_user;. Если вы следовали нашим указаниям, вы увидите только одного пользователя, но при добавлении новых, все они будут отображаться на этой странице. На странице вы найдёте такие полезные вещи как: фильтрация, сортировка и поиск. Варианты фильтрации находятся справа, сортировка доступна при нажатии на заголовок столбца, а поле поиска вверху позволяет искать пользователей по имени.

Нажмите на имя пользователя, которого вы создали, и вы увидете страницу редактирования.

Рисунок 6.4. Форма редактирования пользователя

Эта страница позволяет изменять параметры пользователя, такие как имя, фамилия, права и другие. (Для редактирования пароля вам следует перейти по ссылке «Изменить пароль» под полем пароля, вместо того, чтобы редактировать хэш). Следует отметить, что разные типы полей имеют разное отображение (виджеты) — например, поля даты/времени будут управляться через календарь, булево поле — чекбокс, текстовое поле — обычное поле ввода.

Вы можете удалить запись, нажав на кнопку «Удалить» в левом нижем углу на странице редактирования этой записи. Вы перейдёте на страницу подтверждения удаления, которая, в некоторых случаях, отобразит связанные обьекты, которые также будут удалены. (Например, если вы удалите издателя, книги этого издателя также будут удалены!).

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

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

Рисунок 6.5. Форма редактирования с ошибками

При редактировании существующего объекта обратите внимание на ссылку «История» в верхнем правом углу окна. Каждое изменение, произведённое через интерфейс администратора, сохраняется в журнале. Доступ к журналу изменений производится нажатием на ссылку «История».

Рисунок 6.6. Страница изменений объекта

Добавление ваших моделей на интерфейс администратора

Есть одна важная часть, которую мы ещё не сделали. Давайте добавим наши модели в интерфейс администратора, что бы мы могли добавлять, редактировать и удалять объекты в наших таблицах с помощью этого изящного интерфейса. Продолжим наш пример с книгами из главы « Модели », в которой мы создали три модели: Publisher, Author и Book.

В каталоге books (mysite/books) создайте файл admin.py и добавьте в него следущие строки:

from django.contrib import admin
from mysite.books.models import Publisher, Author, Book

admin.site.register(Publisher)
admin.site.register(Author)
admin.site.register(Book)

Этот код указывает Django предоставлять интерфейс для каждой из перечисленных моделей.

После этого перейдите на домашнюю страницу административной части (http://127.0.0.1:8000/admin/), вы должны увидеть раздел «Books» с ссылками на Authors, Books и Publishers. (Возможно потребуется остановить и запустить тестовый сервер, чтобы сделанные изменения вступили в силу.)

Теперь у вас есть полнофункциональный интерфейс администратора для этих моделей. Это было не так сложно!

Добавьте и отредактируйте записи для наполнения вашей базы данных данными. Если вы следовали примерам создания объектов Publisher, которые были даны в главе « Модели » (и не удалили их), вы увидите их записи на странице списка объектов модели.

Следует упомянуть то, что интерфейс администратора обрабатывает внешние ключи и связь многие-ко-многим, которые присутствуют в модели Book. Вспомним как выглядит модель Book:

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

    def __unicode__(self):
        return self.title

На странице «Добавить книгу» (http://127.0.0.1:8000/admin/books/book/add/), издатель (ForeignKey) представлен выпадающим списком, а автор (ManyToManyField) представлен выпадающим списком с множественным выбором. Рядом с этими полями находится иконка зелёного цвета с изображением плюса, которая позволяет добавлять связаные обьекты соответствующего типа. Например, если нажать на иконку возле поля «Издатель», появится дополнительное окно, позволяющее добавить издателя. После того, как вы успешно добавите издателя в этом окне, в форме добавления книги добавится созданый издатель. Красиво.

Как работает интерфейс администратора

Как же работает интерфейс администратора? Всё очень просто.

Когда Django загружает схему URL из urls.py при старте сервера, выполняется функция admin.autodiscover(), которую мы добавили для активации интерфейса администрации. Эта функция проходит по элементам параметра INSTALLED_APPS и проверяет наличие admin.py в каждом установленом приложении. Если admin.py присутствует, выполняется его код.

В admin.py нашего приложения books, каждый вызов admin.site.register() регистрирует указаную модель в интерфейсе администратора, предоставляя возможность управлять содержимым модели.

Приложение django.contrib.auth имеет свой собственный файл admin.py — именно поэтому «Пользователи» и «Группы» автоматически отображаются на интерфейсе администратора. Другие приложения django.contrib, например django.contrib.redirects также автоматически добавляют свои модели в интерфейс администратора, так же как и множество сторонних приложений Django, которые вы можете загрузить из интернета.

Несмотря на всё это, административный интерфейс является всего навсего простым приложением Django, со своими собственными моделями, шаблонами, представлениями и схемой URL. Вы добавляете его в свой проект через подключение в схему URL, аналогично добавлению своих представлений. Вы можете изучить его шаблоны, представления и схему URL, просмотрев в каталог django/contrib/admin вашей копии кода Django, но не пытайтесь поменять что-то непосредственно там, есть много возможностей настроить интерфейс администратора. (Если вы решили изучить приложение административного интерфейса, учтите, что оно производит достаточно сложные операции с метаданными моделей, поэтому понадобится много времени, чтобы разобраться в нём.)

Делаем поля необязательными

После того, как вы познакомились с интерфейсом администратора, вы наверное заметили ограничение — форма редактирования требует, что бы каждое поле было заполнено, тогда как вы хотели, чтобы некоторые поля были необязательными. Например, мы желаем, чтобы поле email модели Author было необязательным, то есть оно могло принимать пустую строку. В реальности, возможно, вам не потребуется хранить адреса электронной почты всех авторов.

Чтобы указать, что поле email является необязательным нужно изменить модель Author (которая, как вы помните из главы « Модели », находится в mysite/books/models.py). Просто добавьте blank=True к полю email:

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField(blank=True)

Это указывает Django, что поле адреса электронной почты может быть пустым. По умолчанию, все поля имеют blank=False — это означает, что поля не могут быть пустыми.

Здесь происходит кое-что интересное. До сих пор, за исключением метода __unicode__, модели рассматривались в качестве представления таблиц в базе данных — по-существу отображением SQL-запроса CREATE TABLE. Добавив blank=True мы приступили к расширению нашей модели за рамки простого представления таблицы в базе данных. Теперь наша модель начала превращаться в более сложное представление о структуре данных модели Author и о том, что она может делать. Теперь поле email является не только полем типа VARCHAR в базе данных, но и необязательным в контексте административного интерфейса.

После того, как вы добавили blank=True, обновите страницу «Добавить автора» (http://127.0.0.1:8000/admin/books/author/add/) и вы заметите, что поле «Email», больше не выделено жирным шрифтом. Это означает, что оно является необязательным. Теперь вы можете добавить автора, оставив поле «Email» пустым, и вы больше не увидите ярко-красного сообщения «Поле обязательно».

Необязательные числовые поля и поля с датой

Особо надо отметить использование blank=True совместно с полями даты/времени и численными. Погрузимся в теорию.

SQL обладает собственным способом представления пустых значений — специальное значение NULL. NULL может обозначать «неизвестное» или «неверное», или какое-то другое конкретное значение.

В SQL значение NULL отличается от пустой строки, так же как и в Python специальный объект None отличается от пустой строки («»). Это значит, что некоторые типы полей (например, VARCHAR) могут содержать как NULL, так и пустые строки.

Это может вызвать нежелательную путаницу и неопределённость: «Почему эта запись содержит NULL, а следующая пустую строку? В чём разница? Или данные просто были ошибочно введены?» И: «Как мне получить все записи с пустым полем — следует ли искать записи с пустой строкой и с NULL, или учитывать только пустую строку?»

Что бы избежать подобной путаницы, Django автоматически создает операторы CREATE TABLE (которые рассматривались в главе « Модели »), добавляя явно NOT NULL к каждому полю. Например, сгенерированый запрос создания таблицы для модели Author:

CREATE TABLE "books_author" (
    "id" serial NOT NULL PRIMARY KEY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(40) NOT NULL,
    "email" varchar(75) NOT NULL
);

В большинстве случаев, такое стандартное поведение является оптимальным для вашего приложения и избавит вас от проблем, вызванных несоответствием данных. И оно отлично работает с остальными компонентами Django: такими как административный интерфейс, который вставляет пустую строку (а не значение NULL), если вы оставляете символьное поле пустым.

Исключением являются поля базы данных, для которых пустая строка является недопустимым значением — например, поля даты, времени, числовые. Если вы попытаетесь добавить пустую строку в поле даты или целочисленное поле, скорее всего вы получите ошибку, в зависимости от типа базы данных, которую вы используете. (PostgreSQL, являясь строгим, вернет исключение; MySQL может принять или нет, в зависимости от используемой версии, времени дня и фазы луны.) В таких случаях, NULL является единственным вариантом определения пустого значения. В моделях Django, вы можете указать, что поле допускает значения NULL, добавив параметр null=True в определение поля.

Так вот к чему мы ведём: если вы хотите разрешить пустые значения для полей даты(таких как, DateField, TimeField, DateTimeField) или числовых полей( IntegerField, DecimalField, FloatField), вы должны использовать и blank=True, и null=True.

Для примера, изменим модель Book, разрешив пустое значение для поля publication_date. Вот так вот будет выглядеть наша модель:

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField(blank=True, null=True)

Добавление null=True является более сложным, чем добавление blank=True, потому что null=True изменяет структуру базы данных — то есть, изменяет оператор CREATE TABLE, убирая NOT NULL у поля publication_date. Чтобы завершить изменение, вы должны обновить структуру таблицы в базе данных.

По ряду причин Django не пытается автоматизировать изменение структуры базы данных, так что ответственность за выполнение соответствующих запросов ALTER TABLE ложится на вас всякий раз, когда вы вносите подобные изменения в модель. Напомним, что вы можете использовать команду manage.py dbshell, чтобы войти в оболочку сервера базы данных. Вот запрос для внесения изменений, которые мы совершили:

ALTER TABLE books_book ALTER COLUMN publication_date DROP NOT NULL;

(Обратите внимание, что такой синтаксис специфичен для PostgreSQL.)

Изменения структуры базы данных более детально мы рассмотрим в главе « Расширения для шаблонной системы ».

Завершив изменения, вернёмся к интерфейсу администратора. Теперь форма редактирования книги позволяет оставлять пустой дату публикации.

Настройка меток полей

На форме редактирования в интерфейсе администратора каждая метка поля создаётся на основе имён полей модели. Алгоритм простой: Django просто заменяет символы подчеркивания на пробелы и делает первую букву заглавной, например, поле publication_date модели Book будет иметь метку «Publication date».

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

Например, ниже показано как мы можем изменить метку для поля Author.email на «e-mail»:

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField(blank=True, verbose_name='e-mail')

Сделав это изменение и перезапустив сервер, вы увидите новую метку на форме редактирования автора.

Следует отметить, что не нужно делать первую букву заглавной, если слово не пишется всегда с заглавной (например, «USA state»). Django при необходимости самостоятельно сделает первую букву заглавной, и оставит verbose_name без изменения, где заглавная буква не нужна.

Наконец, отметим, что вы можете передавать verbose_name как позиционный аргумент — более компактный синтаксис. Этот пример эквивалентен предыдущему:

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField('e-mail', blank=True)

Такой вариант не подходит для полей ManyToManyField и ForeignKey, потому что для них первым аргументом должна быть модель. Для этих случаев следует явно указывать аргумент verbose_name.

Собственные классы ModelAdmin

Изменения, которые мы сделали ранее — null=True, blank=True и verbose_name — являются изменениями на уровне модели, а не на уровне интерфейса администратора. То есть, эти изменения являются фундаментальной частью модели, интерфейс администратора Django просто использует их, и в них ничего специфического для административного интерфейса нет.

Помимо этого, интерфейс администратора предоставляет множество возможностей настроить его работу для конкретной модели. Эти настройки реализуются с помощью классов ModelAdmin.

Настройка списка редактирования

Давайте погрузимся в настройку интерфейса, определив поля, которые будут отображатся в списке редактирования для нашей модели Author. По умолчанию, в списке изменения отображается результат выполнения метода __unicode__() для каждого объекта модели. В главе « Модели » мы определили метод __unicode__() для модели Author так, чтобы он возвращал имя и фамилию:

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField(blank=True, verbose_name='e-mail')

    def __unicode__(self):
        return u'%s %s' % (self.first_name, self.last_name)

В результате список изменения обьектов Author отображает имя и фамилию вместе, вы можете это увидеть на рисунке « Страница списка редактирования авторов »:

Рисунок 6.7. Страница списка редактирования авторов

Мы можем изменить это стандартное поведение, добавив в список дополнительные поля. Было бы удобно, например, если бы на этой странице отображался адрес электронной почты автора и можно было сортировать по имени и фамилии.

Чтобы это реализовать определим класс ModelAdmin для модели Author. Этот класс является ключом к настройке интерфейса администратора и самое простое, что он может — это определить список полей, которые будут отображаться на странице списка редактирования. Приведём admin.py к следующему виду:

from django.contrib import admin
from mysite.books.models import Publisher, Author, Book

class AuthorAdmin(admin.ModelAdmin):
    list_display = ('first_name', 'last_name', 'email')

admin.site.register(Publisher)
admin.site.register(Author, AuthorAdmin)
admin.site.register(Book)

Вот, что мы сделали:

Мы создали класс AuthorAdmin. Этот класс, унаследованый от django.contrib.admin.ModelAdmin, содержит настройки для конкретной модели в интерфейсе администратора. Мы изменили только одну настройку — list_display, назначив ей кортеж с именами полей, которые будут отображаться на странице списка редактирования. Естественно, модель должна содержать перечисленные поля.

Мы изменили вызов admin.site.register(), добавив AuthorAdmin после Author. Это можно прочитать как: «Зарегистрировать модель Author с настройками AuthorAdmin».

Функция admin.site.register() принимает унаследованый от ModelAdmin класс, как необязательный аргумент. Если вы не указали второй аргумент(как в случае с Book и Publisher), Django будет использовать настройки по умолчанию для этой модели.

Внеся новые настройки, обновляем страницу списка редактирования авторов, и вы увидите, что на странице теперь отображается три колонки — имя, фамилия и email-адресс. К тому же, можно производить сортировку по колонкам с помощью нажатия на их заголовок. (См. рисунок « Страница списка редактирования авторов с опцией list_display »):

Рисунок 6.8. Страница списка редактирования авторов с опцией list_display

Теперь добавим простую панель поиска. Добавьте search_fields в AuthorAdmin, вот так:

class AuthorAdmin(admin.ModelAdmin):
    list_display = ('first_name', 'last_name', 'email')
    search_fields = ('first_name', 'last_name')

Обновите страницу и вы увидите панель поиска вверху. (См. рисунок « Страница списка редактирования авторов с опцией search_fields ».) Мы просто указали странице со списком редактирования, чтобы она отобразила панель поиска, которая производит поиск по полям first_name и last_name. Как можно ожидать, поиск регистронезависимый и работает с обоими полями. То есть при поиске строки «bar», будет найдет как автор с именем Barney, так и автор с фамилией Hobarson.

Рисунок 6.9. Страница списка редактирования авторов с опцией search_fields

Теперь добавим фильтр по дате к странице списка редактирования модели Book:

from django.contrib import admin
from mysite.books.models import Publisher, Author, Book

class AuthorAdmin(admin.ModelAdmin):
    list_display = ('first_name', 'last_name', 'email')
    search_fields = ('first_name', 'last_name')

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)

admin.site.register(Publisher)
admin.site.register(Author, AuthorAdmin)
admin.site.register(Book, BookAdmin)

Так как мы работаем с другим набором настроек, мы создали отдельный класс ModelAdmin — BookAdmin. Сначала мы определили list_display для более приемлемого отображения списка редактирования. Затем установили list_filter, указав кортеж полей для фильтра, который отобразится в правой части страницы. Для поля даты, Django предоставляет готовые варианты фильтрации списка: «Сегодня», «Последние 7 дней», «Этот месяц», «Этот год» — разработчики Django нашли их самыми популярными вариантами условий фильтрации. Рисунок « Страница списка редактирования книг с опцией list_filter » показывает, как это выглядит:

Рисунок 6.10. Страница списка редактирования книг с опцией list_filter

Параметр list_filter работает с полями разных типов, не только DateField. (Попробуйте, например, с полями BooleanField и ForeignKey.) Фильтры отображаются, если есть хотя бы два значения для фильтрации.

Ещё одним способом фильтрации по дате является использование опции date_hierarchy:

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'

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

Рисунок 6.11. Страница списка редактирования книг с опцией date_hierarchy

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

Теперь изменим сортировку по умолчанию, чтобы на странице списка редактирования, книги сортировались по дате публикации в направлении убывания. По умолчанию, объекты сортируются в соответствии с атрибутом ordering класса Meta (См. главу « Модели »), но если ordering там не указан, то сортировка не определена.

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'
    ordering = ('-publication_date',)

Опция ordering административного интерфейса работает так же, как и для класса Meta, за исключением того, что использует только первое поле указанное в списке. Чтобы указать тип сортировки по-убыванию, необходимо поставить минус перед названием поля.

Обновим страницу списка редактирования. Обратите внимание, что возле заголовка поля даты публикации появилась стрелка, указывающая на направление сортировки. (См. рисунок « Страница списка редактирования книг с сортировкой ».)

Рисунок 6.12. Страница списка редактирования книг с сортировкой

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

Настройка форм редактирования

Так же, как и со списками редактирования, форма редактирования может быть настроена множеством способов.

Сначала определим порядок отображения полей. По умолчанию, поля отображаются в том же порядке, в каком они были определены в модели. Мы можем изменить этот порядок с помощью опции fields в нашем классе, унаследованом от ModelAdmin:

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'
    ordering = ('-publication_date',)
    fields = ('title', 'authors', 'publisher', 'publication_date')

Теперь форма редактирования для книг будет использовать указаный порядок полей. Более естественно, если название книги будет идти перед автором. Конечно, порядок полей зависит от вашего способа работы с данными. Все формы разные.

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

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'
    ordering = ('-publication_date',)
    fields = ('title', 'authors', 'publisher')

В результате, форма редактирования книг не предоставляет возможности изменения даты публикации. Это может быть полезно в случае, если вы издатель, которые предпочитает, чтобы его авторы не отодвигали даты публикаций. (Естественно, это чисто гипотетический пример.)

Когда пользователь использует эту неполную форму для добавления книги, Django устанавливает publication_date в None — поэтому убедитесь, что поле имеет свойство null=True.

Другая часто используемая настройка формы редактирования используется для полей типа многие-ко-многим. Как можно было увидеть на форме редактирования для книг, интерфейс администратора представляет каждое поле типа ManyToManyField в виде списка с возможностью выбора нескольких элементов, который является самым логичным методом отображения в формате HTML — но такие списки не всегда удобно использовать. Если вам требуется отметить несколько элементов, вы должны удерживать клавишу Ctrl для этого. Интерфейс администратора добавляет текст, который напоминает об этом, но представьте ситуацию когда надо выделить сотню элементов.

Интерфейс администратора предоставляет решение — filter_horizontal. Добавим его в BookAdmin и посмотрим что получилось:

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'
    ordering = ('-publication_date',)
    filter_horizontal = ('authors',)

(Обратите внимание, что мы удалили параметр fields.)

Обновите страницу и вы заметите, что поле «Автор» теперь использует удобный javascript-интерфейс фильтрации, который позволяет искать среди элементов и перемещать выбранных авторов в поле выбранных и наоборот.

Рисунок 6.13. Страница списка редактирования книг с опцией filter_horizontal.

Мы настоятельно рекомендуем использовать filter_horizontal для полей ManyToManyField у которых более 10 элементов. Также вы можете использовать filter_horizontal для нескольких полей, указав их имена в кортеже.

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

Опции filter_horizontal и filter_vertical работают только с полями ManyToManyField, не с ForeignKey. По умолчанию, интерфейс администратора использует <select> для полей ForeignKey, но, так же как и для поля ManyToManyField, иногда вы не хотите видеть все объекты в выпадающем списке. Например, если в вашей базе книг будут тысячи авторов, форма добавления книг будет вынуждена загружать всех авторов в выпадающий список.

Методом для решения этой задачи является использование опции raw_id_fields. Укажите для неё кортеж с именами полей ForeignKey и эти поля будут отображаться простым текстовым полем (<input type="text">) вместо <select>. (См. рисунок « Страница списка редактирования книг с опцией raw_id_fields. ».)

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'publisher', 'publication_date')
    list_filter = ('publication_date',)
    date_hierarchy = 'publication_date'
    ordering = ('-publication_date',)
    filter_horizontal = ('authors',)
    raw_id_fields = ('publisher',)

Рисунок 6.14. Страница списка редактирования книг с опцией raw_id_fields.

Что вы будете вводить в это поле? Идентификатор издателя. Учитывая то, что люди не могут запомнить идентификаторы из базы данных, рядом находится иконка с увеличительным стеклом, которую вы можете нажать для отображения окна, на котором вы можете выбрать требуемого издателя.

Пользователи, группы и права

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

Такие учётные записи являются достаточно общими для использования вне интерфейса администратора, но в данный момент мы будем их рассматривать в качестве учётных записей административных пользователей. В главе « Средства от других разработчиков » мы рассмотрим, как интегрировать учетные записи пользователей с остальной частью вашего сайта (т.е., использовать их не только в интерфейсе администратора).

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

Флаг «is active» (активен) управляет активностью пользователя в общем смысле. Если флаг отключен, пользователь не пройдёт аутентификацию даже при указании правильного пароля.

Флаг «is staff» (сотрудник) управляет доступом пользователя к интерфейсу администратора (т.е., является ли пользователь штатным сотрудником вашей организации). Так как, тот же самый пользователь системы может управлять доступом к публичной, не административной, части сайтов (обратитесь к главе « Средства от других разработчиков »), значение этого флага разделяет обычных пользователей от администраторов.

Флаг «is superuser» (суперпользователь) предоставляет пользователю полные права на добавление, создание и удаление любого элемента на интерфейсе администратора. Обычные правила доступа игнорируются.

«Обычные» административные пользователи — т.е., сотрудники — имеют ограниченный набор прав для работы с интерфейсом администратора. Каждый объект, который можно отредактировать через интерфейс (т.е., книги, авторы) обладает тремя правами: право на создание, право на редактирование и право на удаление. Наделение правами пользователя позволяет ему выполнять эти действия.

Когда вы создаёте пользователя, он не обладает никакими правами и задача наделения пользователя ими лежит целиком на вас. Например, вы можете дать пользователю право на добавление и изменение издателей, но не дать права на их удаление. Следует отметить, что права выдаются на каждую модель в отдельности, а не на каждый объект модели. Таким образом, это позволяет вам сказать «Иван может изменять любую книгу», но не позволяет сказать «Иван может изменять любую книгу изданную Apress». Такая функциональность, как права на каждый объект лежит вне данной книги, но хорошо освещена в официальной документации на Django.

Замечание

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

Также вы можете добавлять пользователей в группы. Группа — это список пользователей, которых объединяет общий набор прав. Группы полезны при назначении одинаковых прав на большое количество пользователей.

Когда и почему надо использовать интерфейс администратора, а когда не надо

Изучив эту главу, вы должны иметь хорошее представление о том, как использовать интерфейс администратора. Но мы хотим рассмотреть подробнее, когда и почему вы можете использовать его, и когда это делать не следует.

Интерфейс администратора Django особенно полезен в ситуации, когда его использует неискушённый пользователь для ввода данных, что является основной задачей этого компонента системы. В редакции газеты, где Django был разработан, взаимодействие журналиста и разработчика при выдаче, например, специального репортажа о качестве воды в муниципальной сети, происходит как-то так:

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

Разработчик создаёт модель для этих данных и затем открывает доступ репортёру к интерфейсу администратора.

Корреспондент осматривает интерфейс администратора. Указать на недостающие или лишние поля — лучше сейчас, чем позже. Разработчик оперативно изменяет модели.

Пока журналист вводит данные в Django, разработчик может сфокусироваться на работе над публичной частью интерфейса (прикольная часть!).

Другими словами, смысл существования (raison d'être) интерфейса администратора Django — способствовать одновременной работе журналиста (наполнение сайта) и разработчика (создание функционала).

Тем не менее, кроме очевидных задач по вводу данных, мы находим интерфейс администратора полезным в некоторых других случаях:

Проверка моделей данных: После определение нескольких моделей, будет полезно открыть их в интерфейсе и ввести некоторое количество данных. Обычно на этом этапе находятся ошибки в схеме базы данных.

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

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

Напоследок хотим сказать, что интерфейс администратора не «такой как есть и все». За многие годы мы видели, как его улучшали и модернизировали, чтобы он выполнял функции, на которые не был рассчитан. Он не предназначен для общего использования, сложной сортировки и поиска данных. Как мы упоминали в этой главе, он предназначен для доверенных администраторов сайта. Знание этого — ключ к эффективному использованию интерфейса администратора.

Что дальше?

Мы создали несколько моделей и настроили первоклассный интерфейс для редактирования данных. В следующей главе мы перейдём к внутренностям веб-разработки: созданию форм и их обработке.

Глава 7. Формы

Перевод © Попов Руслан <ruslan.popov • gmail>

HTML формы являются основой интерактивных веб-сайтов, от простой формы поиска Google и вездесущих форм для комментирования в блогах до сложных уникальных интерфейсов для ввода данных. Эта глава рассматривает возможности Django по обработке форм, проверки переданных значений. Здесь мы рассмотрим объекты HttpRequest и Form.

Получение данных из объекта запроса

Мы впервые коснулись объектов HttpRequest в главе « Представления и привязки URL » при начальном изучении функций представления, но мы многого тогда не рассказали. Помните, каждая функция представления принимала объект HttpRequest в качестве первого аргумента, как в нашем представлении hello():

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello world")

HttpRequest объекты, в нашем случае представленные переменной request, обладают рядом интересных атрибутов и методов, с которыми вам следует познакомиться, чтобы вы знали их возможности. Вы можете использовать данные атрибуты для получения информации о текущем запросе (т.е., о пользователе и браузере, который запросил текущую страницу вашего сайта) во время выполнения функции представления.

Информация об URL

HttpRequest объекты содержат информацию о запрошенном URL:

Таблица 7.1. Информация об URL

Атрибут/Метод | Описание | Пример
request.path | Полный путь, без домена, но с начальным слешом. | /hello/
request.get_host() | Имя компьютера (т.е., «домен»). | 127.0.0.1:8000 или www.example.com
request.get_full_path() | Равен path, но со строкой запроса (если она существует). | /hello/?print=true
request.is_secure() | True в случае использования HTTPS. Иначе False. | True или False

Всегда используйте эти атрибуты/методы вместо прямого вписывания URL в ваши представления. Такой подход делает код более гибким и его можно будет повторно использовать в других местах. Простейший пример:

# BAD!
def current_url_view_bad(request):
    return HttpResponse("Welcome to the page at /current/")

# GOOD
def current_url_view_good(request):
    return HttpResponse("Welcome to the page at %s" % request.path)

Остальная информация о запросе

Атрибут request.META является словарём, который содержит все доступные HTTP заголовки текущего запроса, включая IP адрес посетителя и информацию об его браузере (название и версию). Следует отметить, что полный список доступных заголовков зависит от того, какие заголовки были посланы браузером посетителя и какие заголовки были установлены веб-сервером. Перечислим несколько стандартных ключей этого словаря:

HTTP_REFERER — ссылка на страницу с которой пришли на текущую, если такая существует.

HTTP_USER_AGENT — идентификационная строка браузера. Обычно выглядит так: "Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17".

REMOTE_ADDR — IP адрес пользователя, т.е., "12.245.67.89". (Если запрос был передан через прокси, тогда адрес может быть передан в виде списка: "12.245.67.89,23.45.78.90".)

Следует отметить, что так как request.META является обычным словарём языка Python, вы получите исключение KeyError, если попытаетесь использовать несуществующий ключ. (Так как HTTP заголовки являются внешними данными — они передаются браузерами посетителей — им не стоит доверять, и вы должны всегда проектировать ваше приложение на грамотную обработку возникающих ошибок, если нужного заголовка нет или он пустой.) Следует использовать блок try/except или метод get() для обработки случая с неопределёнными ключами:

# BAD!
def ua_display_bad(request):
    ua = request.META['HTTP_USER_AGENT']  # Might raise KeyError!
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 1)
def ua_display_good1(request):
    try:
        ua = request.META['HTTP_USER_AGENT']
    except KeyError:
        ua = 'unknown'
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 2)
def ua_display_good2(request):
    ua = request.META.get('HTTP_USER_AGENT', 'unknown')
    return HttpResponse("Your browser is %s" % ua)

Мы рекомендуем вам написать небольшое представление, которое будет отображать все данные из request.META, так вы сможете узнать что там есть. Вот так может выглядеть ваше представление:

def display_meta(request):
    values = request.META.items()
    values.sort()
    html = []
    for k, v in values:
        html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
    return HttpResponse('<table>%s</table>' % '\n'.join(html))

В качестве упражнения, вы можете преобразовать данный код для использования возможностей шаблонной системы Django, а не использовать «сырой» HTML.

Информация о переданных данных

Кроме основных метаданных о запросе, HttpRequest имеет два атрибута, которые содержат информацию переданную пользователем: request.GET и request.POST. Оба этих атрибута являются словарными объектами, которые предоставляют доступ к данным GET и POST.

Словарные объекты

Когда мы говорим, что request.GET и request.POST являются «словарными» объектами, мы подразумеваем, что они ведут себя как обычные словари языка Python, но технически не являются словарями. Например, request.GET и request.POST обладают методами get(), keys() и values() и вы можете выполнять итерацию по их ключам, так for key in request.GET.

Так в чём разница? В том, что эти объекты обладают дополнительными методами, которых нет у обычных словарей. Мы ещё остановимся на этом.

Вы можете встретиться с подобным термином — «файловые объекты» — такие объекты обладают несколькими основными методами, подобными read(), которые позволяют работать с ними почти как с настоящими файлами.

POST данные обычно получают из HTML <form>, а GET данные могут приходить как от форм, так и из URL.

Пример обработки простой формы

Продолжая наш пример с книгами, авторами и издателями, давайте создадим простое представление, которое позволит пользователям производить поиск по нашей базе книг по их названию.

Обычно, разработка форм подразумевает два шага: пользовательский интерфейс в HTML и код сопровождающего представления, которое обрабатывает переданные данные. Первый шаг несложен. Давайте создадим представление, которое отображает поисковую форму:

from django.shortcuts import render_to_response

def search_form(request):
    return render_to_response('search_form.html')

Как мы уже говорили в главе « Представления и привязки URL », представление может располагаться где угодно, пока оно находится через PYTHONPATH. Для порядка разместим его в books/views.py.

Соответствующий шаблон, search_form.html, может выглядеть так:

<html>
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

Схема URL в urls.py может выглядеть так:

from mysite.books import views

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    # ...
)

(Следует отметить, что мы импортируем модуль views напрямую, а не указываем from mysite.views import search_form, поскольку первый метод более краток. Мы рассмотрим этот механизм импортирования более подробно в главе « Усовершенствованные представления и схемы URL ».)

Если вы запустите runserver и посетите http://127.0.0.1:8000/search-form/, вы увидете поисковый интерфейс. Очень простой.

Попробуйте отправить форму, несмотря на то, что вы получите ошибку 404. Форма указывает на URL /search/, обработчик которого ещё не был реализован. Давайте это исправим с помощью второй функции представления:

# urls.py

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    (r'^search/$', views.search),
    # ...
)

# views.py

def search(request):
    if 'q' in request.GET:
        message = 'You searched for: %r' % request.GET['q']
    else:
        message = 'You submitted an empty form.'
    return HttpResponse(message)

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

Тег <form> определяет переменную q. При отправке формы, значение этой переменной посылается через GET (method="get") на URL /search/.

Представление обрабатывающее URL /search/ (search()) получает доступ к значению переменной q через request.GET.

Обратите внимание, мы явно проверяем существование переменной q в request.GET. Как мы говорили ранее, вы должны проявлять должное недоверие к данным, переданным пользователями. Если бы мы не сделали такую проверку, то любая передача пустой формы приводила бы к вызову исключения KeyError в представлении:

# BAD!
def bad_search(request):
    # The following line will raise KeyError if 'q' hasn't
    # been submitted!
    message = 'You searched for: %r' % request.GET['q']
    return HttpResponse(message)

Параметры строки запроса

Так как GET данные передаются в строке запроса (т.е., /search/?q=django), вы можете использовать request.GET для доступа к переменным, переданным в этой строке. В главе « Представления и привязки URL » при ознакомлении со схемой URL, мы сравнивали подход Django к формированию URL с традиционными PHP/Java URL, например, /time/plus?hours=3 и обещали вернуться к этому в этой главе. Теперь вы знаете как получать доступ к параметрам строки запроса в ваших представлениях — просто используйте request.GET.

POST данные обрабатываются аналогично GET — только используйте request.POST вместо request.GET. Какая разница существует между GET и POST? Используйте GET в случаях, когда форма используется для «получения» данных[9]. Используйте POST при обработке форм, которая имеет побочный эффект — изменение данных или при отправке электронных сообщений, в общем для всего, что лежит вне задачи простого отображения данных. В нашем примере мы используем GET, так как запрос не изменяет данные на сервере. (Обратитесь к http://www.w3.org/2001/tag/doc/whenToUseGet.html, если вы желаете узнать больше о GET и POST).

После того как мы проверили переданные данные, давайте используем их для поиска по базе данных (и снова, в views.py):

from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')

Остановимся и опишем, что мы сделали:

Помимо проверки существования q в request.GET, мы удостоверились, что request.GET['q'] не содержит пустую строку, перед её передачей в базу данных.

Мы используем Book.objects.filter(title__icontains=q) для поиска по нашей таблице книг, разыскивая книги у которых заголовок содержит переданную строку. Суффикс icontains является модификатором (это было рассмотрено в главе « Модели » и в Приложении B FIXME) и данный оператор может быть приблизительно переведён в «Получить все книги, названия которых содержат q, не принимая во внимание регистр букв.»

Это очень простой способ реализации поиска книг. Мы не рекомендуем использовать простой icontains запрос на больших реальных базах данных, так как он может быть очень медленным. (В реальном мире, вам потребуется собственная поисковая система. Поищите в Интернет полнотекстовую поисковую систему с открытым кодом, чтобы получить информацию о их возможностях.)

Мы передаём books, список объектов Book, в шаблон. Шаблонный код в файле search_results.html может выглядеть так:

<p>You searched for: <strong>{{ query }}</strong></p>

{% if books %}
    <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
    <ul>
        {% for book in books %}
        <li>{{ book.title }}</li>
        {% endfor %}
    </ul>
{% else %}
    <p>No books matched your search criteria.</p>
{% endif %}

[9] Старайтесь использовать в формах POST (прим. переводчика).

Усовершенствование примера обработки простой формы

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

Во-первых, наша обработка пустой строки в search() слишком примитивна — мы просто отображаем сообщение «Please submit a search term.», требуя от пользователя воспользоваться кнопкой Назад. Это неприятно и непрофессионально, и если вы попробуете сделать такое в реальном приложении, вас отлучат от Django.

Гораздо лучше будет отобразить форму снова, вывести сообщение об ошибке над ней — пользователь сможет немедленно исправить её и сразу отправить данные. Простейшим способом сделать это является повторная обработка шаблона, вот так:

from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search_form(request):
    return render_to_response('search_form.html')

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
        return render_to_response('search_form.html', {'error': True})

(Отметьте, что мы включили сюда search_form(), чтобы вы могли видеть оба представления вместе.)

Итак, мы доработали search() для повторного отображения шаблона search_form.html, если запрос будет пустым. А раз нам необходимо отображать сообщение об ошибке в шаблоне, мы передали шаблонную переменную. Теперь мы может отредактировать search_form.html, добавив проверку наличия переменной error:

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

Мы по-прежнему можем использовать этот шаблон из нашего оригинального представления, search_form(), так как оно не передаёт error в шаблон — следовательно, сообщение об ошибке не будет отображаться.

Учитывая сделанные изменения, появляется вопрос: действительно ли мы нуждаемся в представлении search_form()? Действительно, запрос к URL /search/ (без параметров GET) отобразит пустую форму (но с ошибкой). Мы можем удалить представление search_form() вместе с соответствующей строкой в схеме URL, останется только изменить search() так, чтобы оно не выдавало сообщение об ошибке, если кто-то зайдёт по этому URL без указания GET параметров:

def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})

Теперь, если пользователь посетит /search/ без указания GET параметров, он увидит поисковую форму без сообщения об ошибке. Если пользователь передаст форму с пустым значением q, он увидит форму c сообщением об ошибке. И, наконец, если пользователь передаст форму с непустым значением q, он увидит результаты выполнения поискового запроса.

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

<form action="/search/" method="get">

можно писать так:

<form action="" method="get">

Параметр action="" означает «Отправь форму по URL текущей страницы.» Учитывая это изменение, больше не требуется изменять action при переходе формы на другой URL.

Простая проверка данных

Наш пример с поиском всё ещё остаётся достаточно простым, особенно с точки зрения проверки передаваемых данных. Мы просто проверяем наличие информации в запросе. Множество HTML форм включают в себя значительно более сложные проверки. Все мы видели такие сообщения об ошибках на сайтах:

«Пожалуйста, введите верный адрес электронной почты. 'foo' не является адресом электронной почты.»

«Пожалуйста, введите верный шестизначный код почтового отделения. '123' не является почтовым кодом.»

«Пожалуйста, введите верную дату в формате DD-MM-YYYY.»

«Пожалуйста, введите пароль, который содержит как минимум 8 символов и содержит хотя бы одну цифру.»

Об использовании JavaScript для проверки данных

Хотя мы не рассказываем об этом, но вы можете использовать JavaScript для проверки данных на стороне клиента, прямо в браузере. Но помните, даже если вы это делаете, вы обязаны сделать проверку и на стороне сервера. Некоторые пользователи не включают JavaScript, а «злобные хакеры» могут передавать самостоятельно сформированные данные, которые не доступны JavaScript коду.

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

Давайте доработаем наше представление search() так, чтобы оно проверяло переданную строку на превышение её длины в 20 символов. (Предположим для этого примера, что передача более длинной строки значительно замедлит скорость выполнения поиска.) Как мы можем это сделать? Самым простым способом будет внедрение логики прямо в представление, вот так:

def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        elif len(q) > 20:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})

Теперь, при попытке отослать запрос с длиной строки более 20 символов, вы получите сообщение об ошибке. Но это сообщение в search_form.html пока говорит «Please submit a search term.» и нам потребуется изменить его так, чтобы оно учитывало оба случая:

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term 20 characters or shorter.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

Есть что-то некрасивое в этом. Наше «универсальное» сообщение об ошибке может запутать. Как может сообщение для пустого запроса упоминать об ограничении размера строки запроса 20-ю символами? Сообщение об ошибке должно быть точное.

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

def search(request):
    errors = []
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            errors.append('Enter a search term.')
        elif len(q) > 20:
            errors.append('Please enter at most 20 characters.')
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'errors': errors})

Затем, надо сделать небольшое исправление в search_form.html для того, чтобы учесть изменения в коде — в шаблон передаётся список error, а не булево значение:

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

Создание формы для контактной информации

Несмотря на то, что мы несколько раз изменяли форму поиска по книгам и значительно её улучшили, она осталась достаточно простой: просто одно поле, q. Из-за её простоты мы даже не воспользовались библиотекой форм Django. Более сложные формы потребуют более сложную обработку — сделаем что-нибудь посложнее: форму для передачи контактной информации.

Это будет форма, которая позволит пользователям отправлять отклик, с необязательным указанием своего адреса электронной почты. После передачи данных формы и их проверки, мы автоматически отправим сообщение по электронной почте сотрудникам сайта.

Начнём с шаблона, contact_form.html:

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject"></p>
        <p>Your e-mail (optional): <input type="text" name="email"></p>
        <p>Message: <textarea name="message" rows="10" cols="50"></textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

Мы определили три поля: заголовок, адрес электронной почты и сообщение. Второе поле необязательное. Следует отметить, что мы используем method="post", так как форма имеет побочный эффект — она отправляет сообщение. Также мы скопировали код, отвечающий за отображение ошибки из нашего предыдущего шаблона search_form.html.

Если продолжать методику, начатую при работе с нашим представлением search(), первой наивной версией нашего представления contact() будет:

from django.core.mail import send_mail
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response

def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html',
        {'errors': errors})

(Если вы сразу проверяете наш код в действии, вы можете заинтересоваться зачем размещать это представление в файле books/views.py. Оно предназначено для работы с книжным приложением, где же ему ещё находиться? Но в общем, всё зависит от вас. Django нет разницы где размещён код, если она может найти его по связи в URL. Мы предпочитаем создавать отдельный каталог, contact на уровне каталога books Он будет содержать пустой __init__.py и views.py.)

Рассмотрим отличия:

Мы проверяем, что request.method равен 'POST'. Это условие будет верным только в случае отправки формы, иначе считаем, что происходит простое отображение формы. (В последнем случае, request.method будет установлен в 'GET', так как для отображения формы браузеры используют GET, а не POST.) Такой подход обеспечивает отличный способ изоляции случая «отображения формы» от «обработки формы».

Вместо использования request.GET мы применяем request.POST для доступа к переданным данным. Это необходимо, так как <form> в contact_form.html использует method="post". Если данное представление вызывается через POST, значит request.GET будет пустым.

В этот раз у нас есть два обязательных поля, subject и message, нам следует проверять оба. Следует отметить, что для получения значения из POST мы используем метод get() и подставляем пустую строку в качестве значения по умолчанию. Это хороший, краткий способ обработки двух проблем: отсутствие ключа и отсутствие данных.

Несмотря на то, что поле email является необязательным, мы всё равно проверяем его, правда только при наличии в нём данных. Наша проверка достаточно простая — мы проверяем наличие в строке символа @. В реальной ситуации вам потребуется более устойчивая проверка (и Django предоставляет её, мы очень скоро её покажем).

Мы используем функцию send_mail из модуля django.core.mail для отправки сообщения по электронной почте. Эта функция принимает четыре обязательных аргумента: тема сообщения, тело сообщения, адрес «от» и список адресов получателей. Функция send_mail является удобной оболочкой над классом Django EmailMessage, который предоставляет дополнительные возможности, например, вложение файлов, multipart сообщения и полный контроль над заголовками сообщения.

Следует отметить, что для отправки сообщения с помощью send_mail ваш сервер должен быть соответственно сконфигурирован и Django следует указать на сервер исходящей почты. Обратитесь к http://docs.djangoproject.com/en/dev/topics/email/ для подробностей.

После отправки сообщения мы перенаправляем пользователя на страницу с сообщением об успешном выполнении операции, возвращая объект HttpResponseRedirect. Мы оставляем реализацию такой страницы вам в качестве домашнего задания (это просто представление/привязка URL/шаблон), но нам следует объяснить причину, почему мы инициировали перенаправление вместо, например, простого вызова render_to_response() прямо здесь.

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

Вы всегда должны осуществлять перенаправление после успешной обработки POST запроса.

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

Следующей проблемой является повторное отображение формы. В случае возникновения ошибок при проверке полей, лучше всего будет отобразить форму повторно, внеся в поля переданные данные, так пользователь сможет увидеть, что именно он сделал неправильно (и также ему не потребуется вводить эти данные повторно). Мы можем вручную передать POST данные обратно в шаблон, но нам потребуется отредактировать каждое HTML поле для вставки соответствующего значения на соответствующее место.

views.py:

def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html', {
        'errors': errors,
        'subject': request.POST.get('subject', ''),
        'message': request.POST.get('message', ''),
        'email': request.POST.get('email', ''),
    })

contact_form.html:

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject" value="{{ subject }}"></p>
        <p>Your e-mail (optional): <input type="text" name="email" value="{{ email }}"></p>
        <p>Message: <textarea name="message" rows="10" cols="50">**{{ message }}**</textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

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

Ваш первый класс формы

Django поставляется с библиотекой для работы с формами, она находится в модуле django.forms и может решать множество задач, которые мы рассмотрим в этой главе — от отображения форм до проверки полей. Давайте переработаем наше приложение с контактной формой, используя эту библиотеку.

Библиотека newforms

Общаясь в Django сообществе, вы могли услышать о django.newforms. Так раньше называлась стандартная библиотека django.forms — эта библиотека рассматривается в данной главе.

Причина такого переименования историческая. Первый релиз Django поставлялся со сложной, запутанной библиотекой для работы с формами — django.forms. Она была полностью переписана и новая версия имела имя django.newforms, люди могли использовать как одну, так и другую библиотеки. После выхода Django 1.0 старая библиотека была убрана, а django.newforms стала django.forms.

Основным способом использования библиотеки является определения класса Form для каждого тега <form> в ваших шаблонах. В нашем случае, у нас есть только один такой тег, следовательно и класс нам надо создать только один. Этот класс может располагаться в любой части кода, даже в файле views.py, но, учитывая соглашения Django сообщества, код класса следует располагать в файле forms.py. Создайте этот файл рядом с views.py и впишите следующее:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField()

Синтаксис похож на синтаксис описания модели. Каждое поле формы представлено потомками класса Field — в нашем случае используются CharField и EmailField — в виде атрибутов класса Form. Каждое поле по умолчанию является обязательным, таким образом, чтобы отменить это поведение у поля email, мы используем атрибут required=False.

Давайте перейдём в интерактивный интерпретатор Python и посмотрим, что этот класс может делать. Во-первых, он может отображать себя в виде HTML:

>>> from contact.forms import ContactForm
>>> f = ContactForm()
>>> print f
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" name="subject" id="id_subject" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="text" name="email" id="id_email" /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr>

Django добавляет метку для каждого поля с помощью тега <label>. Идея состоит в том, чтобы сделать стандартное поведение наиболее оптимальным.

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

>>> print f.as_ul()
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></li>
<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></li>
<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li>
>>> print f.as_p()
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></p>
<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p>

Следует отметить, что открывающие и закрывающие теги <table>, <ul> и <form> не указываются, значит вы можете сами добавить любую дополнительную запись при необходимости.

Эти методы являются простыми сокращениями для общей задачи «отображения всей формы». Вы можете также отобразить HTML для конкретного поля формы:

>>> print f['subject']
<input type="text" name="subject" id="id_subject" />
>>> print f['message']
<input type="text" name="message" id="id_message" />

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

>>> f = ContactForm({'subject': 'Hello',
                     'email': 'adrian@example.com',
		     'message': 'Nice site!'})

Теперь можно проверить каждому ли полю назначено значение:

>>> f.is_bound
True

Вызывайте метод is_valid() для Form, чтобы проверить данные, переданные форме. Мы передали верные данные для каждого поля, таким образом форма должна быть правильной:

>>> f.is_valid()
True

Если мы не передадим поле email, форма по-прежнему останется верной, так как мы указали атрибут required=False для этого поля:

>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'})
>>> f.is_valid()
True

Но если мы не укажем subject или message, тогда форма перестанет быть верной:

>>> f = ContactForm({'subject': 'Hello'})
>>> f.is_valid()
False
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.is_valid()
False

Вы можете получить сообщения об ошибке для каждого поля:

>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f['message'].errors
[u'This field is required.']
>>> f['subject'].errors
[]
>>> f['email'].errors
[]

Каждый экземпляр Form имеет атрибут errors, который хранит словарь со списком сообщений для каждого поля:

>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.errors
{'message': [u'This field is required.']}

Наконец, для экземпляров Form, данные которых признаны верными, становится доступным атрибут cleaned_data. Атрибут содержит словарь с «чистыми» переданными данными. Библиотека для работы с формами не только проверяет данные, но и может их обрабатывать, конвертируя значения в соответствующие типы языка Python.

>>> f = ContactForm({‘subject’: ‘Hello’, ‘email’: ‘adrian@example.com’, ‘message’: ‘Nice site!’})
>>> f.is_valid()
True
>>> f.cleaned_data
{‘message’: u’Nice site!’, ‘email’: u’adrian@example.com’, ‘subject’: u’Hello’}

Наша форма работает только со строками, которые конвертируются в объекты Unicode — но если мы бы мы использовали IntegerField или DateField, библиотека бы предоставила в cleaned_data соответствующие типы целого или datetime.date объекты для соответствующих полей.

Используем формы в представлениях

Вот так можно переписать наше представление contact(), обладая базовыми знаниями о классе Form.

Файл views.py:

from django.core.mail import send_mail
from django.shortcuts import render_to_response
from mysite.books.forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm()
    return render_to_response('contact_form.html', {'form': form})

Файл contact_form.html:

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

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

Попробуйте запустить это локально. Загрузите форму, отправьте её без данных, отправьте её с неправильным адресом электронной почты, затем заполните её правильными данными и отправьте. (Конечно, в зависимости от конфигурации вашего почтового сервера, вы можете получить ошибку при вызове send_mail, но это уже другая история.)

Изменения способа отображения полей

Возможно первое, что вам потребуется сделать, когда вы попробуете локально поработать с нашей формой — изменить отображение поля message так, чтобы оно использовало <textarea> вместо <input type="text">. Мы можем исправить это с помощью атрибута поля widget:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

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

Рассматривайте классы Field в качестве проверочной логики, а виджеты в качестве логики представления.

Установка максимального размера поля

При проверке значения поля обычно сначала проверяют длину переданной строки. Ограничим поле subject нашей формы ста символами. Для этого, просто назначьте соответствующее значение атрибуту max_length классу CharField поля:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

Также существует необязательный атрибут min_length.

Установка начальных значений

В качестве усовершенствования формы, добавим начальное значение для поля subject: I love your site! (Небольшое внушение не повредит.) Для этого следует воспользоваться аргументом initial при создании экземпляра формы:

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm(
            initial={'subject': 'I love your site!'}
        )
    return render_to_response('contact_form.html', {'form': form})

Теперь поле subject будет отображаться заполненным.

Следует отметить, что существует разница между передачей начальных данных и передачей данных, которые привязаны к форме. Разница заключается в том, что если просто передаёте начальные данные, то форма остаётся незаполненной, что она не будет содержать сообщений об ошибках[10].

[10] Авторы имеют в виду, что если при возникновении ошибки во время проверки данных и вы возвратите пользователю форму, передав данные в форму через initial, то никаких сообщений о возникших ошибках не сохранится. Примечание переводчика.

Собственные правила проверки

Представьте, что мы выложили форму на боевой сервер и к нам начали приходить сообщения. Но есть одна проблема — некоторые сообщения содержат всего одно-два слова, что недостаточно для нашей цели. Мы решили применить новую политику проверки: четыре слова или больше, пожалуйста.

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

Раз нам требуется дополнительная проверка поля message, добавим метод clean_message() в наш класс:

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

    def clean_message(self):
        message = self.cleaned_data['message']
        num_words = len(message.split())
        if num_words < 4:
            raise forms.ValidationError("Not enough words!")
        return message

Библиотека автоматически ищет любой метод, который начинается с clean_ и заканчивается на имя поля. Если такой метод существует, он используется во время выполнения проверки поля.

Данные метод clean_message() будет вызван после выполнения стандартной проверки для данного поля (в этом случае, необходимая проверка для CharField). Так как значение поля уже будет частично проверено, мы получаем его из self.cleaned_data. Также, нам не стоит беспокоиться о наличии значения или о том, что оно пустое, всё это обеспечит стандартный обработчик.

Мы просто используем комбинацию len() и split() для подсчёта количества слов. Если пользователь ввёл слишком мало слов, мы вызываем исключение ValidationError модуля forms. Строка, переданная исключению, будет отображена пользователю в виде элемента списка ошибок.

Важно то, что мы явно возвращаем обработанное значение поля в конце метода. Это позволяет нам модифицировать значение (или преобразовать его в другой тип Python) в рамках данного метода. Если мы забудем воспользоваться оператором return, тогда будет возвращён None и оригинальное значение будет утеряно.

Определение меток

По умолчанию метки для полей формы создаются автоматически из имени поля путём замены символов подчёркивания на пробелы и переводом первой буквы на заглавную — из имени поля email мы получим «Email». (Знакомо? Это тот же простой алгоритм, который используется в моделях Django для вычисления значения verbose_name для их полей. Мы рассмотрели его в главе « Модели ».)

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

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False, label='Your e-mail address')
    message = forms.CharField(widget=forms.Textarea)

Настройка дизайна формы

Наш шаблон contact_form.html использует {{ form.as_table }} для отображения формы, но мы можем отображать её другими способами для получения более точного контроля над процессом отображения.

Наиболее быстрым способом настройки вида формы будет использование CSS. Списки ошибок могут отображаться с визуальным выделением, автосгенерированные списки используют <ul class="errorlist">, это можно использовать:

<style type="text/css">
    ul.errorlist {
        margin: 0;
        padding: 0;
    }
    .errorlist li {
        background-color: red;
        color: white;
        display: block;
        font-size: 10px;
        margin: 0 0 3px;
        padding: 4px 5px;
    }
</style>

Чаще всего вы будете изменять стандартный вид формы. Тег {{ form.as_table }} и остальные являются лишь полезными сокращениями, но процессом отображения формы можно управлять, в основном на уровне шаблона.

Каждый виджет поля (<input type="text">, <select>, <textarea> и так далее) может быть индивидуально отображён с помощью указания {{ form.fieldname }} в шаблоне, а любые ошибки, ассоциированные с конкретным полем, можно получить через {{ form.fieldname.errors }}. Помня это, мы можем создать собственный шаблон для нашей формы с помощью следующего кода:

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <div class="field">
            {{ form.subject.errors }}
            <label for="id_subject">Subject:</label>
            {{ form.subject }}
        </div>
        <div class="field">
            {{ form.email.errors }}
            <label for="id_email">Your e-mail address:</label>
            {{ form.email }}
        </div>
        <div class="field">
            {{ form.message.errors }}
            <label for="id_message">Message:</label>
            {{ form.message }}
        </div>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

Тег {{ form.message.errors }} отобразит <ul class="errorlist">, при наличии ошибок и пустую строку, при их отсутствии (или если к форме не привязаны данные). Мы можем также рассматривать form.message.errors в качестве булевого значения или в качестве итератора. Например:

<div class="field{% if form.message.errors %} errors{% endif %}">
    {% if form.message.errors %}
        <ul>
        {% for error in form.message.errors %}
            <li><strong>{{ error }}</strong></li>
        {% endfor %}
        </ul>
    {% endif %}
    <label for="id_message">Message:</label>
    {{ form.message }}
</div>

В случае ошибок при проверке данных, к тегу <div> будет добавлен класс errors и будут отображены ошибки в виде ненумерованного списка.

Что дальше?

Данная глава завершает вводную часть этой книги. Следующие главы, с « Усовершенствованные представления и схемы URL » по « Сессии, пользователи и регистрация », рассматривают подробности профессионального использования Django, включая вопрос развёртывания приложения.

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

Мы начнём главу « Усовершенствованные представления и схемы URL » с возвращения к представлениям и схеме URL (о которых было рассказано в главе « Представления и привязки URL »).

Глава 8. Усовершенствованные представления и схемы URL

Перевод © Попов Руслан <radz • yandex • ru>

В главе « Представления и привязки URL » мы рассказали об основах функций представлений Django и о схеме URL, в которой определено соответствие URL этим функциям. Эта глава остановится на этих вопросах более подробно.

Схема URL: полезные приёмы

В схеме URL нет ничего особенного, как всё остальное в Django, это просто код на языке Python. Вы можете использовать это знание различными способами, как показано в разделах главы далее.

Упорядочивание функций импорта

Рассмотрим нижеприведённый файл со схемой URL, созданой по примеру главы « Представления и привязки URL »:

                    from django.conf.urls.defaults import *
                    from mysite.views import hello, current_datetime, hours_ahead

                    urlpatterns = patterns('',
                        (r'^hello/$', hello),
                        (r'^time/$', current_datetime),
                        (r'^time/plus/(\d{1,2})/$', hours_ahead),
                    )

Как объяснялось в главе « Представления и привязки URL », каждый шаблон схемы URL ассоциирован с функцией представления, которая указывается в виде функционального объекта. Это означает, что необходимо импортировать функции представления вначале модуля.

Но по мере усложнения Django приложения, его схема URL также растёт, управление этими импортами может быть утомительным. (Для каждой новой функции представления вам потребуется не забыть об её импорте, это скажется на размере оператора импорта.) Есть возможность избежать этого, просто импортируя сам модуль views . Нижеприведённый пример эквивалентен предыдущему:

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^hello/$', views.hello),
                        (r'^time/$', views.current_datetime),
                        (r'^time/plus/(d{1,2})/$', views.hours_ahead),
                    )

Django предоставляет другой способ указания функции представления для определённого шаблона в схеме URL. Вы можете передать строку, которая содержит имя модуля и имя функции, вместо передачи самого объекта. Продолжаем работу над нашим примером:

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                        (r'^hello/$', 'mysite.views.hello'),
                        (r'^time/$', 'mysite.views.current_datetime'),
                        (r'^time/plus/(d{1,2})/$', 'mysite.views.hours_ahead'),
                    )

Следует отметить кавычки вокруг имён представления! Мы используем «'mysite.views.current_datetime'» - с кавычками - вместо «mysite.views.current_datetime»

Используя этот метод, больше нет необходимости импортировать функции представления. Django автоматически импортирует соответствующую функцию представления как только она понадобится, в соответствии с текстовым описанием её имени и пути.

Ещё сильнее код можно сократить, используя общий префикс для пути к функциям представления. В нашем примере, каждая строка начиналась с « mysite.views » , т.е. опять дублирование. Мы можем исключить этот общий префикс и передать его в качестве первого аргумента функции patterns() , например так:

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('mysite.views',
                        (r'^hello/$', 'hello'),
                        (r'^time/$', 'current_datetime'),
                        (r'^time/plus/(d{1,2})/$', 'hours_ahead'),
                    )

Следует заметить, что не надо ставить завершающую точку в префикс и не надо ставить точку перед именами функций представления. Всё это будет автоматически сделано Django.

Так какой из этих двух подходов лучше? Это зависит от вашего стиля написания кода и ваших нужд.

Преимущества использования строк вместо объектов:

Краткий код, т.к. не требуется импортировать функции представления.

Более читаемая и управляемая схема URL в случае, если ваши функции представления располагаются в нескольких различных модулях.

Преимущества использования объектов вместо строк:

Простая замена функций представления. Обратитесь к разделу « Wrapping View Functions » FIXME (что-то нет такого в оригинальной книге) в этой главе.

Стиль Python, сохраняются его традиции, такие как передача функции в виде объекта.

Оба подхода правильные. Вы можете смешивать их в одном файле, описывающем схему URL. Это ваш выбор.

Использование множества префиксов для представлений

На практике при использовании строчной методики вы можете столкнуться с ситуацией, когда функции представления в схеме URL не будут иметь общего префикса. Однако, это не помешает вам воспользоваться преимуществами префикса. Просто добавьте нужное количество patterns(), вот так:

                    # Раньше
                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                        (r'^hello/$', 'mysite.views.hello'),
                        (r'^time/$', 'mysite.views.current_datetime'),
                        (r'^time/plus/(\d{1,2})/$', 'mysite.views.hours_ahead'),
                        (r'^tag/(\w+)/$', 'weblog.views.tag'),
                    )

                    # Теперь
                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('mysite.views',
                        (r'^hello/$', 'hello'),
                        (r'^time/$', 'current_datetime'),
                        (r'^time/plus/(\d{1,2})/$', 'hours_ahead'),
                    )

                    urlpatterns += patterns('weblog.views',
                        (r'^tag/(\w+)/$', 'tag'),
                    )

Всё, о чём заботится среда разработки, — это о существовании переменной rlpatterns. Эта переменная может быть создана динамически, как это показано в примере. Следует отметить, что объекты, возвращаемые patterns(), могут быть объединены оператором "+".

Специальные URL в режиме отладки

Говоря о динамическом построении urlpatterns , вы можете пожелать использовать преимущества этой методики для изменения поведения вашей схемы URL при работе Django в режиме отладки. Чтобы осуществить это, просто проверяйте значение параметра DEBUGво время работы приложения, вот так:

                    from django.conf import settings
                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^$', views.homepage),
                        (r'^(\d{4})/([a-z]{3})/$', views.archive_month),
                    )

                    if settings.DEBUG:
                        urlpatterns += patterns('',
                            (r'^debuginfo/$', views.debug),
                        )

В этом примере URL /debuginfo/будет работать, только если параметр DEBUG установлен в True.

Использование именованных групп

Ранее во всех примерах мы использовали простые, не именованные группы регулярных выражений. Мы ограничивали части URL круглыми скобками, которые требовалось обработать функцией представления, как аргументы. Существует возможность использования именованных групп регулярных выражений для получения кусков URL и последующей их передачи функции представления как именованных аргументов.

Именованные аргументы или позиционные аргументы

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

Например, рассмотрим эту простую функцию:

                            def sell(item, price, quantity):
                                print "Selling %s unit(s) of %s at %s" % (quantity, item, price)

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

                            sell('Socks', '$2.50', 6)

Для вызова функции с помощью именованных аргументов следует указать имена и значения аргументов. Следующие операторы эквивалентны предыдущему примеру:

                            sell(item='Socks', price='$2.50', quantity=6)
                            sell(item='Socks', quantity=6, price='$2.50')
                            sell(price='$2.50', item='Socks', quantity=6)
                            sell(price='$2.50', quantity=6, item='Socks')
                            sell(quantity=6, item='Socks', price='$2.50')
                            sell(quantity=6, price='$2.50', item='Socks')

Наконец, вы можете смешивать эти два подхода, пока обязательные аргументы находятся на своих местах. Следующие операторы эквивалентны предыдущим примерам:

                            sell('Socks', '$2.50', quantity=6)
                            sell('Socks', price='$2.50', quantity=6)
                            sell('Socks', quantity=6, price='$2.50')

В регулярных выражениях Python синтаксис для именованных групп регулярных выражений будет таким: (?P<name>pattern), где name является именем группы, а pattern — неким шаблоном.

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

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^articles/(\d{4})/$', views.year_archive),
                        (r'^articles/(\d{4})/(\d{2})/$', views.month_archive),
                    )

Теперь приведём тот же файл, который переписан с использованием именованных групп:

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^articles/(?P<year>\d{4})/$', views.year_archive),
    (r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', views.month_archive),
)

Этот пример аналогичен предыдущему за исключением одного тонкого различия: полученные значения передаются в функции представления в виде именованных аргументов.

Например, при использовании не именованных групп запрос к /articles/2006/03/ будет преобразован к такому вызову функции:

                    month_archive(request, '2006', '03')

Однако при использовании именованных групп тот же самый запрос будет преобразован к такому вызову функции:

                    month_archive(request, year='2006', month='03')

На практике, использование именованных групп позволяет вашей схеме URL быть более очевидной и менее подверженной внесению ошибок. Вы можете менять порядок следования аргументов в ваших функциях представления. Следуя вышеописанным примерам, если нам потребуется так изменить URL, чтобы вставить месяц перед годом, при использовании не именованных групп, нам потребуется не забыть внести соответствующие изменения в порядок аргументов в представлении month_archive. При использовании именованных групп такое изменение порядка параметров в регулярном выражении никак не скажется на работе функции представления.

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

Понимание алгоритма совпадения/группировки

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

Если есть хотя бы один именованный аргумент, то не именованные аргументы игнорируются.

Иначе, передаются все не именованные аргументы в качестве обязательных.

В обоих случаях передаётся дополнительная информация в виде именованного аргумента. Читайте далее для получения подробностей.

Передача дополнительных аргументов в функции представления

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

                    # urls.py

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^foo/$', views.foo_view),
                        (r'^bar/$', views.bar_view),
                    )

                    # views.py

                    from django.shortcuts import render_to_response
                    from mysite.models import MyModel

                    def foo_view(request):
                        m_list = MyModel.objects.filter(is_new=True)
                        return render_to_response('template1.html', {'m_list': m_list})

                    def bar_view(request):
                        m_list = MyModel.objects.filter(is_new=True)
                        return render_to_response('template2.html', {'m_list': m_list})

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

                    # urls.py

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^(foo)/$', views.foobar_view),
                        (r'^(bar)/$', views.foobar_view),
                    )

                    # views.py

                    from django.shortcuts import render_to_response
                    from mysite.models import MyModel

                    def foobar_view(request, url):
                        m_list = MyModel.objects.filter(is_new=True)
                        if url == 'foo':
                            template_name = 'template1.html'
                        elif url == 'bar':
                            template_name = 'template2.html'
                        return render_to_response(template_name, {'m_list': m_list})

Проблема такого решения заключается в том, что URL переносятся в код. Если вы решите переименовать /foo/ в /fooey/, вам потребуется не забыть внести изменения в код функции представления.

Элегантное решение требует включить необязательный параметр в шаблон схемы URL. Каждый шаблон в схеме может включать в себя третий элемент: словарь именованных аргументов для передачи его в функцию представления.

Учитывая это, мы можем переписать наш предыдущий пример так:

                    # urls.py

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        (r'^foo/$', views.foobar_view, {'template_name': 'template1.html'}),
                        (r'^bar/$', views.foobar_view, {'template_name': 'template2.html'}),
                    )

                    # views.py

                    from django.shortcuts import render_to_response
                    from mysite.models import MyModel

                    def foobar_view(request, template_name):
                        m_list = MyModel.objects.filter(is_new=True)
                        return render_to_response(template_name, {'m_list': m_list})

Как можно увидеть, шаблон в схеме URL имеет третий параметр template_name

Такая методика является отличным способом передачи дополнительной информации в функции представления с минимальными затратами. Эта же методика используется в поставляемых с Django приложениях, в основном в его основной системе представлений, которую мы рассмотрим в главе « Генерация данных, отличных от HTML ».

Следующие секции содержат ряд идей о том как вы можете использовать эту методику в своих проектах.

Faking Captured URLconf Values

Допустим, у вас есть набор представлений, которые соответствуют шаблону, наряду с другим URL, который не совпадает с шаблоном, но логика его функции представления аналогична. В этом случае, вы можете «подделать» получение значений из URL с помощью дополнительной опции для обработки этого дополнительного URL той же функцией представления.

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

                        /mydata/jan/01/
                        /mydata/jan/02/
                        /mydata/jan/03/
                        # ...
                        /mydata/dec/30/
                        /mydata/dec/31/

Довольно просто это обработать, используя синтаксис именованных групп:

urlpatterns = patterns('',
    (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)

А сигнатура функции представления станет такой:

                        def my_view(request, month, day):
                        # ....

Это прямолинейный подход, ничего подобного раньше вы не видели. Эта уловка пригодится когда понадобится добавить другой URL, который будет использовать my_view, но её URL не будет обрабатывать month и/или day.

Например, вы можете добавить другой URL, /mydata/birthday/, который будет аналогичен /mydata/jan/06/. В этом случае использование дополнительного параметра даст вам преимущество:

urlpatterns = patterns('',
    (r'^mydata/birthday/$', views.my_view, {'month': 'jan', 'day': '06'}),
    (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)

Секрет в том, что не требуется вносить какие-либо изменения в код функции представления. Функция должна обрабатывать параметры month и day, не важно получены ли они из URL или через дополнительные параметры.

Переход к обобщённым представлениям

Снижение количества дублирующего кода является хорошим тоном разработки программного обеспечения. Например, эти функции, написанные на языке Python:

                        def say_hello(person_name):
                            print 'Hello, %s' % person_name

                        def say_goodbye(person_name):
                            print 'Goodbye, %s' % person_name

мы можем упростить, выделив приветствие в параметр функции:

                        def greet(person_name, greeting):
                            print '%s, %s' % (greeting, person_name)

Вы можете использовать эту философию применимо к представлениям Django, используя дополнительные параметры шаблонов схемы URL.

Учитывая это, вы можете начать создание высокоуровневых абстракций для ваших представлений. Вместо того, чтобы думать «Это представление отображает список объектов Event» или «Этопредставление отображает список объектов BlogEntry», рассматривайте их как два особых случая«Представление, которое отображает список объектов, где тип объекта является переменной».

Для примера рассмотрим следующий код:

                        # urls.py

                        from django.conf.urls.defaults import *
                        from mysite import views

                        urlpatterns = patterns('',
                            (r'^events/$', views.event_list),
                            (r'^blog/entries/$', views.entry_list),
                        )

                        # views.py

                        from django.shortcuts import render_to_response
                        from mysite.models import Event, BlogEntry

                        def event_list(request):
                            obj_list = Event.objects.all()
                            return render_to_response('mysite/event_list.html', {'event_list': obj_list})

                        def entry_list(request):
                            obj_list = BlogEntry.objects.all()
                            return render_to_response('mysite/blogentry_list.html', {'entry_list': obj_list})

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

                        # urls.py

                        from django.conf.urls.defaults import *
                        from mysite import models, views

                        urlpatterns = patterns('',
                            (r'^events/$', views.object_list, {'model': models.Event}),
                            (r'^blog/entries/$', views.object_list, {'model': models.BlogEntry}),
                        )

                        # views.py

                        from django.shortcuts import render_to_response

                        def object_list(request, model):
                            obj_list = model.objects.all()
                            template_name = 'mysite/%s_list.html' % model.__name__.lower()
                            return render_to_response(template_name, {'object_list': obj_list})

Внеся небольшие изменения, мы неожиданно получили образцовое представление, пригодное для многократного использования. Начиная с этого момента, как только нам понадобится представление, которое отображает набор объектов в виде списка, мы можем просто использовать представление object_list, вместо создания ещё одного представления. Теперь рассмотрим, что же мы сделали:

Мы напрямую передаём классы модели, используя параметр model. Словарь с дополнительными параметрами может передавать в представление объект Python любого типа, а не только строки.

Строка model.objects.all() является примеромутиного набора FIXME: « Если это ходит как утка и разговаривает как утка, мы можем считать это уткой. » Следует отметить, что код ничего не знает о типе объекта model, единственное требование — model должна иметь атрибут objects, у которого как раз есть метод all().

Мы используем model.__name__.lower() для того, чтобы определить имя шаблона. В Python каждый класс имеет атрибут __name__, который содержит имя класса. Эта особенность полезна в подобных случаях, когда мы не знаем имя класса во время работы приложения. Например, атрибут __name__ класса BlogEntry содержит строку «BlogEntry».

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

Так как сайты, хранящие свои данные в базах данных, используют одинаковые шаблоны, Django поставляется с набором «общие представления», который используется вышеописанную методику для сохранения вашего времени. Мы рассмотрим встроенные общие представления Django в главе « Генерация данных, отличных от HTML ».

Giving a View Configuration Options

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

Чаще всего изменению подвергаются имена шаблонов:

                        def my_view(request, template_name):
                            var = do_something()
                            return render_to_response(template_name, {'var': var})

Understanding Precedence of Captured Values vs. Extra Options

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

Например:

                        from django.conf.urls.defaults import *
                        from mysite import views

                        urlpatterns = patterns('',
                            (r'^mydata/(?P<id>\d+)/$', views.my_view, {'id': 3}),
                        )

В данном примере параметр id находится и в регулярном выражении и в словаре дополнительных параметров. Параметр из словаря получает преимущество. Это означает, что любой запрос (т.е., /mydata/2/ или /mydata/432432/) будет интерпретирован как-будто бы параметр id установлен в 3, независимо от значения в URL.

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

Использование стандартных аргументов для представления

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

Пример:

# urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^blog/$', views.page),
    (r'^blog/page(?P<num>\d+)/$', views.page),
)

# views.py

def page(request, num="1"):
    # Output the appropriate page of blog entries, according to num.
    # ...

В данном примере оба шаблона URL указывают на одно и то же представление, views.page , но первый шаблон не получает параметров из URL. Если срабатывает первый шаблон, то будет выполнена функция page() будет использовать стандартные значения аргументов(num=1 ). Если срабатывает второй шаблон, функция page() будет использовать значение для параметра num полученное с помощью регулярного выражения.

Заметьте, что в качестве значения по-умолчанию мы указали строку "1", а не число 1. Это для согласованности т.к. любое полученное для num значение будет строкой.

Как объяснялось ранее, обычно используют данную методику совместно с параметрами конфигурации. Этот пример немного улучшает пример из раздела « Giving a View Configuration Options », предоставляя стандартное значение для template_name:

                    def my_view(request, template_name='mysite/my_view.html'):
                        var = do_something()
                        return render_to_response(template_name, {'var': var})

Представления для особых случаев

Иногда так случается, что есть шаблон в вашей схеме URL, который обрабатывает большой набор URL, но вам необходимо реализовать для них особые случаи. Для этого надо просто описать эти случаи первыми.

Например, страницы «добавить объект» на сайте администратора Django представлены в схеме URL такой строкой:

                    urlpatterns = patterns('',
                    # ...
                    ('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'),
                    # ...
                    )

Эта строка совпадает с URL такими как /myblog/entries/add/ и /auth/groups/add/. Однако страница добавления пользователей (/auth/user/add/) является особым случаем — оно не отображает все поля формы, она отображает два поля для ввода пароля и так далее. Мы можем решить эту проблему реализовав особое поведение представления, например так:

                    def add_stage(request, app_label, model_name):
                        if app_label == 'auth' and model_name == 'user':
                            # do special-case code
                        else:
                            # do normal code

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

                    urlpatterns = patterns('',
                        # ...
                        ('^auth/user/add/$', 'django.contrib.admin.views.auth.user_add_stage'),
                        ('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'),
                        # ...
                    )

Теперь вызов /auth/user/add/ будет обработан представлением user_add_stage. Несмотря на то, что этот URL совпадает со вторым шаблоном, он будет обработан первым.

Обработка совпадающего текста

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

(r'^articles/(?P<year>\d{4})/$', views.year_archive),

Аргумент year в этой строке будет передан в представление views.year_archive() в виде строки, не в виде целого числа, несмотря на то, что выражение \d{4} совпадает только со строкой чисел.

Эту особенность следует помнить при разработке приложения. Множество встроенных функций Python принимают объекты строго определённого типа. Общей ошибкой является попытка создать объект datetime.date, используя строковое представление даты, а не целочисленное:

                    >>> import datetime
                    >>> datetime.date('1993', '7', '9')
                    Traceback (most recent call last):
                        ...
                    TypeError: an integer is required
                    >>> datetime.date(1993, 7, 9)
                    datetime.date(1993, 7, 9)

С точки зрения схемы URL это ошибка выглядит так:

                    # urls.py

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                        (r'^articles/(\d{4})/(\d{2})/(\d{2})/$', views.day_archive),
                    )

                    # views.py

                    import datetime

                    def day_archive(request, year, month, day)
                        # The following statement raises a TypeError!
                        date = datetime.date(year, month, day)

А правильно можно написать так:

                    def day_archive(request, year, month, day):
                        date = datetime.date(int(year), int(month), int(day))

Следует отметить, что сам метод int() вызывает исключение ValueError при передаче ему строки, в которой есть не только цифры. Но мы избегаем эту ошибку в нашем случае, так как регулярное выражения в шаблоне URL обеспечивает передачу только цифр.

Определение того, с чем совпадёт шаблон URL

При получении запроса Django пытается найти соответствующий шаблон в схеме URL как для обычной строки Python (не как для Unicode строки). При этом параметры GET и POST или доменное имя не принимаются во внимание. Также не рассматривается начальный слэш, потому что каждое URL имеет начальный слэш.

Например, в запросе http://www.example.com/myapp/ Django будет искать шаблон URL для myapp/. В запросе http://www.example.com/myapp/?page=3 Django будет искать шаблон для myapp/.

Метод запроса (т.е. GET, POST, HEAD) не принимается во внимание при обработке схемы URL. Другими словами, все методы будут обрабатываться теми же функциями. Ветвление обработки запроса в зависимости от использованного метода лежит полностью на функциях представления.

Высокий уровень абстракции в функциях представления

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

                    # urls.py

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        # ...
                        (r'^somepage/$', views.some_page),
                        # ...
                    )

                    # views.py

                    from django.http import Http404, HttpResponseRedirect
                    from django.shortcuts import render_to_response

                    def some_page(request):
                        if request.method == 'POST':
                            do_something_for_post()
                            return HttpResponseRedirect('/someurl/')
                        elif request.method == 'GET':
                            do_something_for_get()
                            return render_to_response('page.html')
                        else:
                            raise Http404()

В этом примере, обработка методов POST и GET функцией some_page() совсем отличается. Все что у них общего это URL /somepage/. Поэтому не очень красиво обрабатывать их одной функцией. Было бы на много лучше, если бы одна функция обрабатывала запрос GET, а другая - POST - и вызывалась только одна, которая необходима для данного типа запросов.

Мы можем реализовать это создав функцию, которая будет вызывать другие функции, руководствуясь определённой логикой. Этот пример покажет как этот подход поможет упростить нашу функцию some_page().

                    # views.py

                    from django.http import Http404, HttpResponseRedirect
                    from django.shortcuts import render_to_response

                    def method_splitter(request, GET=None, POST=None):
                        if request.method == 'GET' and GET is not None:
                            return GET(request)
                        elif request.method == 'POST' and POST is not None:
                            return POST(request)
                        raise Http404

                    def some_page_get(request):
                        assert request.method == 'GET'
                        do_something_for_get()
                        return render_to_response('page.html')

                    def some_page_post(request):
                        assert request.method == 'POST'
                        do_something_for_post()
                        return HttpResponseRedirect('/someurl/')

                    # urls.py

                    from django.conf.urls.defaults import *
                    from mysite import views

                    urlpatterns = patterns('',
                        # ...
                        (r'^somepage/$', views.method_splitter, {'GET': views.some_page_get, 'POST': views.some_page_post}),
                        # ...
                    )

Давайте рассмотрим что она делает:

Мы создали новую функцию представления method_splitter(), которая вызывает другие функции в соответствии с request.method. Она принимает два именованных аргумента GET и POST, которые должны быть функциями представления. Если request.method равен 'GET' - вызывается функция GET, если 'POST' - POST. Если request.method равен чему-то другому(HEAD, например), или GET или POST не указаны, тогда вызывается исключения Http404.

В схеме URL /somepage/ указывает на method_splitter() и передает ей дополнительные параметры - функции представления для GET и POST.

Разделяем функцию представления some_page() на две функции - some_page_get() и some_page_post(). Это на много лучше, чем использовать всю эту логику в одной функции.

Заметьте что теперь эти функции не должны проверять request.method, потому что method_splitter() делает это. (В тот момент, когда some_page_post() будет вызвана, мы можем быть уверенны, что request.method равен 'POST'.) Все же, что бы быть уверенными, ну и для документирования, мы добавили assert, который проверяет что в request.method то, что мы ожидаем.

111222 Now we have ourselves a nice, generic view function that encapsulates the logic of delegating a view by request.method. Nothing about method_splitter() is tied to our specific application, of course, so we can reuse it in other projects.

But, while we’re at it, there’s one way to improve on method_splitter(). As it’s written, it assumes that the GET and POST views take no arguments other than request. What if we wanted to use method_splitter() with views that, for example, capture text from URLs, or take optional keyword arguments themselves?

To do that, we can use a nice Python feature: variable arguments with asterisks. We’ll show the example first, then explain it:

                    def method_splitter(request, *args, **kwargs):
                        get_view = kwargs.pop('GET', None)
                        post_view = kwargs.pop('POST', None)
                        if request.method == 'GET' and get_view is not None:
                            return get_view(request, *args, **kwargs)
                        elif request.method == 'POST' and post_view is not None:
                            return post_view(request, *args, **kwargs)
                        raise Http404

Here, we’ve refactored method_splitter() to remove the GET and POST keyword arguments, in favor of *args and **kwargs (note the asterisks). This is a Python feature that allows a function to accept a dynamic, arbitrary number of arguments whose names aren’t known until runtime. If you put a single asterisk in front of a parameter in a function definition, any positional arguments to that function will be rolled up into a single tuple. If you put two asterisks in front of a parameter in a function definition, any keyword arguments to that function will be rolled up into a single dictionary.

For example, with this function:

                    def foo(*args, **kwargs):
                        print "Positional arguments are:"
                        print args
                        print "Keyword arguments are:"
                        print kwargs

Here’s how it would work:

                    >>> foo(1, 2, 3)
                    Positional arguments are:
                    (1, 2, 3)
                    Keyword arguments are:
                    {}
                    >>> foo(1, 2, name='Adrian', framework='Django')
                    Positional arguments are:
                    (1, 2)
                    Keyword arguments are:
                    {'framework': 'Django', 'name': 'Adrian'}

Bringing this back to method_splitter(), you can see we’re using *args and **kwargs to accept any arguments to the function and pass them along to the appropriate view. But before we do that, we make two calls to kwargs.pop() to get the GET and POST arguments, if they’re available. (We’re using pop() with a default value of None to avoid KeyError if one or the other isn’t defined.)

Wrapping View Functions

Our final view trick takes advantage of an advanced Python technique. Say you find yourself repeating a bunch of code throughout various views, as in this example:

                    def my_view1(request):
                        if not request.user.is_authenticated():
                            return HttpResponseRedirect('/accounts/login/')
                        # ...
                        return render_to_response('template1.html')

                    def my_view2(request):
                        if not request.user.is_authenticated():
                            return HttpResponseRedirect('/accounts/login/')
                        # ...
                        return render_to_response('template2.html')

                    def my_view3(request):
                        if not request.user.is_authenticated():
                            return HttpResponseRedirect('/accounts/login/')
                        # ...
                        return render_to_response('template3.html')

Here, each view starts by checking that request.user is authenticated — that is, the current user has successfully logged into the site — and redirects to /accounts/login/ if not. (Note that we haven’t yet covered request.user — « Средства от других разработчиков » does — but, as you might imagine, request.user represents the current user, either logged-in or anonymous.)

It would be nice if we could remove that bit of repetitive code from each of these views and just mark them as requiring authentication. We can do that by making a view wrapper. Take a moment to study this:

                    def requires_login(view):
                        def new_view(request, *args, **kwargs):
                            if not request.user.is_authenticated():
                                return HttpResponseRedirect('/accounts/login/')
                            return view(request, *args, **kwargs)
                        return new_view

This function, requires_login, takes a view function (view) and returns a new view function (new_view). The new function, new_view is defined within requires_login and handles the logic of checking request.user.is_authenticated() and delegating to the original view (view).

Now, we can remove the if not request.user.is_authenticated() checks from our views and simply wrap them with requires_login in our URLconf:

                    from django.conf.urls.defaults import *
                    from mysite.views import requires_login, my_view1, my_view2, my_view3

                    urlpatterns = patterns('',
                        (r'^view1/$', requires_login(my_view1)),
                        (r'^view2/$', requires_login(my_view2)),
                        (r'^view3/$', requires_login(my_view3)),
                    )

This has the same effect as before, but with less code redundancy. Now we’ve created a nice, generic function — requires_login that we can wrap around any view in order to make it require login.

Подключение других схем URL

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

В любом случае ваша схема может «подключать» другие схемы. По существу происходит «внедрение» набора URL после этой команды. Например:

                from django.conf.urls.defaults import *

                urlpatterns = patterns('',
                    (r'^weblog/', include('mysite.blog.urls')),
                    (r'^photos/', include('mysite.photos.urls')),
                    (r'^about/$', 'mysite.views.about'),
                )

Здесь есть важный момент: регулярные выражения в этом примере, которые указывают на include() не имеют символа $, но имеют завершающий слэш. Когда Django встречает include() он отрезает совпавшую часть от URL и отправляет оставшуюся часть в подключенные схемы для дальнейшей обработки.

Продолжая работу над нашим примером [11] :

                from django.conf.urls.defaults import *

                urlpatterns = patterns('',
                    (r'^(\d\d\d\d)/$', 'mysite.blog.views.year_detail'),
                    (r'^(\d\d\d\d)/(\d\d)/$', 'mysite.blog.views.month_detail'),
                )

Используя эти две схемы URL, покажем как будут обработаны тестовые запросы:

/weblog/2007/: В первом наборе привязок совпадёт шаблон r'^weblog/'. Так как для этого шаблона используется include(), Django отрежет совпавший текст, в данном случае это будет weblog/. Оставшаяся часть URL(т.е. 2007/) совпадёт с первым шаблоном в mysite.blog.urls.

/weblog//2007/: В первом наборе привязок совпадёт шаблон r'^weblog/'.Так как для этого шаблона используется include(), Django отрежет совпавший текст, в данном случае это будет weblog/. Оставшаяся часть URL (т.е./2007/, с ведущим слэшом) не совпадёт ни с одним шаблоном в mysite.blog.urls.

/about/: Этот запрос будет обработан представлением mysite.views.about в первом наборе привязок, показывая, что вы можете смешивать подключаемые шаблоны со встроенными.

Как совпавшие параметры работают совместно с include()

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

# root urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
)

# foo/urls/blog.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'foo.views.blog_index'),
    (r'^archive/$', 'foo.views.blog_archive'),
)

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

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

Как работают дополнительные параметры совместно с include()

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

Например, следующие два набора привязок функционально идентичны.

Первый:

                    # urls.py

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                    (r'^blog/', include('inner'), {'blogid': 3}),
                    )

                    # inner.py

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                    (r'^archive/$', 'mysite.views.archive'),
                    (r'^about/$', 'mysite.views.about'),
                    (r'^rss/$', 'mysite.views.rss'),
                    )

Второй:

                    # urls.py

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                    (r'^blog/', include('inner')),
                    )

                    # inner.py

                    from django.conf.urls.defaults import *

                    urlpatterns = patterns('',
                    (r'^archive/$', 'mysite.views.archive', {'blogid': 3}),
                    (r'^about/$', 'mysite.views.about', {'blogid': 3}),
                    (r'^rss/$', 'mysite.views.rss', {'blogid': 3}),
                    )

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

[11] См. предыдущий пример. Данный код подключается командой include('mysite.blog.urls').

Глава 9. Базовые представления

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

И снова повторимся: со своей худшей стороны веб разработка является нудным и монотонным процессом. Также мы рассказывали, что Django пытается избавить вас от этой монотонности на уровнях моделей и шаблонов, но остаётся ещё уровень представлений.

Базовые представления Django были разработаны для облегчения решения этой проблемы. Они следуют общим определённым идиомам и шаблонам, которые были найдены во время разработки представлений, и позволяют быстро создавать общие представления на основе минимума кода. Действительно, почти каждый пример представления в предыдущих главах может быть переписан с использованием базовых представлений.

Глава « Усовершенствованные представления и схемы URL » кратко касалась вопроса как можно сделать представление «базовым». При решении этой задачи мы можем выделить общие шаги, такие как отображение списка объектов, создание кода, который может выводить список любых объектов. Тогда рассматриваемую модель можно передать в виде дополнительного параметра в файл со схемой URL.

Django поставляется с базовыми представлениями:

Выполнение общих «простых» задач: перенаправление на другую страницу и обработка шаблона страницы.

Отображение списка и подробной страницы для одного объекта. Представления event_list и entry_list из главы « Усовершенствованные представления и схемы URL » являются примерами представлений, которые отображают списки. Страница для одного события является примером, который мы называем «детальным» представлением.

Отображение объектов для работы с датой на архивных страницах, ассоциированных подробностей и страниц с «свежей» информации. Архивы блогов о Django (http://www.djangoproject.com/weblog/) с доступом по годам, месяцам и дням созданы на этой основе.

Позволяет пользователям создавать, изменять и удалять объекты — с и без авторизации.

Объединённые вместе, эти представления предоставляют простой интерфейс для решения наиболее общих задач с которыми сталкиваются разработчики.

Использование базовых представлений

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

Ниже представлен простой файл со схемой URL, который можно использовать для представления статичной страницы «Информация о»:

from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    })
)

Несмотря на то, что на первый взгляд здесь есть нечто «магическое» — посмотрите, представление без кода! — это то же, что и примеры из главы « Усовершенствованные представления и схемы URL »: представление direct_to_template просто получает информацию из словаря дополнительных параметров и использует эту информацию при отображении представления.

Так как это базовое представление, как и все остальные, является обычным представлением, которое функционирует подобно другим, мы можем использовать его внутри наших собственных представлений. В качестве примера, давайте улучшим наш пример «Информация о» для вывода для URL вида /about/что-то/ статических страниц about/что-то.html. Мы сделаем это сначала изменив привязку:

from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
from mysite.books.views import about_pages

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    }),
    ('^about/(\w+)/$', about_pages),
)

Затем мы напишем представление about_pages:

from django.http import Http404
from django.template import TemplateDoesNotExist
from django.views.generic.simple import direct_to_template

def about_pages(request, page):
    try:
        return direct_to_template(request, template="about/%s.html" % page)
    except TemplateDoesNotExist:
        raise Http404()

Здесь мы рассматриваем direct_to_template как любую другую функцию. Так как она возвращает HttpResponse, мы можем отдавать его без обработки. Есть только одна хитрость — работа при отсутствии шаблонов. Нам не нужна ошибка, которая при этом возникнет, следовательно мы ловим исключения TemplateDoesNotExist и возвращаем вместо этого ошибку 404.

Есть ли проблемы с безопасностью?

Внимательные читатели могли отметить возможную дыру в безопасности: мы создаём имя шаблона, используя информацию от браузера (template="about/%s.html" % page). На первый взгляд, это похоже на уязвимость выход из каталога (directory traversal) (обратитесь за подробностями к главе « Безопасность »). Но так ли это?

Не совсем. Специально подобранное значение параметра page может привести к выходу из каталога, но несмотря на то, что page получается из URL, не каждое значение будет обработано. Вся хитрость в шаблоне URL: мы используем регулярное выражение \w+ для совпадения с page частью URL, а \w принимает только буквы и числа. Следовательно, любые вредоносные символы (такие как точки и слэши) будут отброшены в момент поиска функции представления для обработки переданного URL.

Базовые представления объектов

Функция direct_to_template полезна, но базовые представления Django показывают все свои возможности при отображении информации из базы данных. Так как это является частой задачей, Django поставляется с встроенными базовыми представлениями, которые значительно упрощают генерацию списка или страниц с детальной информацией.

Давайте взглянем на одно из этих базовых представлений: представление «список объектов». Мы используем объект Publisher из главы « Модели »:

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    class Meta:
        ordering = ["-name"]

    class Admin:
        pass

    def __unicode__(self):
        return self.name

Чтобы создать страницу со списком всех издателей[12] мы использовали такой файл со схемой URL:

from django.conf.urls.defaults import *
from django.views.generic import list_detail
from mysite.books.models import Publisher

publisher_info = {
    "queryset" : Publisher.objects.all(),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

Это весь код на языке Python, который нам потребуется написать. Однако, нам ещё надо создать шаблон. Мы можем явно указать представлению object_list какой шаблон надо использовать, добавив параметр template_name в словарь дополнительных параметров. Но в случае отсутствия явного указания шаблона Django будет искать шаблон, соответствующий имени объекта. В этом случае, таким шаблоном будет books/publisher_list.html — часть books идёт от имени приложения, которое определяет модель, а publisher является именем модели приведённым к нижнему регистру.

Этот шаблон будет обработан с учётом контекста, содержащего переменную object_list со списком всех объектов Book. Очень простой шаблон может выглядеть так:

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

Вот всё что надо. Все преимущества базовых представления идут от изменения словаря с информацией, который передаётся такому представлению. Приложение « Справочник по базовым представлениям » документирует все базовые представления и все их параметры. Остальная часть главы описывает общие способы работы и настройки базовых представлений.

[12] В оригинальной книге, здесь они создают страницу со списком всех книг. Хотя, судя по коду, книги тут ни при чём.

Расширение базовых представлений

Не существует задач, для которых базовые представления не могут существенно ускорить разработку. Тем не менее, в большинстве проектов наступает момент когда базовые представления перестают удовлетворять текущим требованиям. Действительно, наиболее общим вопросом, задаваемым новыми адептами Django, является «как заставить базовые представления обрабатывать широкий спектр ситуаций».

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

Создаём дружественные контексты для шаблонов

Вы могли отметить, что тестовый шаблон списка издателей хранит все книги в переменной object_list. Пока всё работает прекрасно, не всё так «дружественно» по отношению к авторам шаблона: они должны «просто знать», что в данном случае они работают с книгами. Лучшим именем для этой переменной будет publisher_list, тогда содержимое переменной более очевидно.

Мы можем легко изменить имя этой переменной с помощью аргумента template_object_name:

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

Предоставлять эту полезную переменную всегда является хорошей идеей. Ваши коллеги, которые занимаются разработкой шаблонов, будут вам благодарны.

Добавление дополнительного контекста

Часто вам просто требуется предоставить немного больше информации, чем это позволяет базовое представление. Например, подумайте об отображении списка всех других издателей на каждой странице определённого издателя. Базовое представление object_detail предоставляет имя издателя для контекста, но кажется, что нет способа для этого шаблона получить список всех издателей.

Но это не так: все базовые представления могут принимать дополнительный параметр — extra_context. Этим параметром является словарь дополнительных объектов, которые будет добавлен в контекст шаблона. Таким образом, для передачи списка всех издателей в представление мы должны использовать словарь, подобный этому:

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : Book.objects.all()}
}

Этот код заполнит переменную {{ book_list }} в контексте шаблона. Этот способ может быть использован для передачи любой информации в шаблон базового представления. Это очень удобно.

Тем не менее, здесь есть малозаметная ошибка — можете её найти?

Проблема проявляется в момент вычисления запросов в extra_context. Так как этот пример является фрагментом файла со схемой URL, то Book.objects.all()[13] будет вычислен только однажды, при первой загрузке данного файла. После того, как вы добавите или удалите издателя, вы обнаружите, что базовое представление не отражает эти изменения до момента перезагрузки веб сервера (см. "Caching and QuerySets" в приложении « Справочник по API взаимодействия с базой данных » для получения большего объёма информации по вопросам кэширования и вычисления QuerySet).

Замечание

Эта проблема не затрагивает аргумент queryset базового представления. Так как Django знает, что этот набор данных никогда не должен подвергаться кэшированию, базовое представление берёт на себя задачу очистки кэша после каждого использования представления.

Решение заключается в использовании обработчика (callback) в extra_context вместо значения. Любая функция, передаваемая extra_context, будет вычислена при каждом вызове представления, а не один раз. Вы можете сделать это с помощью явно определённой функции:

def get_books():
    return Book.objects.all()

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : get_books}
}

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

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : Book.objects.all}
}

Следует отметить отсутствие скобок после Book.objects.all. Таким образом мы ссылаемся на функцию, не производя её вызов (вызов делает базовое представление позже).

Просмотр поднабора объектов

Пришло время рассмотреть этот queryset, который мы используем уже давно. Большинство базовых представлений принимают такой аргумент — именно так представление узнает, какой набор объектов надо отобразить (определение наборов данных было дано в главе « Выборка объектов », а в приложении « Справочник по API взаимодействия с базой данных » приведены все подробности).

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

book_info = {
    "queryset" : Book.objects.all().order_by("-publication_date"),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/$', list_detail.object_list, book_info),
)

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

apress_books = {
    "queryset": Book.objects.filter(publisher__name="Apress Publishing"),
    "template_name" : "books/apress_list.html"
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/apress/$', list_detail.object_list, apress_books),
)

Следует отметить, что совместно с отсортированным queryset мы также используем своё имя для шаблона. Если мы не будем так делать, то базовое представление будет использовать тот же шаблон как «vanilla FIXME» список объектов, что может не совпадать с вашими желаниями.

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

Замечание

Если вы получите ошибку 404 при обращении к books/apress/, удостоверьтесь, что у вас действительно есть издатель с именем «Apress Publishing». Базовые представления имеют параметр allow_empty как раз для этого случая. Подробности смотрите в приложении « Справочник по базовым представлениям ».

Сложная фильтрация с помощью функций-обработчиков

Часто бывает необходимо реализовать фильтрацию объектов по какому-либо ключу из URL. Ранее мы жёстко прописывали имя издателя в привязку URL, но что делать, если нам требуется создать представление, которое отображает все книги одного определённого издателя? Мы можем «обернуть» базовое представление object_list для того, чтобы не писать много кода вручную. Как обычно, мы начнём с файла привязок:

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/(\w+)/$', books_by_publisher),
)

Затем, следует написать представление books_by_publisher:

from django.http import Http404
from django.views.generic import list_detail
from mysite.books.models import Book, Publisher

def books_by_publisher(request, name):

    # Ищем издателя (и вызываем 404, если ничего не нашли).
    try:
        publisher = Publisher.objects.get(name__iexact=name)
    except Publisher.DoesNotExist:
        raise Http404

    # Используем представление object_list view.
    return list_detail.object_list(
        request,
        queryset = Book.objects.filter(publisher=publisher),
        template_name = "books/books_by_publisher.html",
        template_object_name = "books",
        extra_context = {"publisher" : publisher}
    )

Это работает потому что в базовых представлениях нет ничего особенного — это простые функции языка Python. Подобно любой другой функции представления, базовые представления принимают определённый набор аргументов и возвращают объекты HttpResponse. Следовательно, невероятно просто оборачивать небольшие функции вокруг базового представления, которые выполняют дополнительную работу перед (или после, см. следующую секцию) передачи управления базовому представлению.

Замечание

Следует отметить, что в предыдущем примере мы передавали текущего издателя, который был указан в extra_context. Это положительный аспект поведения обёрток. Они позволяют шаблонам знать о родительском объекте.

Выполнение дополнительной работы

Последняя задача, которую мы рассмотрим в этой главе, — как выполнять некоторую обработку данных до или после вызова базового представления.

Представьте, что у нас есть поле last_accessed в объекте Author, которое мы используем для хранения информации о времени последнего доступа к информации об авторе. Базовое представление object_detail, естественно, ничего не знает об этом поле, но как и раньше мы можем легко создать отдельное представление для обработки этого поля.

Сначала, нам надо добавить соответствующую привязку URL, которая будет указывать на наше представление:

from mysite.books.views import author_detail

urlpatterns = patterns('',
    #...
    (r'^authors/(?P<author_id>\d+)/$', author_detail),
)

Затем, пишем обработчик:

import datetime
from mysite.books.models import Author
from django.views.generic import list_detail
from django.shortcuts import get_object_or_404

def author_detail(request, author_id):
    # Получаем объект или вызываем ошибку 404.
    author = get_object_or_404(Author, pk=author_id)

    # Записываем текущую дату.
    author.last_accessed = datetime.datetime.now()
    author.save()

    # Отображаем страницу.
    return list_detail.object_detail(
        request,
        queryset = Author.objects.all(),
        object_id = author_id,
    )

Замечание

Этот код не будет работать пока вы не добавите поле last_accessed к модели Author и не создадите шаблон books/author_detail.html.

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

def author_list_plaintext(request):
    response = list_detail.object_list(
        request,
        queryset = Author.objects.all(),
        mimetype = "text/plain",
        template_name = "books/author_list.txt"
    )
    response["Content-Disposition"] = "attachment; filename=authors.txt"
    return response

Это работает, потому что базовое представление возвращает простые объекты HttpResponse, которые могут рассматриваться как словари при установке HTTP заголовков. Заголовок Content-Disposition, между прочим, указывает браузеру, что поток данных следует скачать и сохранить, вместо отображения в окне браузера.

[13] В оригинальной книге тут конкретно гонят. Пришлось писать этот кусок самому.

Глава 10. Расширения для шаблонной системы

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

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

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

Если вы рассматриваете возможность использования шаблонной системы Django в другом приложении (т.е., без остальной части среды разработки), прочитайте раздел « Работа в автономном режиме » этой главы.

Обзор языка шаблонов

Сначала давайте быстро рассмотрим ряд терминов, введённых в главе « Шаблоны »:

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

Блочным тегом называют символ внутри шаблона, который производит какое-то действие. Это определение умышленно сделано неясным. Например, блочный тег может производить контент, работать как управляющая структура (как оператор if или loop), получать информацию из базы данных или разрешать доступ к другим шаблонным тегам.

Блочные теги указываются внутри {% и %}:

{% if is_logged_in %}
  Спасибо за аутентификацию!
{% else %}
  Пожалуйста, назовите себя.
{% endif %}

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

Переменные указываются внутри {{ и }}:

Меня зовут {{ first_name }} {{ last_name }}.

Контекстом называют соответствие имя - переменная (аналогичное словарю языка Python), которое передаётся в шаблон.

Шаблон обрабатывает контекст, заменяя переменные значениями из этого контекста и исполняя все блочные теги.

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

Процессоры RequestContext и Context

Для обработки шаблона вам необходим контекст. Обычно это экземпляр django.template.Context, но в поставку Django также входит специальный подкласс — django.template.RequestContext, который работает немного иначе. Класс RequestContext по умолчанию добавляет группу переменных в контекст шаблона - такие как объект HttpRequest или информацию о текущем пользователе.

Используйте RequestContext когда вы не желаете указывать тот же набор переменных в серии шаблонов. Например, рассмотрим эти четыре представления:

from django.template import loader, Context

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am view 1.'
    })
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the second view.'
    })
    return t.render(c)

def view_3(request):
# ...
    t = loader.get_template('template3.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the third view.'
    })
    return t.render(c)

def view_4(request):
    # ...
    t = loader.get_template('template4.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the fourth view.'
    })
    return t.render(c)

Следует отметить, что мы явно не используем сокращение render_to_response() — мы вручную загружаем шаблоны, конструируем объекты с контекстом и обрабатываем шаблоны. Мы делаем так с целью облегчения понимания вами этих примеров.

Каждое представление принимает три одинаковые переменные, app, user и ip_address, для своего шаблона. Неплохо было бы убрать эту избыточность.

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

Самый низкоуровневый метод использования контекстных процессоров заключается в создании нескольких процессоров и в передачи им RequestContext. Перепишем вышеприведённые примеры с использованием контекстных процессоров:

from django.template import loader, RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = RequestContext(request, {'message': 'I am view 1.'},
            processors=[custom_proc])
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = RequestContext(request, {'message': 'I am the second view.'},
            processors=[custom_proc])
    return t.render(c)

def view_3(request):
    # ...
    t = loader.get_template('template3.html')
    c = RequestContext(request, {'message': 'I am the third view.'},
            processors=[custom_proc])
    return t.render(c)

def view_4(request):
    # ...
    t = loader.get_template('template4.html')
    c = RequestContext(request, {'message': 'I am the fourth view.'},
            processors=[custom_proc])
    return t.render(c)

Разберём этот код:

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

Мы изменили четыре функции представления так, чтобы они использовали RequestContext вместо Context. Есть два различия в методике построения контекста. Первое, RequestContext требует, чтобы первым аргументом был HttpRequest, который передаётся в функцию представления первым аргументом (request). Второе, RequestContext принимает необязательный аргумент processors, который может быть списком или кортежем с функциями контекстного процессора, которые надо будет использовать. В нашем случае мы передали custom_proc — наш процессор, который мы определили ранее.

Теперь каждое представление не нуждается больше во включении переменных app, user и ip_address в её контекстную конструкцию, потому что они предоставляются функцией custom_proc.

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

В главе « Шаблоны » мы описали сокращение render_to_response(), которое позволяет вам не вызывать loader.get_template(), затем создавать Context, а затем вызывать метод render() для шаблона. Для того, чтобы продемонстрировать низкоуровневую работу контекстных процессоров, вышеприведённые примеры не используют сокращение render_to_response(). Но это возможно и предпочтительно. Сделаем это с помощью аргумента context_instance:

from django.shortcuts import render_to_response
from django.template import RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    return render_to_response('template1.html',
        {'message': 'I am view 1.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

def view_2(request):
    # ...
    return render_to_response('template2.html',
        {'message': 'I am the second view.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

def view_3(request):
    # ...
    return render_to_response('template3.html',
        {'message': 'I am the third view.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

def view_4(request):
    # ...
    return render_to_response('template4.html',
        {'message': 'I am the fourth view.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

Здесь мы привели в порядок код обработки шаблона в каждой функции представления.

This is an improvement, but, evaluating the conciseness of this code, we have to admit we're now almost overdosing on the other end of the spectrum. FIXME. Мы убрали избыточность в данных (переменные нашего шаблона) за счёт добавления избыточности в код (в вызов processors). Использование контекстных процессоров не избавляет вас от необходимости набирать много строк кода.

По этим причинам Django предоставляет поддержку для глобальных контекстных процессоров. Параметр TEMPLATE_CONTEXT_PROCESSORS[14] определяет то, какой процессор должен всегда применяться к RequestContext. Он исключает необходимость указания processors при каждом использовании RequestContext.

По умолчанию параметр TEMPLATE_CONTEXT_PROCESSORS определён так:

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
)

Этот параметр представляет собой кортеж вызываемых сущностей FIXME???, которые используют тот же интерфейс, что и вышеописанная функция custom_proc — функции, принимающие объект запроса и возвращающие словарь элементов, который затем включается в контекст. Следует отметить, что значения параметра TEMPLATE_CONTEXT_PROCESSORS указаны в виде строк, которые определяют процессоры (они должны быть доступны интерпретатору языка Python).

Процессоры применяются по порядку. Таким образом, если один процессор добавляет переменную в контекст, а затем второй процессор добавляет в контекст переменную с таким же именем, то второе значение перезапишет первое.

Django предоставляет ряд простых контекстных процессоров, включая активные по умолчанию:

Процессор django.core.context_processors.auth

Если параметр TEMPLATE_CONTEXT_PROCESSORS содержит этот процессор, то каждый объект RequestContext будет содержать следующие переменные:

user: Экземпляр класса django.contrib.auth.models.User, представляющий текущего авторизованного пользователя. Если пользователь не авторизован, то это будет экземпляр класса AnonymousUser.

messages: Список сообщений (в виде строк) для текущего авторизованного пользователя. С технической точки зрения, эта переменная вызывает request.user.get_and_delete_messages() для каждого запроса. Этот метод получает пользовательские сообщения и удаляет их из базы данных.

perms: Экземпляр класса django.core.context_processors.PermWrapper, который представляет собой права текущего авторизованного пользователя.

В главе « Сессии, пользователи и регистрация » предоставлена информацию о пользователях, их правах и сообщениях.

django.core.context_processors.debug

Этот процессор помещает отладочную информацию в слой шаблона. Если параметр TEMPLATE_CONTEXT_PROCESSORS содержит этот процессор, то каждый экземпляр класса RequestContext будет содержать следующие переменные:

debug: Значение параметра DEBUG (True или False). Вы можете использовать эту переменную в шаблоне для определения работы в режиме отладки.

sql_queries: Список словарей вида {'sql': ..., 'time': ...} представляющих каждый SQL запрос, который был выполнен во время обработки запроса и затраченное на это время. Порядок запросов в списке соответствует реальному порядку их выполнения.

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

Параметр DEBUG установлен в True.

Запрос пришёл с IP адреса указанного в списке INTERNAL_IPS.

django.core.context_processors.i18n

Если этот процессор включен, то каждый экземпляр класса RequestContext будет содержать следующие переменные:

LANGUAGES: Значение параметра LANGUAGES файла конфигурации.

LANGUAGE_CODE: Будет содержать значение request.LANGUAGE_CODE, если оно существует. В противном случае — значение параметра LANGUAGE_CODE файла конфигурации.

В приложении « Параметры конфигурации » об этих двух параметрах написано подробнее.

django.core.context_processors.request

Если этот процессор включен, то каждый экземпляр класса RequestContext будет содержать переменную request, которая будет указывать на текущий экземпляр класса HttpRequest. Следует отметить, что этот процессор не включен по умолчанию, вам потребуется активировать его самостоятельно.

Как написать свой процессор?

Вот несколько советов:

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

Следует помнить, что любой контекстный процессор, указанный в TEMPLATE_CONTEXT_PROCESSORS, будет доступен в любом шаблоне, подчинённом данному файлу конфигурации. Таким образом, выбирайте имена переменных так, чтобы они не конфликтовали с именами переменных в ваших шаблонах. Так как имена переменных чувствительны к регистру, неплохо будет использовать прописные (т.е., большие) буквы для переменных, которые предоставляет процессор.

Безразлично, где на файловой системе располагается контекстные процессоры, пока они находятся в пути интерпретатора. Учитывая это, удобно хранить их в файле context_processors.py внутри вашего приложения или проекта.

[14] См. файл settings.py или глобальный файл конфигурации.

Загрузка шаблонов

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

Django может загружать шаблоны двумя способами:

django.template.loader.get_template(template_name): Метод get_template() возвращает скомпилированный шаблон (объект Template) для указанного шаблона. Если шаблон не существует, вызывается исключение TemplateDoesNotExist.

django.template.loader.select_template(template_name_list): Метод select_template() подобен методу get_template(), но принимает список имён шаблонов. Метод возвращает первый существующий шаблон из этого списка. Если ни одного шаблона не существует, вызывается исключение TemplateDoesNotExist.

Как было рассказано в главе « Шаблоны », каждая из этих функций по умолчанию использует параметр TEMPLATE_DIRS при загрузке шаблонов. С технической точки зрения, эти функции делегируют свою работу загрузчику шаблонов.

Некоторые из загрузчиков отключены по умолчанию, но вы можете активировать их, отредактировав параметр TEMPLATE_LOADERS. Этот параметр должен содержать кортеж строк, где каждая строка будет представлять загрузчик. Следующие шаблонные загрузчики поставляются с Django:

django.template.loaders.filesystem.load_template_source: Этот загрузчик подгружает шаблоны с файловой системы, учитывая параметр TEMPLATE_DIRS. Этот загрузчик включен по умолчанию.

django.template.loaders.app_directories.load_template_source: Этот загрузчик подгружает шаблоны из приложений Django на файловой системе. Для каждого приложения указанного в параметре INSTALLED_APPS, загрузчик ищет каталог templates. Если такой каталог существует, Django ищет шаблоны в нём.

Это означает, что вы можете хранить шаблоны внутри отдельных приложений, упрощая процесс распространения приложений со стандартными шаблонами. Например, если параметр INSTALLED_APPS содержит ('myproject.polls', 'myproject.music'), то метод get_template('foo.html') будет искать шаблоны по порядку:

/path/to/myproject/polls/templates/foo.html

/path/to/myproject/music/templates/foo.html

Следует отметить, что загрузчик выполняет некую оптимизацию при первой загрузке, он кэширует список приложений из параметра INSTALLED_APPS, которые имеют свой каталог templates.

Этот загрузчик включен по умолчанию.

django.template.loaders.eggs.load_template_source: Этот загрузчик аналогичен app_directories, но подгружают шаблоны из питонских яиц ;), а не с файловой системы. Этот загрузчик отключен по умолчанию. Если вы используете «яйца» для распространения своих приложений, вам потребуется активировать его.

Django использует загрузчики шаблонов в порядке их определения в параметре TEMPLATE_LOADERS, перебирая их, пока не найдёт подходящий.

Расширение шаблонной системы

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

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

Создание шаблонной библиотеки

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

Создание шаблонной библиотеки состоит из двух шагов:

Во-первых, определитесь какое приложение Django должно будет содержать шаблонную библиотеку. Если вы создали приложение с помощью python manage.py startapp, вы можете расположить её там или вы можете создать другое приложение, полностью ориентированное на хранение библиотеки.

Независимо от вашего выбора, следует удостовериться, что вы добавили приложение в параметр INSTALLED_APPS. Мы ещё вернёмся к этому.

Во-вторых, создайте каталог templatetags в соответствующем пакете приложения Django. Он должен быть на том же уровне, что и файлы models.py, views.py и так далее. Например:

books/
    __init__.py
    models.py
    templatetags/
    views.py

Создайте два пустых файла в этом каталоге: файл __init__.py, чтобы интерпретатор Python знал, что это пакет для него; и файл, который будет содержать определения ваших тегов и фильтров. Имя последнего файла можете выбрать сами. Например, если ваши теги и фильтры находятся в файле poll_extras.py, вам придётся писать в шаблоне так:

{% load poll_extras %}

Тег {% load %} просматривает содержимое параметра INSTALLED_APPS в файле конфигурации и просто разрешает загрузку шаблонных библиотек из установленных приложений. Это особенность предназначена для обеспечения безопасности, она позволяет размещать код Python во многих шаблонных библиотеках на одном компьютере без разрешения доступа ко всем им для каждой инсталляции Django.

Если вы разрабатываете шаблонную библиотеку, которая не связана ни с одной определённой моделью или представлением, то правильно и достаточно стандартно иметь пакет приложения, который содержит только пакет templatetags. Нет никакого ограничения на количество модулей, которые вы можете разместить в пакете templatetags. Просто не забывайте, что оператор {% load %} будет загружать теги и фильтры по имени данного модуля, а не по имени приложения.

После создания такого модуля, вам потребуется написать немного кода, в зависимости от того, тег вы реализуете или фильтр.

Для того, чтобы быть правильной библиотекой тегов, модуль должен содержать в себе переменную register, которая является экземпляром template.Library. Этот экземпляр класса Library является структурой данных, в которой зарегистрированы все теги и фильтры. Таким образом, ваш модуль должен начинаться примерно так:

from django import template

register = template.Library()

Замечание

Ряд хороших примеров по разработке тегов и фильтров как обычно находится в исходном коде Django. Изучите файлы django/template/defaultfilters.py и django/template/defaulttags.py. Некоторые приложения в django.contrib также содержат шаблонные библиотеки.

После создания переменной register, вы будете использовать её при создании шаблонных фильтров и тегов.

Разработка своих шаблонных фильтров

Фильтры это обычные функции языка Python, которые могут принимать от одного до двух аргументов:

Значение переменной (вход);

Значение аргумента, который может иметь значение по умолчанию или не иметь.

Например, в фильтре {{ var|foo:"bar" }}, фильтру foo будет передано содержимое переменной var и аргумент "bar".

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

Пример определения фильтра:

def cut(value, arg):
    "Удаляет все значения аргумента arg из строки value"
    return value.replace(arg, '')

А это пример того, как можно использовать данный фильтр:

{{ somevariable|cut:"0" }}

Большинство фильтров не принимают аргументы. В этом случае, просто не используйте второй аргумент в вашей функции:

def lower(value): # Only one argument.
    "Преобразовывает регистр строки в строчный"
    return value.lower()

После определения своего фильтра требуется зарегистрировать его в вашем экземпляре класса Library, чтобы он стал доступен через шаблонный язык Django:

register.filter('cut', cut)
register.filter('lower', lower)

Метод filter() класса Library принимает два аргумента:

Имя фильтра в виде строки;

Саму функцию фильтра.

Если вы используете Python версии 2.4 и выше, то вы можете воспользоваться для этого декоратором register.filter():

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

Если вы опустите аргумент name, как это сделано во втором примере, Django будет использовать имя функции в качестве имени фильтра.

Ниже приведён полный пример шаблонной библиотеки, предоставляющей фильтр cut:

from django import template

register = template.Library()

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

Разработка своих шаблонных тегов

Теги более сложны в реализации, чем фильтры, потому что теги могут делать практически всё.

В главе « Шаблоны » было описано как работает шаблонная система, два шага: компиляция и рендеринг. Для того, чтобы определить свой шаблонный тег, вам понадобится указать Django как управлять этими шагами при обработке вашего тега.

При компиляции шаблона Django разбирает его текст на узлы. Каждый узел является экземпляром django.template.Node и имеет метод render(). Следовательно, скомпилированный шаблон является списком объектов Node.

Когда вы вызываете метод render() для скомпилированного шаблона, сам шаблон вызывает метод render() для каждого объекта Node в своём списке узлов, передавая им текущий контекст. Результаты рендеринга узлов будут объединены вместе. Следовательно, для того, чтобы определить свой шаблонный тег вы указываете метод преобразования тега в узел и метод рендеринга для этого тега.

Далее в секциях мы рассмотрим все шаги необходимые для создания своего тега:

Определение функции компиляции

Для каждого встречающегося тега шаблонный парсер вызывает функцию, передавая ей содержимое тега и ссылку на себя. Эта функция отвечает за возвращение экземпляра класса Node, построенного по переданной информации.

Например, давайте реализуем шаблонный тег {% current_time %}, который отображает текущую дату и время, отформатированную в соответствии с параметром тега, используя синтаксис strftime (см. http://docs.python.org/library/datetime.html#datetime.date.strftime). Неплохо будет сначала продумать синтаксис тега. В нашем случае, пусть будет так:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>

Замечание

Конечно, этот шаблонный тег является лишним, стандартный тег Django {% now %} делает то же самое и с более простым синтаксисом. Данный шаблонный тег представлен здесь лишь в целях обучения.

Парсер должен выделять параметр и создавать объект Node для этой функции:

from django import template

def do_current_time(parser, token):
    try:
        # метод split_contents() знает, что не надо разделять строки в кавычках
        tag_name, format_string = token.split_contents()
    except ValueError:
        # камрады с мест сообщают, что в следующей строке должно использоваться
	# token.split_contents()[0], требуется дополнительное подтверждение.
        msg = '%r tag requires a single argument' % token.contents[0]
        raise template.TemplateSyntaxError(msg)
    return CurrentTimeNode(format_string[1:-1])

Разберём этот код:

Аргумент parser является экземпляром шаблонного парсера. В данном примере мы его не используем.

token.contents является строкой содержимого тега. В нашем примере это current_time "%Y-%m-%d %I:%M %p".

Метод token.split_contents() разделяет аргументы по пробелу, не разбивая строку в кавычках. Избегайте использовать token.contents.split(), которая использует стандартную семантику разбиения строк. Она не учитывает кавычки, разбивая все по пробелам.

Эта функция отвечает за вызов исключения django.template.TemplateSyntaxError с соответствующим сообщением на любую ошибку синтаксиса.

Не следует жёстко определять имя тега в сообщениях об ошибках. token.split_contents()[0] будет всегда именем вашего тега, даже если тег не имеет аргументов.

Функция возвращает экземпляр класса CurrentTimeNode (который мы скоро определим), содержащий всё, что требуется узлу знать об этом теге. В данном случае, просто передается "%Y-%m-%d %I:%M %p". Ведущая и завершающая кавычки убираются с помощью format_string[1:-1].

Функции компиляции шаблонных тегов должны возвращать подкласс Node. Всё остальное — ошибка.

Определение шаблонного узла

На втором шаге следует определить подкласс Node, который должен иметь метод render(). Продолжая наш пример, нам надо определить CurrentTimeNode:

import datetime

class CurrentTimeNode(template.Node):

    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        now = datetime.datetime.now()
        return now.strftime(self.format_string)

Эти два метода (__init__ и render) соответствую двум шагам обработки (компиляция и рендеринг). Таким образом, метод инициализации применяется только для сохранения строки формата для дальнейшего использования, а метод render() делает основную работу.

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

Регистрация тега

В конце, следует зарегистрировать тег в экземпляре класса Library. Регистрация собственных тегов очень похожа на регистрацию своих фильтров. Создайте экземпляр template.Library и вызовите метод tag(). Например:

register.tag('current_time', do_current_time)

Метод tag() принимает два аргумента:

Имя шаблонного тега в виде строки. Если имя не указано, то используется имя функции компиляции.

Функция компиляции.

Как и при регистрации фильтра, также возможно использование декоратора (необходим Python версии 2.4+):

@register.tag(name="current_time")
def do_current_time(parser, token):
    # ...

@register.tag
def shout(parser, token):
    # ...

Если вы не укажете аргумент name, как было во втором примере, Django будет использовать имя функции в качестве имени тега.

Установка переменных в контексте

Пример из предыдущей секции просто возвращал значение. Часто бывает полезным устанавливать шаблонную переменную вместо возвращения значения. Для этого авторы шаблонов могут использовать переменные, устанавливаемые их шаблонными тегами.

Для того, чтобы установить переменную в контексте, используйте словарное назначение для контекстного объекта в методе render(). Ниже показана обновлённая версия класса CurrentTimeNode:

class CurrentTimeNode2(template.Node):

    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        now = datetime.datetime.now()
        context['current_time'] = now.strftime(self.format_string)
        return ''

Следует отметить, что метод render() возвращает пустую строку. Данный метод должен всегда возвращать строку. Раз шаблонный тег устанавливает переменную, то метод должен возвращать пустую строку.

Вот так теперь это можно использовать:

{% current_time2 "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>

Но с классом CurrentTimeNode2 есть одна проблема — имя переменной current_time жёстко определено. Это означает, что вам придётся проверять ваши шаблоны, что они не используют {{ current_time }}, так как {% current_time2 %} слепо перепишет значение этой переменной.

Более явное решение — позволить шаблонному тегу указывать имя устанавливаемой переменной, например так:

{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

Для этого потребуется провести рефакторинг функции компиляции и класса Node:

import re

class CurrentTimeNode3(template.Node):

    def __init__(self, format_string, var_name):
        self.format_string = format_string
        self.var_name = var_name

    def render(self, context):
        now = datetime.datetime.now()
        context[self.var_name] = now.strftime(self.format_string)
        return ''

def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        msg = '%r tag requires arguments' % token.contents[0]
        raise template.TemplateSyntaxError(msg)

    m = re.search(r'(.*?) as (\w+)', arg)
    if m:
        fmt, var_name = m.groups()
    else:
        msg = '%r tag had invalid arguments' % tag_name
        raise template.TemplateSyntaxError(msg)

    if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
        msg = "%r tag's argument should be in quotes" % tag_name
        raise template.TemplateSyntaxError(msg)

    return CurrentTimeNode3(fmt[1:-1], var_name)

Теперь do_current_time() передаёт строку формата и имя переменной в CurrentTimeNode3.

Обработка до другого блочного тега

Шаблонные теги могут работать как блоки, содержащие другие теги (типа {% if %}, {% for %} и так далее). Для создания такого шаблонного тега следует использовать parser.parse() в вашей функции компиляции.

Ниже показана реализация стандартного тега {% comment %}:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

Метод parse() получает кортеж имён блочных тегов, до которых он должен вести обработку. Метод возвращает экземпляр django.template.NodeList, который является списком всех объектов класса Node, которые парсер обнаружил на обработки любого тега из переданного кортежа.

Таким образом, в предыдущем примере nodelist является списком всех узлов между {% comment %} и {% endcomment %}.

После вызова метода parse() парсер ещё не «использовал» тег {% endcomment %}, поэтому необходимо явно вызвать метод delete_first_token(), чтобы не обрабатывать этот тег дважды.

Затем CommentNode.render() просто возвращает пустую строку. Всё, что находилось между тегами {% comment %} и {% endcomment %} игнорируется.

Обработка до другого блочного тега с сохранением содержимого

В предыдущем примере, метод do_comment() отбрасывал всё, что находилось между тегами {% comment %} и {% endcomment %}. Можно так не делать.

Например, здесь представлен шаблонный тег {% upper %}, который преобразует все буквы в заглавные между собой и тегом {% endupper %}:

{% upper %}
    This will appear in uppercase, {{ your_name }}.
{% endupper %}

Как и в предыдущем примере мы будет использовать parser.parse(). В этот раз мы передадим nodelist в Node:

@register.tag
def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):

    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

Единственная новая концепция в этом коде это использование self.nodelist.render(context) в UpperNode.render(). Он просто вызывает метод render() у каждого экземпляра класса Node в списке.

Примеры более сложной обработки можно подсмотреть в исходном коде {% if %}, {% for %}, {% ifequal %} и {% ifchanged %}. Их код можно найти в django/template/defaulttags.py.

Сокращения для простых тегов

Множество шаблонных тегов принимают только один аргумент, строку или ссылку на шаблонную переменную, и возвращают строку после выполнения обработки, руководствуясь переданным аргументом и некоторой внешней информацией. Например, тег current_time, который мы реализовали ранее, подходит под это описание. Если ему передать строку формата, то он возвратит время в виде строки.

Для того, чтобы упростить создание тегов такого типа Django предоставляет функции-помощники, simple_tag. Этот метод принадлежит django.template.Library, он принимает функцию одного аргумента, оборачивает её методом render() и регистрирует в шаблонной системе.

Наша предыдущая функция current_time могла быть переписана так:

def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.simple_tag(current_time)

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

@register.simple_tag
def current_time(token):
    ...

Про simple_tag следует сказать следующее:

Эта функция одного аргумента.

Проверку обязательного количества аргументов провела наша функция, так что не надо это здесь делать.

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

Теги включения

Другим общим типом тегов являются теги, которые отображают данные с помощью обработки другого шаблона. Например, интерфейс администратора Django использует свои шаблонные теги для отображения кнопок FIXME. Эти кнопки всегда выглядят одинаково, но их ссылки изменяются в соответствии с объектом, к которому они относятся. Данные кнопки являются отличным примером использования небольшого шаблона, который заполняется информацией из текущего объекта.

Этот тип тегов называется включениями. Реализацию такого тега лучше демонстрировать на примере. Давайте создадим тег, который создаёт список значений, несколько из которых можно выбрать — класс Poll. Мы будем использовать этот тег так:

{% show_results poll %}

Результат будет выглядеть так:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

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

def show_books_for_author(author):
    books = author.book_set.all()
    return {'books': books}

Затем мы создаём шаблон, который будет использоваться при обработке тега. Следуя нашему примеру, шаблон будет очень простым:

<ul>
{% for book in books %}
    <li> {{ book }} </li>
{% endfor %}
</ul>

Наконец, мы создаём и регистрируем тег с помощью метода inclusion_tag() объекта Library.

Следуя нашему примеру, раз предыдущий шаблон был в файле polls/result_snippet.html, регистрируем тег так:

register.inclusion_tag('books/books_for_author.html')(show_books_for_author)

И как обычно, Python версии 2.4+ позволяет использование декоратора:

@register.inclusion_tag('books/books_for_author.html')
def show_books_for_author(show_books_for_author):
    ...

Иногда может потребоваться, чтобы ваш тег получал доступ к значениям контекста родительского шаблона. Для решения этой задачи Django предоставляет опцию takes_context. Если вы укажете её при создании шаблонного тега, то тегу не понадобятся обязательные аргументы, функция нижнего уровня будет использовать контекст шаблона из которого данный тег был вызван.

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

@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }

Замечание

Первый параметр функции должен называться context.

Шаблон link.html может содержать такую строку:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

Тогда, когда потребуется использовать этот тег, загрузите его библиотеку и вызовите его без аргументов:

{% jump_link %}

Реализация своих шаблонных загрузчиков

Встроенные в Django шаблонные загрузчики (описанные в секции « Загрузка шаблонов ») покрывают практически все ваши нужды, но в случае необходимости будет достаточно просто реализовать свой собственный загрузчик. Например, вы можете загружать шаблоны из базы данных или напрямую из репозитория Subversion, или из ZIP архива.

Ожидается, что каждый шаблонный загрузчик, описанный в параметре TEMPLATE_LOADERS конфигурационного файла, будет функцией вида:

load_template_source(template_name, template_dirs=None)

Аргумент template_name является именем шаблона, который требуется загрузить (имя передаётся в loader.get_template() или loader.select_template()), а template_dirs является необязательным списком каталогов, в которых следует производить поиск вместо указанных в TEMPLATE_DIRS.

Если загрузчик может успешно загрузить шаблон, он должен возвратить кортеж (template_source, template_path). Здесь template_source является шаблонной строкой, которая будет скомпилирована шаблонной системой, а template_path — путь до загружаемого шаблона. Этот путь может отображаться пользователю в отладочных целях.

Если загрузчик не может загрузить шаблон, он должен вызывать исключение django.template.TemplateDoesNotExist.

Функция каждого загрузчика должна также иметь атрибут is_usable. Это значение булевого типа, которое информирует шаблонную систему о том, доступен ли этот загрузчик текущему проекту. Например, загрузчик, который может загружать шаблоны из питонских яиц ;) устанавливает значение False для этого атрибута, если модуль pkg_resources не установлен, так как этот модуль необходим для его работы с яйцами.

Пример должен всё прояснить. Ниже представлена функция шаблонного загрузчика, который может загружать шаблоны из ZIP файла. Он использует свой конфигурационный параметр TEMPLATE_ZIP_FILES в качестве пути поиска вместо TEMPLATE_DIRS и ожидает, что каждый путь должен быть ZIP файлом, который содержит шаблоны:

import zipfile
from django.conf import settings
from django.template import TemplateDoesNotExist

def load_template_source(template_name, template_dirs=None):
    """Template loader that loads templates from a ZIP file."""

    template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])

    # Try each ZIP file in TEMPLATE_ZIP_FILES.
    for fname in template_zipfiles:
        try:
            z = zipfile.ZipFile(fname)
            source = z.read(template_name)
        except (IOError, KeyError):
            continue
        z.close()
        # We found a template, so return the source.
        template_path = "%s:%s" % (fname, template_name)
        return (source, template_path)

    # If we reach here, the template couldn't be loaded
    raise TemplateDoesNotExist(template_name)

# This loader is always usable (since zipfile is included with Python)
load_template_source.is_usable = True

Для того, чтобы воспользоваться данным загрузчиком, его надо добавить в параметр TEMPLATE_LOADERS. Если мы разместили этот код в пакете mysite.zip_loader, тогда надо добавить mysite.zip_loader.load_template_source в параметр TEMPLATE_LOADERS.

Использование встроенного шаблонного справочника

Интерфейс администратора Django включает в себя полный справочник по всем шаблонным тегам и фильтрам доступным для текущего сайта. Справочник является инструментом, который разработчики кода предоставляют разработчикам шаблонов. Для того, чтобы воспользоваться справочником зайдите в интерфейс администратора и перейдите в раздел документации.

Справочник разделён на четыре секции: теги, фильтры, модели и представления. Секции теги и фильтры описывают все встроенные теги и все ваши доступные библиотеки тегов и фильтров.

Наиболее значимой секцией является секция представлений. Каждый URL на всём сайте имеет здесь отдельную запись. Если соответствующее представление имеет документирующую строку, то нажатие на этот URL отобразит вам следующее:

Имя функции представления.

Краткое описание того, что делает это представление.

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

Имя шаблона или шаблонов, которые используются в этом представлении.

Подробный пример документирования представления можно получить в процессе чтения исходного кода базового представления object_list, который можно найти в файле django/views/generic/list_detail.py.

Так как сайты под управлением Django обычно используют объекты хранящиеся в базе данных, секция моделей описывает каждый тип объекта в системе, включая все поля доступные этому объекту.

В целом, страницы документации могут рассказать всё о каждом теге, фильтре, переменной и объекте, которые доступны вам в шаблоне.

Работа в автономном режиме

Замечание

Эта секция пригодится только тем разработчикам, которые будут пытаться использовать шаблонную систему в качестве компонента другого приложения. Если вы используете эту систему как часть Django, вам эта информация не понадобится.

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

Например, мне понадобилось создавать и отправлять своему менеджеру отчёт о проделанной работе. Менеджер мог понимать почтовые сообщения только в формате HTML, почтовое сообщение должно было быть отформатировано определённым образом. Я не мог заставить себя тратить полчаса на заполнение его «портянки». Поэтому был написан небольшой скрипт, который для генерации HTML представления сообщения использовал шаблонную систему Django. Описанное далее, учитывает мой опыт.

Для решения этой задачи потребуется использовать параметры конфигурации, которые описаны в приложении « Параметры конфигурации ». Это означает, что переменная среды PYTHONPATH должна указывать на каталог, где развёрнут Django, а также то, что вам необходимо определить соответствующие параметры конфигурации. Можно, конечно, создать скрипт в котором будет определяться переменная среды DJANGO_SETTINGS_MODULE (и, соответственно, потребуется создать файл с настройками, как это происходит в Django приложении), а затем вызываться скрипт. Но лучше сделать следующее (на примере шаблонной системы):

import django
django.conf.settings.configure(TEMPLATE_DIRS = ('/home/rad/devel/cpr_report',))

Через запятую, можно определить столько параметров конфигурации, сколько надо. Среди параметров могут быть такие как TEMPLATE_DIRS (если вам потребуется использовать шаблонные загрузчики), DEFAULT_CHARSET (хотя использование UTF-8 будет достаточным) и TEMPLATE_DEBUG. Все доступные параметры описаны в приложении « Параметры конфигурации », особый интерес представляют параметры, начинающиеся с TEMPLATE_.

Небольшой пример:

import django
from django.template import Context, loader
django.conf.settings.configure(TEMPLATE_DIRS = ('/home/rad/devel/cpr_report',))

template = loader.get_template('cpr_report_template.html')
context = Context({'problems': info_problems or 'нет',
                   'plans': info_plans or 'нет',
                   'done': info_done or 'нет'})
html = template.render(context)

Глава 11. Генерация данных, отличных от HTML

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

Обычно, говоря о разработке веб сайтов, мы имеем в виду генерацию HTML. Конечно, кроме HTML в сфере веба существует ещё много чего. Мы используем веб для распространения информации во всех форматах: RSS, PDF, изображения и так далее.

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

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

Рассылки RSS/Atom.

Карты сайтов (в XML формате, разработанном Google, который очень помогает поисковым машинам).

Мы исследуем каждый из этих инструментов немного позже, сначала рассмотрим основные принципы.

Основы: представление и MIME-типы

Помните из главы « Представления и привязки URL » это:

Функция представления, или просто представление, является функцией языка Python, которая принимает запрос и возвращает отклик. Этим откликом может быть HTML контент веб страницы, перенаправление на другую страницу, ошибка 404, XML документ, изображение или что-нибудь ещё.

Более формально, функция представления Django должна:

Принимать экземпляр класса HttpRequest в качестве первого аргумента.

Возвращать экземпляр класса HttpResponse.

Возможность возвращать из представление не-HTML данные лежит в классе HttpResponse, а точнее в аргументе конструктора mimetype. Определяя MIME-тип, мы можем указывать браузеру формат возвращаемых данных.

Например, рассмотрим представление, которое возвращает изображение в формате PNG. Для простоты мы просто читаем файл с диска:

from django.http import HttpResponse

def my_image(request):
    image_data = open("/path/to/my/image.png", "rb").read()
    return HttpResponse(image_data, mimetype="image/png")

Вот так! Если вы замените путь до файла в вызове функции open() на путь до реального изображения, вы можете использовать это очень простое представление для обработки изображений, а броузер будет отображает их корректно.

Ещё следует помнить о том, что объекты HttpResponse работают через стандартный API для работы с файлами. Это означает, что вы можете использовать HttpResponse в любом месте, где требуется обработать файл.

Для того, чтобы увидеть как всё это работает, давайте рассмотрим как Django генерирует CSV.

Генерация CSV

CSV является простым форматом данных, который обычно используется программным обеспечением для работы с электронными таблицами. Формат описывает набор записей таблицы, в котором каждая ячейка ряда отделена друг от друга запятой (CSV — это аббревиатура для comma-separated values). Например, ниже представлены некоторые данные на "недисциплинированных" пассажиров авиакомпании в формате CSV:

Year,Unruly Airline Passengers
1995,146
1996,184
1997,235
1998,200
1999,226
2000,251
2001,299
2002,273
2003,281
2004,304
2005,203

Замечание

Вышеприведённый пример содержит реальные числа. Они были любезно предоставлены Федеральным Управлением Авиации США. См. http://www.faa.gov/data_statistics/passengers_cargo/unruly_passengers/.

Несмотря на то, что CSV выглядит просто, он не является форматом как таковым. Разные программные продукты создают и используют различные варианты CSV, делая непростым его использование. К счастью, Python поставляется со стандартной библиотекой для работы с CSV.

По причине того, что модуль csv работает как файлоподобные объекты, его можно использовать вместе с HttpResponse:

import csv
from django.http import HttpResponse

# Количество недисциплинированных пассажиров за каждый год диапазона 1995 - 2005.
# В реальном приложении эта информация будет получена из базы данных.
UNRULY_PASSENGERS = [146,184,235,200,226,251,299,273,281,304,203]

def unruly_passengers_csv(request):
    # Создаём объект HttpResponse с соответствующим CSV заголовком.
    response = HttpResponse(mimetype='text/csv')
    response['Content-Disposition'] = 'attachment; filename=unruly.csv'

    # Создаём объект для записи CSV, используя HttpResponse как "файл"
    writer = csv.writer(response)
    writer.writerow(['Year', 'Unruly Airline Passengers'])
    for (year, num) in zip(range(1995, 2006), UNRULY_PASSENGERS):
        writer.writerow([year, num])

    return response

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

Отклик имеет отличный от text/html MIME-тип — text/csv. Это указывает браузерам, что передаваемый документ является CSV файлом.

Отклик получает дополнительный заголовок «Content-Disposition», который содержит имя CSV файла. Этот заголовок, а точнее часть вложения, указывает браузеру, что необходимо запросить у пользователя место куда надо сохранить файл, а не отображать его на экране. Имя файла может быть произвольным, назовите его как пожелаете с помощью диалогового окна «Сохранить как ...».

Подключение к API генерации CSV несложно: просто передайте response в качестве первого аргумента для csv.writer. Функция csv.writer ожидает файлоподобный объект и объекты HttpResponse соответствуют этому требованию.

Для каждой записи в вашем CSV файле требуется вызывать функцию writer.writerow, передавая ей итерационный объект, подобный списку или кортежу.

Модуль csv заботится об экранировании данных. Просто передавайте информацию в функцию writerow() и она сделает всё правильно.

Это основной шаблон поведения в случае когда вам требуется вернуть информацию отличную от HTML: создайте объект HttpResponse (с определённым MIME-типом), передайте его чему-нибудь, ожидающему файл и затем возвратите отклик.

Давайте рассмотрим ещё несколько примеров.

Генерация PDF

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

Вы можете легко создавать PDF документы с помощью Python и Django благодаря отличной открытой библиотеке ReportLab. Преимущество динамического создания PDF документов заключается в том, что вы можете изменять его различными способами, например, разная информация для разных пользователей.

Например, мы используем Django и ReportLab на сайте KUSports.com для генерации NCAA программ соревнований.

Установка ReportLab

Однако перед тем как сгенерировать PDF документ вам потребуется установить ReportLab. Это несложно: просто скачайте и установите библиотеку с http://www.reportlab.org/downloads.html.

Руководство пользователя (доступное только в виде PDF) на http://www.reportlab.org/rsrc/userguide.pdf содержит дополнительные инструкции по установке.

Замечание

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

Например, если вы используете (обалденный) дистрибутив Ubuntu, установка библиотеки крайне проста:

apt-get install python-reportlab

Проверьте установку с помощью импортирования библиотеки в интерактивном интерпретаторе Python:

>>> import reportlab

Если эта команда не вызывает никаких ошибок, значит установка библиотеки прошла успешно.

Создание представления

Аналогично случаю с CSV, динамическое создание PDF объектов с помощью Django является несложным, по причине того, что API библиотеки ReportLab работает как файлоподобный объект.

Ниже представлен пример «Hello World»:

from reportlab.pdfgen import canvas
from django.http import HttpResponse

def hello_pdf(request):
    # Создаём объект HttpResponse с соответствующим PDF заголовком.
    response = HttpResponse(mimetype='application/pdf')
    response['Content-Disposition'] = 'attachment; filename=hello.pdf'

    # Создаём объект PDF, используя объект HttpResponse как файл.
    p = canvas.Canvas(response)

    # Выводим в PDF необходимую информацию. Вот так создаются PDF.
    # Обратитесь к документации на ReportLab для подробностей.
    p.drawString(100, 100, "Hello world.")

    # Явно закрываем объект PDF.
    p.showPage()
    p.save()
    return response

А теперь несколько замечаний:

В данном случае мы используем application/pdf в качестве MIME-типа. Это указывает браузеру, что документ является PDF файлом, а не HTML. Если вы забудете указать эту информацию, браузер проинтерпретирует документ как HTML файл и вывалит кучу мусора на экран.

Подключение к ReportLab API несложно: просто передайте response в качестве первого аргумента для canvas.Canvas. Объект Canvas ожидает файлоподобный объект и объекты HttpResponse соответствуют этому требованию.

Все вызовы дополнительных методов для создания PDF документа производятся от PDF объекта (см. p в коде), а не от response.

Наконец надо вызвать функции showPage() и save() для PDF файла, иначе вы получите испорченный PDF документ.

Сложные PDF документы

Если вы создаёте сложный PDF документ (или любой большой объём данных), рассмотрите возможность использования библиотеки cStringIO в качестве временного хранилища для вашего PDF файла. Библиотека cStringIO предоставляет файлоподобный интерфейс, который написан на языке C для достижения максимальной эффективности.

Перепишем предыдущий «Hello World» пример, используя библиотеку cStringIO:

from cStringIO import StringIO
from reportlab.pdfgen import canvas
from django.http import HttpResponse

def hello_pdf(request):
    # Создаём объект HttpResponse с соответствующим PDF заголовком.
    response = HttpResponse(mimetype='application/pdf')
    response['Content-Disposition'] = 'attachment; filename=hello.pdf'

    temp = StringIO()

    # Создаём объект PDF, используя объект StringIO как файл.
    p = canvas.Canvas(temp)

    # Выводим в PDF необходимую информацию. Здесь генерируется содержимое PDF.
    # Обратитесь к документации на ReportLab для подробностей.
    p.drawString(100, 100, "Hello world.")

    # Явно закрываем объект PDF.
    p.showPage()
    p.save()

    # Получаем данные из буфера StringIO и пишем отклик.
    response.write(temp.getvalue())
    return response

Другие возможности

Существует целый набор других типов данных, которые вы можете генерировать с помощью Python. Ниже представлены некоторые идеи и ссылки на библиотеки, которые вы можете использовать для их реализации:

ZIP файлы: Стандартная библиотека Python поставляется с модулем zipfile, который может читать и писать сжатые ZIP файлы. Вы можете использовать этот модуль для предоставления по требованию архивов с набором файлов или, возможно, сжимать большие документы, когда это необходимо. Аналогично вы можете предоставлять TAR файлы с помощью модуля tarfile стандартной библиотеки.

Динамические изображения: графическая библиотека Python, PIL — это фантастический набор инструментов для создания изображений (в таких форматах как PNG, JPEG, GIF и многих других). Вы можете использовать её для автоматического создания миниатюр изображений, объединения нескольких изображений в единое или даже для онлайновой обработки изображений.

Графики и диаграммы: Существует ряд невероятно мощных библиотек Python для создания графиков и диаграмм, которые вы можете использовать для генерации по запросу различной графической информации. Мы не имеем возможности привести здесь их список, укажем лишь несколько:

Библиотека matplotlib (http://matplotlib.sourceforge.net/) может быть использована для создания высококачественных графиков, аналогичных графиком из MatLab или Methematica.

Библиотека pygraphviz (https://networkx.lanl.gov/wiki/pygraphviz), которая является интерфейсом к GraphViz, может быть использована для создания структурных диаграмм графов и сетей

В общем, любая библиотека языка Python, имеющая возможность записи в файл может быть подключена к Django. Возможности при этом бесконечны.

Мы рассмотрели основы создание отличной от HTML информации, перейдём на другой уровень абстракции. Django поставляется с некоторыми прикольными инструментами для генерации общих типов не-HTML информации.

Средства трансляции

Django поставляется со средствами высокого уровня для трансляции данных, что позволяет легко создавать потоки RSS или Atom.

Что такое RSS? Что такое Atom?

RSS и Atom оба являются форматами, базирующимися на XML, которые вы можете использовать для рассылки автоматически обновляемого «содержания» для вашего сайта. Более подробно об RSS можно узнать на http://www.whatisrss.com/, об Atom — на http://www.atomenabled.org/.

Здесь и далее мы будем использовать термин «трансляция» для английской фразы «syndication feed».

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

Средой высокого уровня для генерации трансляций является представление, которое по договорённости подключено к /feeds/. Django использует хвостовую часть URL (то, что идёт после /feeds/) для определения того, какую именно трансляцию следует вернуть.

Для создания трансляции, вам потребуется написать класс Feed и указать на него в вашем файле привязок URL (обратитесь к главам « Представления и привязки URL » и « Усовершенствованные представления и схемы URL » для подробностей).

Инициализация

Чтобы активировать трансляции на вашем сайте, добавьте следующее в файл привязок:

(r'^feeds/(?P<url>.*)/$',
 'django.contrib.syndication.views.feed',
 {'feed_dict': feeds}
),

Этот код указывает Django, что следует использовать RSS среду для обработки всех URL, которые начинаются с feeds/. (Вы можете изменить этот префикс feeds/ на любой необходимый.)

Следует отметить, что параметр feed_dict должен быть словарём, который совпадает с сокращением для трансляции (метка для короткого URL). Вы можете определить этот параметр в файле привязок. Ниже дан полный пример:

from django.conf.urls.defaults import *
from myproject.feeds import LatestEntries, LatestEntriesByCategory

feeds = {
    'latest': LatestEntries,
    'categories': LatestEntriesByCategory,
}

urlpatterns = patterns('',
    # ...
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
        {'feed_dict': feeds}),
    # ...
)

Вышеприведённый пример регистрирует две трансляции:

Трансляция, представленная LatestEntries, будет жить на feeds/latest/.

Трансляция, представленная LatestEntriesByCategory, будет жить на feeds/categories/.

Теперь надо определить сами классы Feed.

Класс Feed является обычным классом Python и представляет собой класс трансляции. Трансляция может быть простой (т.е. отображающей новости сайта или последние записи блога) или более сложной (т.е. отображающей все записи блога в определённой категории, где категория является переменной).

Вы должны подключать в код представления класс django.contrib.syndication.feeds.Feed. Классы Feed могут располагаться в любой точке вашего кода.

Простая трансляция

Этот простой пример, взятый с http://chicagocrime.org/, описывает трансляцию, которая возвращает пять свежих новостей:

from django.contrib.syndication.feeds import Feed
from chicagocrime.models import NewsItem

class LatestEntries(Feed):
    title = "Chicagocrime.org site news"
    link = "/sitenews/"
    description = "Updates on changes and additions to chicagocrime.org."

    def items(self):
        return NewsItem.objects.order_by('-pub_date')[:5]

Отметим важные аспекты этого кода:

Класс подключает django.contrib.syndication.feeds.Feed.

Атрибуты title, link и description соответствуют стандартным элементам RSS: <title>, <link> и <description>.

Метод items() возвращает список объектов, которые должны быть включены в трансляцию в виде элементов <item>. Несмотря на то, что этот пример возвращает объекты NewsItem с помощью API для работы с базой данных, метод items() не обязан возвращать экземпляры модели.

Вы «бесплатно» получаете некоторую функциональность, используя модели Django, но метод items() может вернуть любой тип объекта.

Надо сделать ещё кое-что. В RSS-потоке каждый элемент <item> имеет элементы <title>, <link> и <description>. Необходимо указать среде какие именно данные следует помещать в эти элементы.

Чтобы указать содержимое для <title> и <description>, создайте шаблоны (см. главу « Шаблоны ») с именами feeds/latest_title.html и feeds/latest_description.html, где latest является сокращением, которое указано в файле привязки для конкретной трансляции. Следует отметить, что наличие расширения .html обязательно.

Система RSS обрабатывает этот шаблон для каждого элемента, передавая ему две контекстные переменные:

obj: текущий объект (один из объектов полученных от метода items()).

site: Объект класса django.models.core.sites.Site, представляющий текущий сайт. Полезен для {{ site.domain }} или {{ site.name }}.

Если вы не создадите шаблон хотя бы для одного элемента, среда будет использовать шаблон {{ obj }} по умолчанию, т.е. обычное текстовое представление объекта.

Также вы можете поменять имена этих двух шаблонов, определив атрибуты title_template и description_template для класса Feed.

При определении содержимого для <link> у вас есть две опции. Для каждого элемента, возвращённого методом items(), Django сначала пытается выполнить его метод get_absolute_url(). Если такого метода нет у объекта, Django пытается вызвать метод item_link() для класса Feed, передавая ему один параметр item, который является этим объектом.

Оба метода, get_absolute_url() и item_link(), должны возвращать URL элемента в виде обычной строки Python.

Для показанного ранее примера LatestEntries мы можем создать очень простые шаблоны:

# latest_title.html
{{ obj.title }}

#latest_description.html
{{ obj.description }}

Это всегда слишком просто ...

Более сложная трансляция

Сайт http://chicagocrime.org предоставляет RSS-потоки по последним преступлениям для каждого полицейского участка в Чикаго. Можно устать, создавая классы Feed для каждого участка. Это нарушит принцип DRY и приведёт к размещению данных в логике программы.

Вместо этого, среда трансляций позволяет вам создать базовые трансляции, которые возвращают элементы в зависимости от URL.

На нашем сайте трансляции для полицейских участков доступны через URL, подобные этим:

http://www.chicagocrime.org/rss/beats/0613/: возвращает данные о последних преступлениях на участке 0613.

http://www.chicagocrime.org/rss/beats/1424/: возвращает данные о последних преступлениях на участке 1424.

Здесь сокращением является beats. Среда трансляций обнаруживает хвостовую часть URL после beats — 0613 и 1424 — и предоставляет вам возможность указать ей, что означает эта дополнительная часть URL и как она влияет на транслируемые данные.

Пример должен всё прояснить. Ниже представлен код для таких трансляций:

from django.core.exceptions import ObjectDoesNotExist

class BeatFeed(Feed):
    def get_object(self, bits):
        # In case of "/rss/beats/0613/foo/bar/baz/", or other such
        # clutter, check that bits has only one member.
        if len(bits) != 1:
            raise ObjectDoesNotExist
        return Beat.objects.get(beat__exact=bits[0])

    def title(self, obj):
        return "Chicagocrime.org: Crimes for beat %s" % obj.beat

    def link(self, obj):
        return obj.get_absolute_url()

    def description(self, obj):
        return "Crimes recently reported in police beat %s" % obj.beat

    def items(self, obj):
        crimes =  Crime.objects.filter(beat__id__exact=obj.id)
        return crimes.order_by('-crime_date')[:30]

Здесь представлен базовый алгоритм среды RSS-потоков. Берём данный класс и обрабатываем URL /rss/beats/0613/:

Среда трансляций получает URL /rss/beats/0613/ и отмечает, что существует дополнительная часть данных в URL. Среда отделяет эту часть по символу / и вызывает у класса Feed метод get_object(), передавая ему эту часть URL.

В данном случае, такой частью будет ['0613']. Для запроса /rss/beats/0613/foo/bar/' — ['0613', 'foo', 'bar'].

Метод get_object() отвечает за получение соответствующего участка из полученного набора.

В данном случае, среда трансляций использует API для работы с базой данных для получения информации по указанному полицейскому участку. Следует отметить, что метод get_object() должен вызывать исключение django.core.exceptions.ObjectDoesNotExist в случае получения неверного параметра. Здесь не использована связка try / except для вызова Beat.objects.get(), т.к. в этом нет необходимости. В случае неудачи данный метод вызывает исключение Beat.DoesNotExist, которое является подклассом ObjectDoesNotExist. Вызов исключения ObjectDoesNotExist в get_object() указывает Django, что необходимы выдать страницу с ошибкой 404 на обрабатываемый запрос.

Для генерации элементов <title>, <link> и <description> Django использует методы title(), link() и description(). В предыдущем примере они были простыми строковыми атрибутами класса, но данный пример иллюстрирует, что они могут быть строками или методами. Для каждого элемента title, link и description Django следует нижеприведённому алгоритму:

Производится вызов метода с передачей ему аргумента obj, где obj является объектом, возвращённым методом get_object().

В случае неудачи, производится вызов метода без аргументов.

В случае неудачи, используется атрибут класса.

Следует отметить, что items() в данном примере также принимает аргумент obj. Алгоритм для items полностью совпадает с предыдущим пунктом — сначала производится вызов items(obj), затем items() и наконец используется атрибут класса items (который должен быть списком).

Полная документация по всем методам и атрибутам классов Feed доступна на официальном сайте Django (http://www.djangoproject.com/documentation/0.96/syndication_feeds/).

Указание типа трансляции

По умолчанию среда трансляций генерирует RSS 2.0. Для изменения этого поведения надо добавить атрибут feed_type в ваш класс Feed:

from django.utils.feedgenerator import Atom1Feed

class MyFeed(Feed):
    feed_type = Atom1Feed

Следует отметить то, что вы устанавливаете этот атрибут для класса, а не для экземпляра. Доступные типы трансляций показаны в таблице « Типы трансляций »:

Таблица 11.1. Типы трансляций

Класс трансляции | Формат
django.utils.feedgenerator.Rss201rev2Feed | RSS 2.01 (по умолчанию)
django.utils.feedgenerator.RssUserland091Feed | RSS 0.91
django.utils.feedgenerator.Atom1Feed | Atom 1.0

Вложения

Для того, чтобы указать вложения (т.е. медиа ресурсы ассоциированные с элементами трансляции, такими как MP3 подкасты), используйте обработчики item_enclosure_url, item_enclosure_length и item_enclosure_mime_type, например:

from myproject.models import Song

class MyFeedWithEnclosures(Feed):
    title = "Example feed with enclosures"
    link = "/feeds/example-with-enclosures/"

    def items(self):
        return Song.objects.all()[:30]

    def item_enclosure_url(self, item):
        return item.song_url

    def item_enclosure_length(self, item):
        return item.song_length

    item_enclosure_mime_type = "audio/mpeg"

Это предполагает, что вы уже создали объект Song с полями song_url и song_length (размер в байтах).

Язык

Трансляции создаваемые средой автоматически получают соответствующий тег <language> (RSS 2.0) или атрибут xml:lang (Atom). Значение этого тега зависит от параметра конфигурации LANGUAGE_CODE.

URL

Атрибут/метод link может возвращать как абсолютный URL (т.е. /blog/), так и полный URL (т.е. http://www.example.com/blog/). Если link не возвратил домен, то среда трансляций добавит домен текущего сайта в соответствии с параметром конфигурации SITE_ID.

Трансляции Atom требуют наличия <link rel="self">, который определяет место расположения трансляции. Среда трансляций создаёт его автоматически, используя домен текущего сайта в соответствии с параметром конфигурации SITE_ID.

Одновременная трансляция Atom и RSS

Некоторые разработчики предпочитают предоставлять трансляции в обоих видах: RSS и Atom. С помощью Django это несложно сделать: просто создайте подкласс для вашего класса feed и установите feed_type в что-нибудь отличное. Затем добавьте его вызов в ваш файл привязок. Вот полный пример:

from django.contrib.syndication.feeds import Feed
from chicagocrime.models import NewsItem
from django.utils.feedgenerator import Atom1Feed

class RssSiteNewsFeed(Feed):
    title = "Chicagocrime.org site news"
    link = "/sitenews/"
    description = "Updates on changes and additions to chicagocrime.org."

    def items(self):
        return NewsItem.objects.order_by('-pub_date')[:5]

class AtomSiteNewsFeed(RssSiteNewsFeed):
    feed_type = Atom1Feed

А здесь соответствующий файл привязок:

from django.conf.urls.defaults import *
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed

feeds = {
    'rss': RssSiteNewsFeed,
    'atom': AtomSiteNewsFeed,
}

urlpatterns = patterns('',
    # ...
    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
        {'feed_dict': feeds}),
    # ...
)

Среда генерации карты сайта

Карта сайта представляет собой XML файл, который располагается на вашем веб сервере и сообщает поисковым движкам как часто изменяются ваши страницы и как «важны» определённые страницы по отношению к другим страницам вашего сайта. Эта информация помогает поисковым движкам индексировать ваш сайт.

Например, ниже представлена карта сайта Django (http://www.djangoproject.com/sitemap.xml):

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://www.djangoproject.com/documentation/</loc>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
  <url>
    <loc>http://www.djangoproject.com/documentation/0_90/</loc>
    <changefreq>never</changefreq>
    <priority>0.1</priority>
  </url>
  ...
</urlset>

Для получения более подробной информации по картам сайтов обратитесь к http://www.sitemaps.org/.

Среда Django для генерации карты сайта автоматизирует создание такого XML файла, позволяя вам выражать эту информацию в коде на языке Python. Для того, чтобы создать карту сайта вам лишь надо написать класс Sitemap и указать на него в файле привязок URL.

Установка

Для того, чтобы установить карту сайта в ваше приложение, следуйте нижеприведённой инструкции:

Добавьте django.contrib.sitemaps в параметр конфигурации INSTALLED_APPS.

Удостоверьтесь, что django.template.loaders.app_directories.load_template_source указан в параметре конфигурации TEMPLATE_LOADERS. По умолчанию он должен там быть, только если вы его сами оттуда не убрали.

Удостоверьтесь, что вы установили sites framework FIXME (см. главу « Средства от других разработчиков »).

Замечание

Приложение для генерации карты сайта не создаёт ни одной таблицы в базе данных. Единственная причина, по которое её требуется указывать в INSTALLED_APPS — так загрузчик шаблонов load_template_source может найти стандартные шаблоны.

Активация

Для активации генератора карты сайта добавьте следующую строку в файл привязок URL:

 (r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps})

Эта строка указывает Django, что необходимо построить карту сайта, когда клиент запросит /sitemap.xml.

Имя для файла содержащего карту сайта не имеет значения, но вот расположение этого файла имеет. Поисковые движки всегда индексируют ссылки в вашей карте относительно текущего уровня URL и ниже. Например, если sitemap.xml находится в корневом каталоге сайта, он может ссылаться на любой URL вашего сайта. Однако, если карта сайта находится по URL /content/sitemap.xml, он может ссылаться на URL, начинающийся с /content/.

Представление для карты сайта принимает дополнительный обязательный аргумент: {'sitemaps': sitemaps}. Значение sitemaps должно быть словарём, который совпадает с сокращённой меткой (т.е. blog или news) для своего класса Sitemap (т.е. BlogSitemap или NewsSitemap). Значение может также совпадать с экземпляром класса Sitemap (т.е., с BlogSitemap(some_var)).

Классы

Класс Sitemap — это обычный Python класс, который представляет «раздел» элементов вашей карты сайта. Например, один класс Sitemap может представлять все записи вашего блога, в то время как другой класс представляет все события вашего календаря.

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

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

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

from django.contrib.sitemaps import Sitemap
from mysite.blog.models import Entry

class BlogSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return Entry.objects.filter(is_draft=False)

    def lastmod(self, obj):
        return obj.pub_date

Объявление Sitemap должна выглядеть подобно декларации класса Feed, таков дизайн Django.

Подобно классам Feed, члены класса Sitemap могут быть как методами, так и атрибутами. Подробности были описаны в разделе « Более сложная трансляция ».

Класс Sitemap может определять следующие методы или атрибуты:

Обязательный метод items предоставляет список объектов. Среда не заботится о типах объектов. Имеет значение только то, что эти объекты передаются в методы location(), lastmod(), changefreq() и priority().

Необязательный метод location предоставляет абсолютный URL для текущего объекта. В данном случае «абсолютный URL» означает URL, который не включает протокол и домен. Вот несколько примеров:

Хороший: /foo/var/.

Плохой: example.com/foo/bar/.

Плохой: http://example.com/foo/bar/.

Если location отсутствует, среда будет использовать метод get_absolute_url() для каждого объекта, возвращённого методом items().

Необязательный метод lastmod возвращает дату последнего изменения объекта в виде стандартного объекта datetime.

Необязательный метод changefreq возвращает информацию о том, как часто изменяется объект. Возможные значения, определённые спецификацией на карты сайтов, представлены ниже:

always

hourly

daily

weekly

monthly

yearly

never

Необязательный метод priority возвращает приоритет между 0.0 и 1.0. Стандартный приоритет для страницы 0.5. О том как работают приоритеты можно узнать в документации на http://sitemaps.org/.

Полезное

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

FlatPageSitemap

Класс django.contrib.sitemaps.FlatPageSitemap просматривает все плоские, страницы определённые для текущего сайта и создаёт запись в карте сайта. Эти записи включают в себя только атрибут location.

Подробности о плоских страницах приведены в главе « Средства от других разработчиков ».

GenericSitemap

Класс GenericSitemap работает с любым базовым представлением (см. главу « Базовые представления »), которое у вас есть.

Для использования этого класса, создайте его экземпляр, передав ему такой же info_dict, какой вы передаёте базовым представлениям. Единственное требование заключается в том, что словарь должен иметь запись queryset. Он также может иметь запись date_field, которая определяет поле даты для объектов получаемых из queryset. Дата будет использована для атрибута lastmod при создании карты сайта. Вы также можете передать именованные аргументы priority и changefreq в конструктор GenericSitemap, для внесения этой информации во все URL.

Ниже представлен пример файла привязок, в котором использованы FlatPageSitemap и GenericSitemap (с гипотетическим объектом Entry, о котором мы говорили ранее):

from django.conf.urls.defaults import *
from django.contrib.sitemaps import FlatPageSitemap, GenericSitemap
from mysite.blog.models import Entry

info_dict = {
    'queryset': Entry.objects.all(),
    'date_field': 'pub_date',
}

sitemaps = {
    'flatpages': FlatPageSitemap,
    'blog': GenericSitemap(info_dict, priority=0.6),
}

urlpatterns = patterns('',
    # some generic view using info_dict
    # ...

    # the sitemap
    (r'^sitemap.xml$',
     'django.contrib.sitemaps.views.sitemap',
     {'sitemaps': sitemaps})
)

Создание индекса для карты сайта

Среда генерации карты сайта также имеет возможность создавать индекс для карты сайта, который ссылается на отдельные файлы с картами сайта, по одному на каждую секцию определённою в словаре sitemaps. Отличия:

Вы используете два представления в вашем файле привязки: django.contrib.sitemaps.views.index и django.contrib.sitemaps.views.sitemap.

Представление django.contrib.sitemaps.views.sitemap должно принимать именованный аргумент section.

Вот так изменится предыдущий пример:

(r'^sitemap.xml$',
 'django.contrib.sitemaps.views.index',
 {'sitemaps': sitemaps}),

(r'^sitemap-(?P<section>.+).xml$',
 'django.contrib.sitemaps.views.sitemap',
 {'sitemaps': sitemaps})

Это автоматически создаст файл sitemap.xml, который будет ссылаться на sitemap-flatpages.xml и sitemap-blog.xml. Классы Sitemap и словарь sitemaps не изменятся.

Уведомление Google

Вы можете организовать уведомление Google об изменениях в карте сайта, чтобы они знали, что надо произвести повторную индексацию вашего сайта. Среда предоставляет функцию для этого — django.contrib.sitemaps.ping_google().

Замечание

Во время работы над этой книгой только Google откликался на уведомления. Тем не менее, есть вероятность, что Yahoo и/или MSD скоро добавят поддержку уведомлений.

В этом случае, мы наверное изменим имя функции ping_google() на что-нибудь подобное ping_search_engines(). Рекомендуем периодически проверять документацию на http://www.djangoproject.com/documentation/0.96/sitemaps/.

Функция ping_google() принимает необязательный аргумент sitemap_url, который должен содержать абсолютный URL для карты сайта (т.е. /sitemap.xml). Если этот аргумент опущен, функция попытается найти карту сайта через файл привязок.

Функция ping_google() вызывает исключение django.contrib.sitemaps.SitemapNotFound, если она не может определить URL для карты сайта.

Ниже показан полезный способ вызова ping_google() из метода save() модели:

from django.contrib.sitemaps import ping_google

class Entry(models.Model):
    # ...
    def save(self):
        super(Entry, self).save()
        try:
            ping_google()
        except Exception:
            # Простое исключение, так как мы можем получить целый
	    # набор исключений, относящихся к HTTP.
            pass

Однако, более эффективным решением будет вызов ping_google() из скрипта cron или из другого периодического процесса. Функция выполняет HTTP запрос к серверам Google и вам может не понравится расход пропускной способности сети при каждом вызове save().

Глава 12. Сессии, пользователи и регистрация

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

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

До сих пор мы рассматривали общий случай создания HTML, но в этой главе мы пойдём другим путём и рассмотрим возможности Django для генерации других видов контента. До этого момента мы рассматривали посетителей нашего сайта как безликую, анонимную массу просматривающую наши аккуратно свёрстанные страницы.

Конечно, на самом деле всё не так. За браузерами, которые работают с нашим сайтом, сидят реальные люди (по крайней мере, иногда). Это нельзя игнорировать. Интернет предназначен для объединения людей, а не машин. Если мы стараемся разрабатывать совершенно неотразимые сайты, в конечном счёте нам придётся учитывать мнение людей.

К сожалению, не всё так просто. HTTP разработан так, чтобы не учитывать состояния. Таким образом, каждый запрос происходит как-бы в вакууме. Не существует взаимосвязи между одним запросом и другим. Соответственно, мы никак не сможем привязаться к какому-нибудь аспекту (IP адрес, тип браузера и так далее) для гарантированного определения запросов от одного конкретного пользователя.

В этой главе вы научитесь как решать подобную задачу. Мы начнём с нижнего уровня (cookies) и продолжим изучение инструментов высокого уровня для поддержки сессий, аутентификации и авторизации пользователей.

Cookie

Разработчики браузеров давно поняли, что отсутствие поддержки состояний в протоколе HTTP вызвало большие трудности у веб разработчиков. И тогда были созданы cookie. Cookie — это небольшие кусочки информации, которые браузер сохраняет по просьбе веб сервера. При каждом обращении браузера к странице определённого сайта, он отсылает полученную ранее cookie.

Давайте рассмотрим как это работает. Когда вы открываете свой браузер и переходите на http://google.com/, ваш браузер отправляет HTTP запрос на Google, который начинается так:

GET / HTTP/1.1
Host: google.com
...

Ответ Google выглядит обычно так:

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671;
            expires=Sun, 17-Jan-2038 19:14:07 GMT;
            path=/; domain=.google.com
Server: GWS/2.1
...

Обратите внимание на заголовок Set-Cookie. Ваш браузер сохранит значение cookie (PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671) и будет отсылать его обратно, при каждом обращении к серверу Google. Таким образом, при следующем посещении Google, ваш браузер будет посылать такой запрос:

GET / HTTP/1.1
Host: google.com
Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671
...

Google затем может использовать значение Cookie для определение того, что вы именно тот, кто обращался к сайту ранее. Значение, например, может быть ключом записи в базе данных, которая хранит информацию о пользователе. Google может (и будет) использовать её для отображения вашего имени на странице.

Получение и установка

Для сохранения состояний в Django большую часть времени вы пожелаете использовать высокоуровневые сессии и/или среду пользователя, которая будет рассмотрена в этой главе чуть позже. Однако, мы прервёмся и посмотрим как читать и писать cookie на низком уровне. Это поможет вам понять как работают инструменты, которые описаны далее. И это вам пригодится, если вам придётся напрямую работать с cookie для решения своих задач.

Прочитать уже установленный cookie невероятно просто. Каждый объект запроса обладает объектом COOKIES, который выглядит как словарь. Вы можете использовать его для чтения значения любого cookie, которые браузер отправил в представление:

def show_color(request):
    if "favorite_color" in request.COOKIES:
        return HttpResponse("Your favorite color is %s" % \
            request.COOKIES["favorite_color"])
    else:
        return HttpResponse("You don't have a favorite color.")

Записать cookie гораздо сложнее. Вам потребуется использовать метод set_cookie() объекта HttpResponse. Ниже приведён пример установки cookie favorite_color, значение для которого берётся из параметра GET:

def set_color(request):
    if "favorite_color" in request.GET:

        # Create an HttpResponse object...
        response = HttpResponse("Your favorite color is now %s" % \
            request.GET["favorite_color"])

        # ... and set a cookie on the response
        response.set_cookie("favorite_color",
                            request.GET["favorite_color"])

        return response

    else:
        return HttpResponse("You didn't give a favorite color.")

Вы также можете передать ряд необязательных аргументов в response.set_cookie(), которые управляют поведением cookie, подробности описаны в таблице « Опции cookie »:

Таблица 12.1. Опции cookie

Параметр
По умолчанию
Описание

max_age
None
Время (в секундах) активности cookie. Если данный параметр имеет значение None, то cookie действует до закрытия браузера.

expires
None
Точная дата и время окончания действия cookie. Должна быть указана в формате Wdy, DD-Mth-YY HH:MM:SS GMT. Если этот параметр установлен, он имеет преимущество над max_age.

path
/

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

Этот параметр очень полезен в случае, когда вы не контролируете высший уровень домена вашего сайта.

domain
None

Домен для которого действителен cookie. Вы можете использовать этот параметр для установке cookie для другого домена. Например, domain=".example.com" установит cookie, который будет отправляться страницам доменов www.example.com, www2.example.com и an.other.sub.domain.example.com.

Если этот параметр установлен в None, то cookie будет доступен только для установившего его домена.

secure
False
Если параметр установлен в True, то браузер передаёт этот cookie на сервер по протоколу HTTPS.

Всеобщее счастье

Вы могли отметить ряд потенциальных проблем методики работы с cookie. Взглянем на наиболее важные из них:

Хранение cookie является исключительно добровольным. Браузеры ничего не гарантируют. В действительности все браузеры позволяют пользователям управлять политикой работы с cookie. Если вы желаете увидеть как важны cookie для веба, включите опцию браузера «Спрашивать разрешение для каждого cookie».

Несмотря на их почти универсальное использование, cookie ненадёжны. Это означает, что разработчики должны проверять, что пользователь действительно принял cookie прежде чем начинать использовать их.

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

Cookie небезопасны, особенно те, которые отправлялись без использования протокола HTTPS. Cookie крайне легко прочитать, по причине того, что они пересылают информацию чистым текстом. Таким образом, атакующий прослушивает канал, ловит cookie и читает информацию. Это означает, что вы никогда не будете пересылать важную информацию в cookie.

Существует более хитрая атака, известная как человек посередине, когда атакующий перехватывает cookie и использует его, чтобы представиться вами. Глава 19 FIMXE подробно описывает такие атаки и методы их предотвращения.

Cookie не защищены даже от того, кому они отсылаются. Большинство браузеров предоставляют простые способы изменения содержимого отдельных cookie, а изобретательные пользователи могут всегда воспользоваться такими инструментами, как mechanize для ручного создания HTTP запросов.

Таким образом, вы не можете хранить в cookie данные, которые могут быть чувствительны к взлому. Стандартная ошибка при работе с cookie — хранение чего-нибудь такого IsLogged=1 в cookie после авторизации пользователя. Вы удивились бы количеству сайтов в которых используется такой механизм при авторизации пользователей. Нужна только секунда для взлома таких «защищённых» сайтов.

Сессии

Если принять во внимание все эти ограничения и потенциальные дыры в безопасности, становится очевидно, что cookie и постоянные сессии являются «головной болью» веб разработчиков. Цель Django в данном случае — быть эффективным средством лечения этой боли, таким образом, оно поставляется со средой управления сессиями, которая решает эти задачи за вас.

Среда управления сессиями позволяет вам хранить и получать определённые данные на основе посетителей сайта. Она сохраняет данные на сервере и берёт управление получением и отправлением cookie на себя. Cookie используются только для передачи идентификатора сессии, а не самих данных, это предохраняет вас от большинства проблем с cookie.

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

Активация

Сессии реализованы с помощью отдельного модуля (см. главу « Компоненты ») и моделей Django. Для активации сессий выполните следующие шаги:

Измените параметр MIDDLEWARE_CLASSES, добавив в него значение django.contrib.sessions.middleware.SessionMiddleware.

Внесите в параметр INSTALLED_APPS значение django.contrib.sessions. Запустите manage.py syncdb после этого.

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

Если вам не требуется использовать сессии, вы можете удалить строку SessionMiddleware из параметра MIDDLEWARE_CLASSES и строку django.contrib.sessions из параметра INSTALLED_APPS. Это позволит вам освободить крайне незначительную часть ресурсов, но из маленьких частей складываются большие.

Использование в функциях представления

После активации SessionMiddleware каждый объект HttpRequest — первый аргумент любой функции представления Django — будет иметь атрибут session по функциональности аналогичный словарю. Пример:

# Установим значение ключа в сессии:
request.session["fav_color"] = "blue"

# Получим значение ключа из сессии -- можно это сделать в
# другом представлении или через несколько запросов
# (или и то и другое):
fav_color = request.session["fav_color"]

# Чистим значение ключа в сессии:
del request.session["fav_color"]

# Проверка наличия ключа в сессии:
if "fav_color" in request.session:
    ...

Вы также можете использовать другие методы, такие как keys() и items() объекта request.session.

Есть ряд простых правил для эффективного использования сессий в Django:

Используйте для объекта request.session обычные строки Python в качестве ключей словаря. Это правило служит больше для удобства, чем для ускорения работы, но оно стоит того, чтобы ему следовать.

Словарные ключи сессии, которые начинаются с символа «подчёркивание», зарезервированы для внутреннего использования Django. На практике среда использует небольшой набор зарезервированных переменных в сессии, но пока вы не знаете об их назначении, не стоит их трогать.

Не заменяйте request.session новым объектом и не работайте с его атрибутами. Используйте его как обычный словарь Python.

Рассмотрим несколько примеров. Нижеприведённая простейшая функция представления устанавливает переменной has_commented значение True после того, как пользователь отправляет комментарий. Это простой (но небезопасный) способ предотвращения повторной отправки пользовательского комментария:

def post_comment(request, new_comment):
    if request.session.get('has_commented', False):
        return HttpResponse("Вы уже отправили комментарий")
    c = comments.Comment(comment=new_comment)
    c.save()
    request.session['has_commented'] = True
    return HttpResponse('Спасибо за ваш комментарий!')

Это простейшее представление аутентифицирует пользователя на сайте:

def login(request):
    try:
        m = Member.objects.get(username__exact=request.POST['username'])
        if m.password == request.POST['password']:
            request.session['member_id'] = m.id
            return HttpResponse("Вы авторизованы.")
    except Member.DoesNotExist:
        return HttpResponse("Ваши логин и пароль не соответствуют.")

Это представление отключает ранее авторизованного пользователя:

def logout(request):
    try:
        del request.session['member_id']
    except KeyError:
        pass
    return HttpResponse("Вы вышли.")

Замечание

В настоящем проекте так не делают. Применение ранее упомянутой среды аутентификации упрощает решение этой задачи. Эти примеры были умышленно упрощены для облегчения их понимания.

Установка тестовых cookie

Как было сказано ранее, вы не можете полагаться на то, что каждый браузер будет принимать cookie. Таким образом, для удобства, Django предоставляет простой способ проверки возможности приёма браузером отправляемых вами cookie. Вам просто следует вызвать в представлении метод request.session.set_test_cookie() и проверить в последующем представлении (не в том же самом), что возвращает метод request.session.test_cookie_worked().

Это неудобное разделение вызовов методом является необходимым из-за особенностей функционирования cookie. Когда вы устанавливаете cookie, вы не можете проверить, принял ли браузер cookie или нет. Для проверки требуется сделать дополнительный запрос.

Хорошим тоном является уборка за собой с помощью метода delete_test_cookie(). Выполните уборку после завершения проверки механизма cookie.

Ниже приведён пример обычного использования этого механизма:

def login(request):

    # Если мы отправили форму...
    if request.method == 'POST':

        # Проверить, что браузер принимает cookie:
        if request.session.test_cookie_worked():

            # Браузер принимает, удаляем тестовый cookie.
            request.session.delete_test_cookie()

            # На практике нам потребуется некая логика для проверки
	    # логина и пароля, но так как это всего лишь пример...
            return HttpResponse("You're logged in.")

        # Проверка не прошла, отображаем сообщение об ошибке.
	# На настоящем сайте нам потребуется отображать это покрасивее.
        else:
            return HttpResponse("Please enable cookies and try again.")

    # Если мы не отсылали форму, отправляем тестовое cookie
    # вместе с формой аутентификации.
    request.session.set_test_cookie()
    return render_to_response('foo/login_form.html')

Замечание

И снова, встроенные функции аутентификации выполнят эту проверку за вас.

Использование сессий вне функций представления

С точки зрения реализации, каждая сессия является обычной моделью Django, определённой в django.contrib.sessions.models. Каждая сессия идентифицировануется 32-символьным хэшем, который сохраняется в cookie. Раз это обычная модель, вы можете получать доступ к сессиям с помощью стандартного API для доступа к базам данных:

>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)

Вам потребуется вызвать метод get_decoded() для получения реальных данных сессии. Этот вызов необходим по причине того, что словарь хранится в закодированном виде:

>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}

Когда сессии сохраняются

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

# Сессия изменена.
request.session['foo'] = 'bar'

# Сессия изменена.
del request.session['foo']

# Сессия изменена.
request.session['foo'] = {}

# Фишка: Сессия НЕ изменена, так как производится изменение
# request.session['foo'], а не request.session.
request.session['foo']['bar'] = 'baz'

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

Следует отметить, что cookie сессии отправляется только в случае, когда сессия была создана или изменена. Если параметр SESSION_SAVE_EVERY_REQUEST имеет значение True, cookie сессии будет отправляться при каждом запросе. Аналогично, элемент сессии expires обновляется каждый раз при отправке cookie.

Время жизни сессий

Вы могли отметить, что cookie, которую нам прислал Google содержала expires=Sun, 17-Jan-2038 19:14:07 GMT;. Cookie могут по необходимости содержать дату окончания своей деятельности, которая указывает браузеру, когда надо удалить эту cookie. Если cookie не содержит такой элемент, браузер удалит её после того, как пользователь закроет его. Вы можете контролировать поведение среды управления сессиями с помощью параметра SESSION_EXPIRE_AT_BROWSER_CLOSE.

По умолчанию параметр SESSION_EXPIRE_AT_BROWSER_CLOSE имеет значение False, которое означает, что cookie сессий будут сохраняться в пользовательском браузере на SESSION_COOKIE_AGE секунд (изначально это значение равно двум неделям или 1'209'600 секунд). Используйте эту возможность, если не требуется постоянно заставлять людей аутентифицироваться при каждом открытии браузера.

Если параметр SESSION_EXPIRE_AT_BROWSER_CLOSE имеет значение True, Django будет использовать cookie, которые будут «жить» пока пользователь не закроет браузер.

Другие параметры сессии

Кроме уже рассмотренных параметров есть ещё несколько, которые влияют на то как Django работает с сессиями, используя cookie. Они показаны в таблице « Параметры, влияющие на поведение cookie »:

Таблица 12.2. Параметры, влияющие на поведение cookie

Параметр | По умолчанию | Описание
SESSION_COOKIE_DOMAIN | None | Домен для которого предназначено cookie. Строковое значение, подобное «.lawrence.com» для кроссдоменных cookie или используйте None для текущего домена.
SESSION_COOKIE_NAME | sessionid | Имя для cookie используемого для сессии. Может быть любой строкой.
SESSION_COOKIE_SECURE | False | Определяет надо ли использовать «защищённые» cookie для сессии. Если значение этого параметра установлено в True, cookie будет помечено как «защищённое» и браузеры будут передавать его только по HTTPS.

Техническая информация

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

Словарь сессии принимает любой объект Python, который может быть сериализован с помощью модуля pickle. За подробностями обратитесь к документации на язык Python к модулю pickle.

Данные сессии хранятся в таблице django_session базы данных.

Данные сессии доступны по требованию. Если вы никогда не обращаетесь к request.session, Django не будет обращаться к этой таблице в базе данных.

Django передаёт cookie только когда это надо. Если вы не назначили данные для сессии, Django не будет передавать cookie для сессии (если только вы не активировали SESSION_SAVE_EVERY_REQUEST).

Среда управления сессиями основана на использовании cookie. Она не будет пытаться размещать идентификатор сессии в URL, как это делают PHP или JSP, в случае когда пользователь отключил поддержку cookie.

Такое поведение определено дизайном Django. Размещение идентификатора сессии в URL не только уродует последний, но и также делает ваш сайт беззащитным к определённым формам кражи идентификатора через заголовок Referrer.

Если мы ещё не убили в вас любопытство, дальнейшие шаги очевидны. Загляните в django.contrib.sessions, там много интересного.

Аутентификация пользователей

Сейчас вы находитесь на половине пути, соединяющим браузер с Реальными Людьми™. Сессии предоставляют нам механизм хранения данных между отдельными запросами. Вторая часть уравнения — использование сессии для авторизации пользователя. Конечно, мы не можем просто доверять, что они те, кем представляются. Следовательно, нам потребуется механизм аутентификации.

Django предоставляет инструменты для решения этой частой задачи (и для многих других). Система аутентификации пользователей Django управляет аккаунтами пользователей, группами, правами и пользовательскими сессиями (с помощью cookie). Эту систему часто называют системой аутентификации и авторизации (auth/auth или система АА). Двойное имя означает, что опознавание пользователей часто состоит из двух шагов. Нам необходимо:

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

Проверить, что пользователь авторизован для выполнения определённых действия (обычно, с помощью проверки по таблице прав).

Согласно этим нуждам система АА состоит из ряда частей:

Пользователи: Люди, зарегистрированные на вашем сайте.

Права: Бинарные (да/нет) флаги, определяющие может данный пользователь выполнять определённую задачу.

Группы: Общий способ применения меток и прав на группу пользователей.

Сообщения: Простой способ организации очереди и отображения системных сообщений пользователям.

Профайлы: Механизм добавления в объект пользователя определённых полей.

Если вы пользовались интерфейсом администратора (описанном в главе « Интерфейс администратора Django », вы видели многие из этих инструментов. Если вы управляли пользователями и группами через интерфейс администратора, в действительности вы управляли данными в таблицах системы аутентификации.

Активация

Подобно среде управления сессиями, поддержка аутентификации поставляется в виде Django приложения в django.contrib, которое необходимо установить. Как и раньше, система АА уже установлена по умолчанию, но если вы её отключали, то необходимо выполнить следующие шаги:

Следует проверить, что среда управления сессиями установлена, как описано ранее в этой главе, так как от неё зависит работоспособность системы аутентификации.

Добавьте в параметр INSTALLED_APPS значение django.contrib.auth и запустите manage.py syncdb.

Проверьте, что в параметр MIDDLEWARE_CLASSES добавлено значение django.contrib.auth.middleware.AuthenticationMiddleware после SessionMiddleware.

После выполнения установки мы готовы работать с пользователями в функциях представлений. Главным интерфейсом, который вы будете использовать для управления пользователями, является request.user — это объект, который представляет собой текущего авторизованного пользователя. Если пользователь не авторизован, то вместо этого объекта будет AnonymousUser (подробности будут дальше).

Определить авторизовался ли пользователь можно с помощью метода is_authenticated():

if request.user.is_authenticated():
    # Пользователь авторизован.
else:
    # Анонимный пользователь.

Использование объекта User

Как только вы получили объект User — чаще всего от request.user, но возможно и с помощью одного из ранее описанных методов — у вас появился доступ к полям и методам этого объекта. Объекты AnonymousUser эмулируют часть этого интерфейса, но не всю, поэтому вы всегда должны проверять авторизацию пользователя с помощью метода is_authenticated() перед работой с методами объекта. В таблице « Поля объектов User » приведены поля, а в таблице « Методы объектов User » — методы объектов User:

Таблица 12.3. Поля объектов User

Поле | Описание
username | Логин. Обязательное поле. Максимум 30 символов. Можно использовать только буквы, цифры и символ подчёркивания.
first_name | Имя пользователя. Необязательное поле. Максимум 30 символов.
last_name | Фамилия пользователя. Необязательное поле. Максимум 30 символов.
email | Электронная почта. Необязательное поле.
password | Пароль. Обязательное поле. Хэш и метаданные пароля (Django не хранит пароль в открытом виде). За подробностями обращайтесь к секции «Пароли» ранее.
is_staff | Определяет входит ли пользователь в привелигерованную группу. Булево значение.
is_active | Определяет, активен ли данный аккаунт, т.е. можно ли им пользоваться. Булево значение. Установите в False для блокировки аккаунта.
is_superuser | Определяет входит ли пользователь в группу администраторов. Булево значение.
last_login | Дата и время последней аутентификации. По умолчанию устанавливается текущее время.
date_joined | Дата и время создания аккаунта. По умолчанию указывается дата и время создания аккаунта.

Таблица 12.4. Методы объектов User

Поле | Описание
is_authenticated() | Всегда возвращает True для «настоящих» объектов User. Метод определяет был ли текущий пользователь авторизован. Метод не назначает никаких прав и не проверяет активность пользователя. Он просто является индикатором аутентификации пользователя.
is_anonymous() | Возвращает True только для объектов AnonymousUser (и False для «настоящих» объектов User). В общем, лучше использовать метод is_authenticated().
get_full_name() | Возвращает значения first_name и last_name с пробелом между ними.
set_password(passwd) | Назначает пароль для пользователя, переданный в виде строки. Метод шифрует пароль. Метод не сохраняет объект User.
check_password(passwd) | Возвращает True при совпадении переданного пароля с имеющимся.
get_group_permissions() | Возвращает в виде строк список прав, которыми обладает пользователь благодаря вхождению в группы.
get_all_permissions() | Возвращает в виде строк список всех прав, которыми обладает пользователь.
has_perm(perm) | Возвращает True, если пользователь обладает указанным правом, которое представлено в формате «package.codename». Если пользователь неактивен, метод будет возвращать False.
has_perms(perm_list) | Возвращает True, если пользователь обладает всеми правами, указанными в переданном списке. Если пользователь неактивен, метод будет возвращать False.
has_module_perms(app_label) | Возвращает True, если пользователь обладает любым правом из переданного app_label. Если пользователь неактивен, метод будет возвращать False.
get_and_delete_messages() | Возвращает список объектов Message из пользовательской очереди и очищает очередь.
email_user(subj, msg) | Отправляет сообщение пользователю. Сообщение отсылается от значения, указанного в параметре DEFAULT_FROM_EMAIL. Вы можете также передать третий аргумент, from_email, для указания другого адреса электронной почты.
get_profile() | Возвращает профайл пользователя. Обратитесь к разделу « Профайлы » для подробностей.

Наконец, объекты User имеют поля «многие-ко-многим»: groups и permissions. Объекты User могут взаимодействовать с другими объектами тем же способом, что и другие поля с такими отношениями:

# Определим группу для пользователя:
myuser.groups = group_list

# Добавим пользователя в несколько других групп:
myuser.groups.add(group1, group2,...)

# Удалим пользователя из этих групп:
myuser.groups.remove(group1, group2,...)

# Удалим пользователя из всех групп:
myuser.groups.clear()

# С правами работаем аналогично:
myuser.permissions = permission_list
myuser.permissions.add(permission1, permission2, ...)
myuser.permissions.remove(permission1, permission2, ...)
myuser.permissions.clear()

Авторизация и выход

Django предоставляет встроенные функции представления для работы с авторизацией и выходом пользователя (а так же ещё для некоторых вещей), но прежде чем мы начнём их использовать, рассмотрим «вручную» как пользователи авторизуются и выходят. Django предоставляет в django.contrib.auth две функции для выполнения таких действий: authenticate() и login().

Для аутентификации пользователя по переданному логину и паролю следует использовать функцию authenticate(). Она принимает два именованных аргумента, username и password, и возвращает объект User, если пароль соответствует логину. В противном случае функция возвращает None:

>>> from django.contrib import auth
>>> user = auth.authenticate(username='john', password='secret')
>>> if user is not None:
...     print "Правильно!"
... else:
...     print "Ой, что-то пошло не так!"

Функция authenticate() только проверяет удостоверение. Для авторизации следует использовать функцию login(). Она принимает объекты HttpRequest и User, сохраняет идентификатор пользователя в сессии, используя соответствующий механизм.

Этот пример показывает как можно совместно использовать эти функции в представлении:

from django.contrib import auth

def login(request):
    username = request.POST['username']
    password = request.POST['password']
    user = auth.authenticate(username=username, password=password)
    if user is not None and user.is_active:
        # Правильный пароль и пользователь "активен"
        auth.login(request, user)
        # Перенаправление на "правильную" страницу
        return HttpResponseRedirect("/account/loggedin/")
    else:
        # Отображение страницы с ошибкой
        return HttpResponseRedirect("/account/invalid/")

Для выхода пользователя следует использовать функцию logout(). Она принимает объект HttpRequest и не возвращает ничего:

from django.contrib import auth

def logout(request):
    auth.logout(request)
    # Перенаправление на страницу.
    return HttpResponseRedirect("/account/loggedout/")

Следует отметить, что функция logout() не вызывает исключений, если пользователь не был авторизован.

На практике вам обычно не понадобится писать свои функции авторизации и выхода, так как система АА поставляется с набором представления для общей обработки этих процессов.

Первым шагом для использования представлений аутентификации будет их привязка к URL:

from django.contrib.auth.views import login, logout

urlpatterns = patterns('',
    # существующие шаблоны располагаются здесь...
    (r'^accounts/login/$',  login),
    (r'^accounts/logout/$', logout),
)

URL /accounts/login/ и /accounts/logout/ являются стандартными для этих представлений.

По умолчанию, представление login использует шаблон registration/login.html (вы можете указать на другой шаблон, передав дополнительный аргумент в представление, template_name). Эта форма нужна для отображения полей username и password. Простой шаблон может выглядеть так:

{% extends "base.html" %}

{% block content %}

  {% if form.errors %}
    <p class="error">Сожалеем, вы неправильно ввели логин или пароль</p>
  {% endif %}

  <form action='.' method='post'>
    <label for="username">Логин:</label>
    <input type="text" name="username" value="" id="username">
    <label for="password">Пароль:</label>
    <input type="password" name="password" value="" id="password">

    <input type="submit" value="login" />
    <input type="hidden" name="next" value="{{ next|escape }}" />
  </form>

{% endblock %}

Если пользователь ввёл всё правильно, он перенаправляется по умолчанию на страницу /accounts/profile/. Вы можете переопределить это поведение, добавив скрытое поле с именем next к URL для перенаправления на другую страницу. Вы также можете передать это значение в качестве параметра GET запроса в представление авторизации и оно будет автоматически добавлено к контексту в виде переменной next, которую вы можете вставить в скрытое поле.

Представление для выхода работает немного по-другому. По умолчанию, оно использует шаблон registration/logged_out.html (которое обычно содержит сообщение «Вы успешно вышли с сайта»). Однако, вы можете вызвать представление с дополнительным аргументом, next_page, который укажет представлению на какую страницу следует перейти после выхода.

Ограничение доступа авторизованным пользователям

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

Простой и прямолинейный способ ограничения доступа к страницам — проверить результат выполнения request.user.is_authenticated() и перенаправить на страницу аутентификации:

from django.http import HttpResponseRedirect

def my_view(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/login/?next=%s' % request.path)
    # ...

или, возможно, отобразить сообщение об ошибке:

def my_view(request):
    if not request.user.is_authenticated():
        return render_to_response('myapp/login_error.html')
    # ...

Для сокращения вы можете использовать удобный декоратор login_required:

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    # ...

Декоратор делает следующее:

Если пользователь не авторизован, перенаправить его на /accounts/login/, передав текущий URL в строке запроса как параметр next. Например: /accounts/login/?next=/polls/3/.

Если пользователь авторизован, отобразить представление. Код функции представления предполагает в этом случае, что пользователь авторизован.

Ограничение доступа определённым пользователям

Процедура ограничения доступа, основанная на наличии определённых прав или на какой-дибо другой проверке, или на перенаправлении на другой раздел сайта для авторизации, работает таким же образом (см. предыдущий раздел).

Прямолинейным способом является проверка request.user в представлении. Например, нижеприведённое представление проверяет авторизован ли пользователь и обладает ли он правом polls.can_vote (о правах будет рассказано ниже):

def vote(request):
    if request.user.is_authenticated() and request.user.has_perm('polls.can_vote')):
        # Голосуем здесь
    else:
        return HttpResponse("Вы не можете голосовать по этому списку избирателей.")

И снова Django предоставляет сокращение user_passes_test. Оно принимает аргументы и создаёт специальный декоратор для вашей частной задачи:

def user_can_vote(user):
    return user.is_authenticated() and user.has_perm("polls.can_vote")

@user_passes_test(user_can_vote, login_url="/login/")
def vote(request):
    # Здесь мы точно уверены, что пользователь авторизован и имеет
    # необходимы права.
    ...

Сокращение user_passes_test принимает один обязательный аргумент: функцию, которая принимает объект User и возвращает True, если пользователь успешно прошёл проверку. Следует отметить, что user_passes_test не производит автоматически проверку авторизации пользователя, вам следует сделать это самостоятельно.

В этом примере мы показали второй необязательный аргумент login_url, который позволяет указать URL для страницы аутентификации (по умолчанию, /accounts/login/).

Так как задача проверки прав пользователя является общей, Django предоставляет сокращение и для этого случая: декоратор permission_required(). Используя этот декоратор, перепишем предыдущий пример так:

from django.contrib.auth.decorators import permission_required

@permission_required('polls.can_vote', login_url="/login/")
def vote(request):
    # ...

Следует отметить, что permission_required() также принимает необязательный параметр login_url, который также, по умолчанию, указывает на /accounts/login/.

Ограничение доступа к базовым представлениям

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

from django.contrib.auth.decorators import login_required
from django.views.generic.date_based import object_detail

@login_required
def limited_object_detail(*args, **kwargs):
    return object_detail(*args, **kwargs)

Естественно, вы можете заменить login_required на любой другой декоратор.

Управление пользователями, правами и группами

Самым простым способом управления системой аутентификации является интерфейс администратора. В главе « Интерфейс администратора Django » рассматривалось как управлять пользователями, их правами и доступом, большую часть времени для этого использовался интерфейс администратора.

Тем не менее, существуют API, которые могут предоставить вам абсолютный контроль над пользователями. Мы рассмотрим их далее.

Создание пользователей

Создать пользователя можно с помощью функции create_user():

>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user(username='john',
...                                 email='jlennon@beatles.com',
...                                 password='glass onion')

В данном случае user является экземпляром класса User, готовым к хранению в базе данных. Функция create_user() не вызывает save(). Вы можете изменять атрибуты экземпляра до его сохранения:

>>> user.is_staff = True
>>> user.save()

Изменение пароля

Изменить пароль пользователя можно с помощью функции set_password():

>>> user = User.objects.get(username='john')
>>> user.set_password('goo goo goo joob')
>>> user.save()

Не устанавливайте атрибут password напрямую, если не знаете, что делаете. Пароль хранится в виде подсоленного хэша FIXME (т.е. хэш с префиксом) и следовательно его просто так не изменить.

Атрибут password объекта User является строкой формата:

hashtype$salt$hash

Здесь представлены тип хэша, соль и сам хэш, разделённые символами доллара.

Тип хэша (hashtype) может быть либо sha1 (по умолчанию), либо md5 и определяет односторонний алгоритм, который используется для создания хэша для пароля. Соль (salt) является случайной строкой, используемой для подсаливания пароля при создании хэша, например:

sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4

Функции User.set_password() и User.check_password() умеют устанавливать и проверять эти значения.

Неужели «подсоленный хэш» имеет отношение к наркоте?

Нет, подсоленный хэш не имеет ничего общего с марихуаной. На самом деле это общий метод безопасного хранения паролей. Хэш — это односторонняя криптографическая функция. Такую функцию просто вычислить, но практически невозможно получить обратно из хэша оригинальное значение.

Если бы мы хранили пароли в чистом виде, т.е. как текст, то любой, получивший доступ к базе паролей, мог бы их прочитать. Хранение паролей в виде хэша затрудняет получение паролей из украденной базы данных.

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

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

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

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

Управление регистрацией

Мы можем использовать эти низкоуровневые инструменты для создания представлений, которые позволяют пользователям регистрироваться на сайте. Почти каждый разработчик желает реализовать процедуру регистрации по своему, по этой причине Django оставляет эту часть работы для вас. К счастью, эта работа не сложна.

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

from django import oldforms as forms
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.contrib.auth.forms import UserCreationForm

def register(request):
    form = UserCreationForm()

    if request.method == 'POST':
        data = request.POST.copy()
        errors = form.get_validation_errors(data)
        if not errors:
            new_user = form.save(data)
            return HttpResponseRedirect("/books/")
    else:
        data, errors = {}, {}

    return render_to_response("registration/register.html", {
        'form' : forms.FormWrapper(form, data, errors)
    })

Эта форма использует шаблон registration/register.html. Ниже представлен вариант содержимого этого шаблона:

{% extends "base.html" %}

{% block title %}Регистрация пользователя{% endblock %}

{% block content %}
  <h1>Регистрация пользователя</h1>
  <form action="." method="post">
    {% if form.error_dict %}
      <p class="error">Пожалуйста исправьте нижеприведённые ошибки.</p>
    {% endif %}

    {% if form.username.errors %}
      {{ form.username.html_error_list }}
    {% endif %}
    <label for="id_username">Логин:</label> {{ form.username }}

    {% if form.password1.errors %}
      {{ form.password1.html_error_list }}
    {% endif %}
    <label for="id_password1">Пароль:</label> {{ form.password1 }}

    {% if form.password2.errors %}
      {{ form.password2.html_error_list }}
    {% endif %}
    <label for="id_password2">Пароль (снова):</label> {{ form.password2 }}

    <input type="submit" value="Зарегистрировать" />
  </form>
{% endblock %}

Замечание

На время публикации этой книги класс django.contrib.auth.forms.UserCreationForm являлся устаревшим. Обратитесь к документации на http://www.djangoproject.com/documentation/0.96/forms/ для подробностей. Процесс миграции на новую версию форм, который описан в главе « Формы », будет завершён в ближайшем будущем.

Использование данных аутентификации в шаблонах

После авторизации пользователя вся информация о нём и о его правах становится доступной в контексте шаблона при использовании RequestContext (см. главу « Расширения для шаблонной системы »).

Замечание

Эти переменные становятся доступными в контексте шаблона только в том случае, если вы используете RequestContext и ваш параметр TEMPLATE_CONTEXT_PROCESSORS содержит значение django.core.context_processors.auth (по умолчанию так и есть). И снова, см. подробности в главе « Расширения для шаблонной системы ».

При использовании RequestContext текущий пользователь (либо экземпляр User, либо экземпляр AnonymousUser) хранится в переменной шаблона {{ user }}:

{% if user.is_authenticated %}
  <p>Добро пожаловать, {{ user.username }}. Спасибо, что не забываете нас.</p>
{% else %}
  <p>Добро пожаловать, незнакомец. Кто вы?</p>
{% endif %}

Права текущего пользователя хранятся в переменной шаблона {{ perms }}. Технически это не просто переменная, а промежуточный слой между рядом методов проверки прав, описанных ранее.

Существует два способа использования объекта perm. Вы можете использовать, что-то подобное {{ perms.polls }} для проверки того, имеет ли пользователь вообще права для конкретного приложения. Также вы можете использовать что-то подобное {{ perms.polls.can_vote }} для проверки, имеет ли пользователь конкретное право.

Следовательно, вы можете проверять права в шаблоне с помощью оператора {% if %}:

{% if perms.polls %}
  <p>У вас есть права на работу с приложением голосования.</p>
  {% if perms.polls.can_vote %}
    <p>Вы можете голосовать!</p>
  {% endif %}
{% else %}
  <p>У вас нет прав на использование приложения.</p>
{% endif %}

Остальное: права, группы, сообщения и профайлы

Кое-что о системе аутентификации и авторизации мы ещё не рассказали. Это время пришло.

Права

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

Сайт администратора Django использует права так:

Доступ к форме для добавления пользователя. Эта операция разрешена пользователям, которые обладают правом добавления для этого типа объектов.

Доступ к списку изменений и к соответствующей форме. Операция внесения изменения разрешена пользователям, которые обладают правом изменения для этого типа объектов.

Операция удаления объекта разрешена пользователям, которые обладают правом удаления для этого типа объектов.

Права на тип объекта устанавливаются глобально, а не на каждый экземпляр класса. Например, можно сказать «Маша может изменять новости», но нельзя сказать «Маша может изменять новости, но только те, которые создала сама» или «Маша может изменять только те новости, которые имеют определённый статус, дату публикации и идентификатор».

Такие три основных права — добавление, изменение и удаления — автоматически создаются для каждой модели Django, которая обладает классом Admin. Эти права добавляются в таблицу auth_permission в момент, когда вы запускаете manage.py syncdb.

Формат определения прав — <app>.<action>_<object_name>. Таким образом, если у вас есть приложение polls с моделью Choice, то вы будете использовать права так: polls.add_choice, polls.change_choice и polls.delete_choice.

Следует отметить, что если ваша модель не имеет класса Admin во время запуска manage.py syncdb, эти права не будут созданы. Если вы добавили класс Admin к модели после создания базы данных, вам потребуется запустить syncdb для обновления базы данных.

Вы также можете создать собственные права для определённой модели, используя атрибут permissions для Meta. Нижеприведённый пример модели создаёт три собственных права:

class USCitizen(models.Model):
    # ...
    class Meta:
        permissions = (
            # Идентификатор права       Описание права
            ("can_drive",               "Может водить"),
            ("can_vote",                "Может голосовать на выборах"),
            ("can_drink",               "Может пить алкоголь"),
        )

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

Аналогично пользователям, права реализованы в виде модели, которая находится в django.contrib.auth.models. Это означает, что вы можете использовать API для работы с базой данных для прямого взаимодействия с необходимыми вам правами.

Группы

Группы являются базовым способом категоризации пользователей и могут быть использованы для назначения прав для этих групп. Пользователь может принадлежать любому количеству групп.

Пользователь, входящий в группу, автоматически получает права назначенные этой группе. Например, если группа «Редакторы сайта» обладает правом can_edit_home_page, любой пользователь входящий в эту группу, получает это право.

Группы также являются удобным способом для назначения пользователя некоторой расширенной функциональности. Например, вы можете создать группу «Особые пользователи» и затем написать код, который может давать доступ этим пользователям к закрытой части сайта, или отправлять им электронное сообщение.

Аналогично пользователям, простейшим способом управления группами является интерфейс администратора. Однако, группы также являются моделью Django, которая находится в django.contrib.auth.models, следовательно вы всегда можете использовать API для работы с базой данных для взаимодействия с группами на низком уровне.

Сообщения

Система сообщений — это простой способ организации очереди сообщений для определённых пользователей. Сообщение ассоциировано с классом User. Нет никаких ограничение на время жизни сообщения.

Сообщения используются интерфейсом администратора Django после успешного выполнения действий. Например, при создании объекта вы получите сообщение «Объект успешно создан — The object was created successfully» на верху страницы.

Вы можете использовать этот API для организации очереди и вывода сообщений в вашем приложении. API прост:

Для создания нового сообщения используйте

user.message_set.create(message='message_text')

Для получения и удаления сообщения используйте user.get_and_delete_messages(), которая возвращает список объектов Message из пользовательской очереди (если такая есть) и удаляет сообщения из очереди.

В представлении, приведённом ниже, система сохраняет сообщение для пользователя после создания списка проигрывания:

def create_playlist(request, songs):
    # Создаём список проигрывания с определёнными треками.
    # ...
    request.user.message_set.create(
        message="Ваш список проигрывания был успешно добавлен."
    )
    return render_to_response("playlists/create.html",
        context_instance=RequestContext(request))

При использовании RequestContext информация об авторизованном пользователе и его сообщениях становятся доступны для контекста шаблона в виде переменной {{ messages }}. Ниже показа пример кода шаблона, который отображает сообщения:

{% if messages %}
<ul>
    {% for message in messages %}
    <li>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

Следует отметить, что RequestContext вызывает get_and_delete_messages(), таким образом любое сообщение будет удалено, даже если вы не отобразили его.

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

Профайлы

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

Множество сайтов нуждаются в хранении большего количество информации, чем позволяет стандартный объект User. Для решения этой задачи такие сайты имеют разные «дополнительные» поля. Таким образом, Django предоставляет простой способ определения объекта «профайл», который подключен к пользователю. Этот объект может меняться от проекта к проекту и даже может управлять различными профайлами для разных сайтов, работающих с использованием одной базы данных.

Первым шагом для создания профайла будет определение модели, которая будет хранить информацию профайла. Единственное требование Django к такой модели — она должна иметь уникальный ForeignKey к модели User. Это поле должно иметь имя user. Кроме данного поля вы можете добавлять любые необходимые вам поля. Ниже представлена крайне произвольная модель профайла:

from django.db import models
from django.contrib.auth.models import User

class MySiteProfile(models.Model):
    # This is the only required field
    user = models.ForeignKey(User, unique=True)

    # The rest is completely up to you...
    favorite_band = models.CharField(max_length=100, blank=True)
    favorite_cheese = models.CharField(max_length=100, blank=True)
    lucky_number = models.IntegerField()

Затем потребуется, чтобы вы указали Django где искать этот объект. Это делается с помощью параметра AUTH_PROFILE_MODULE, надо вписать в него идентификатор вашей модели. Таким образом, если ваша модель определена в приложении myapp, то вам потребуется сделать следующее:

AUTH_PROFILE_MODULE = "myapp.mysiteprofile"

После этого вы можете получить доступ к профайлу пользователя с помощью user.get_profile(). Эта функция может вызвать исключение SiteProfileNotAvailable, если параметр AUTH_PROFILE_MODULE не определён; или исключение DoesNotExist, если пользователь ещё не имеет профайла (обычно такие исключения перехватывают и на лету создают новый профайл).

Глава 13. Кэширование

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

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

Для большинства веб приложений такое расточительство не является критичным. Большинство веб приложений не похожи на http://washingtonpost.com/ или на SlashDot, это небольшие сайты со скромным трафиком. Но для более серьёзных сайтов желательно внимательно относится к экономии ресурсов. Этому поможет кэширование.

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

получен URL, пытаемся найти такую страницу в кэше
если страница есть в кэше:
    возвращаем закэшированную страницу
иначе:
    создаём страницу
    сохраняем её в кэше на будущее
    возвращаем созданную страницу

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

Django хорошо работает с кэшами высокого уровня, такими как Squid (http://www.squid-cache.org/ и кэши браузеров. Существуют типы кэшей, которые вы не можете контролировать напрямую, но которым можно отсылать инструкции (через HTTP заголовки), указывая какая часть вашего сайта должна быть закэширована и как.

Продолжайте читать, чтобы узнать как использовать систему кэширования в Django. Когда ваш сайт станет подобным Slashdot'у, вы похвалите себя за то, что поняли этот материал.

Настройка кэша

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

Настройка кэша производится через параметр CACHE_BACKEND файла конфигурации. Если вы используете кэширование и не указали CACHE_BACKEND, Django будет по умолчанию использовать simple:///. Следующие секции расскажут о всех возможных значениях данного параметра.

Memcached

Безусловно самый быстрый и самый эффективный тип кэша доступный Django — Memcached — является средой кэширования в оперативной памяти, разработанной для управления высокой нагрузкой на сервис LiveJournal (http://www.livejournal.com/ и впоследствии открытый компанией Danga Interactive (http://danga.com/). Он используется такими сайтами как Slashdot и Wikipedia для снижения количества запросов к базе данных и значительно увеличивает производительность сайтов.

Memcached доступен бесплатно на http://danga.com/memcached/. Он запускается в виде сервиса и выделяет себе указанный объём оперативной памяти. Его главной особенностью является интерфейс — супер-быстрый-и-лёгкий интерфейс — используемый для добавления, получения и удаления определённых данных из кэша. Все данные хранятся только в оперативной памяти, таким образом нет излишних затрат ресурсов на работу с файловой системой или базой данных.

После установки Memcached потребуется установить библиотеку с интерфейсом для языка Python, которая не поставляется вместе с Django. Эта библиотека доступна в виде одного модуля memcache.py, который доступен по адресу http://www.tummy.com/Community/software/python-memcached/.

Для использование Memcached совместно с Django установите для параметра CACHE_BACKEND значение memcached://ip:port/, где ip — это IP адрес сервера на котором работает сервис Memcached, а port — соответственно, порт.

В данном примере Memcached запущен на локальной машине (127.0.0.1) на порту 11211:

CACHE_BACKEND = 'memcached://127.0.0.1:11211/'

Одной из замечательных особенностей Memcached является его возможность разделять кэш между множеством серверов. Это означает, что вы можете запустить сервисы Memcached на нескольких машинах и ваше приложение будет рассматривать эту группу компьютеров как единый кэш, без необходимости копирования элементов кэша на каждый сервер. Для того, чтобы воспользоваться такими возможностями в Django, укажите все эти сервера в параметре CACHE_BACKEND, разделяя их точкой с запятой.

В данном примере кэш разделён между экземплярами Memcached, которые работают на машинах с IP адресами 172.19.26.240 и 172.19.26.242, оба на портах 11211:

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11211/'

В следующем примере кэш распределён по экземплярам Memcached, которые работают на IP адресах 172.19.26.240 (порт 11211), 172.19.26.242 (порт 11212) и 172.19.26.244 (11213):

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11212;172.19.26.244:11213/'

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

Кэширование в базу данных

Для использования таблицы базы данных в виде кэша следует создать такую таблицу и указать Django, что надо использовать её для кэширования.

Сначала, создаём таблицу с помощью команды:

python manage.py createcachetable [cache_table_name]

где [cache_table_name] — имя таблицы, которая будет создана. Имя может быть любым, главное, чтобы оно соответствовало требования базы данных и не было уже занято. Эта команда создаёт одну таблицу в базе данных в соответствующем для Django формате.

После создания таблицы необходимо назначить параметру CACHE_BACKEND значение db://tablename, где tablename — имя таблицы, которую вы создали. В данном примере, имя таблицы будет my_cache_table:

CACHE_BACKEND = 'db://my_cache_table'

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

Кэширование на файловую систему

Для хранения кэшированных элементов на файловой системе используйте тип file:// в параметре CACHE_BACKEND, указав каталог на файловой системе, который будет использоваться для кэша.

Например, для хранения данных кэша в каталоге /var/tmp/django_cache используйте это:

CACHE_BACKEND = 'file:///var/tmp/django_cache'

Следует отметить использование трёх слэшей. Первые два относятся к file://, а третий — к пути до каталога /var/tmp/django_cache. Если вы используете Windows, следует указывать букву диска после file://, вот так: file://c:/foo/bar.

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

Следует удостовериться, что каталог существует и доступен для записи и чтения пользователю, от которого запущен веб сервер. Продолжая предыдущий пример, если ваш сервер работает от пользователя apache, следует проверить, что каталог /var/tmp/django_cache доступен для записи и чтения этому пользователю.

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

Кэширование в оперативную память

Если вам требуется использовать скоростные преимущества кэширования в оперативную память, но у вас нет возможности использовать Memcached, рассмотрите использование локального кэша. Такой кэш организовывается для каждого процесса и позволяет многократное использование (thread-safe), но он не так эффективен как Memcached из-за своего простого механизма защёлок и стратегий выделения памяти.

Для использование этого типа кэширования установите для параметра CACHE_BACKEND значение locmem:///, например:

CACHE_BACKEND = 'locmem:///'

Простое кэширование (для разработки)

Простой, обслуживающий один процесс, кэш с расположением данных в оперативной памяти можно организовать с помощью simple:///, например:

CACHE_BACKEND = 'simple:///'

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

Фиктивное кэширование (для разработки)

Наконец, Django поставляется с «фиктивным» кэшем, который не производит никакого кэширования.

Такой тип кэширование полезен в случае когда у вас есть работающий сайт, который мощно использует кэширование, и тестовый сайт, который вам не надо кэшировать. В этом случае установите для параметра CACHE_BACKEND значение dummy:/// в файле конфигурации тестового сайта, например:

CACHE_BACKEND = 'dummy:///'

В результате этих действие тестовый сайт не будет использовать кэширование, а основной сайт будет.

Аргументы CACHE_BACKEND

Каждый тип кэша может принимать аргументы. Они указываются в значении параметра CACHE_BACKEND. Перечислим их:

timeout: Значение по умолчанию, в секундах, для применения кэширования. Значение по умолчанию — 300 секунд (5 минут).

max_entries: Для типов кэширования: простого, локального и базы данных, этот параметр устанавливает максимальное количество элементов кэширования. При превышении этого числа из кэша удаляются самые старые значения. Значение по умолчанию — 300.

cull_frequency: Процент элементов кэша, которые удаляются при достижении максимального значения аргумента max_entries. Вычисляется так: 1/cull_frequency. Таким образом, если установить cull_frequency=2, то при заполнении кэша будет произведено удаление 50% элементов кэша.

Значение 0 для этого аргумента означает, что весь кэш должен быть очищен при заполнении кэша. Это позволяет значительно ускорить очистку кэша, ценой потери закэшированной информации. Значение по умолчанию — 3.

В данном примере, аргумент timeout имеет значение 60:

CACHE_BACKEND = "locmem:///?timeout=60"

В данном примере, аргумент timeout имеет значение 30, аргумент max_entries — 400:

CACHE_BACKEND = "locmem:///?timeout=30&max_entries=400"

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

Кэширование отдельного сайта

После того, как вы указали значение для CACHE_BACKEND, самым простым способом использования кэша будет кэширование всего сайта. Это означает, что каждая страница, которая не имеет параметров GET или POST, будет помещена в кэш на указанное количество времени при первом обращении к ней.

Для активации этого способа кэширования необходимо добавить django.middleware.cache.CacheMiddleware в параметр MIDDLEWARE_CLASSES, как показано на примере:

MIDDLEWARE_CLASSES = (
    'django.middleware.cache.CacheMiddleware',
    'django.middleware.common.CommonMiddleware',
)

Замечание

Порядок указания значений в параметре MIDDLEWARE_CLASSES имеет значение. Обратитесь к секции « Порядок в MIDDLEWARE_CLASSES », описанной в этой главе далее.

Затем добавьте следующие обязательные параметры в файл конфигурации Django:

CACHE_MIDDLEWARE_SECONDS: Количество секунд в течении которых страница должна храниться в кэше.

CACHE_MIDDLEWARE_KEY_PREFIX: Если кэш распределён между несколькими сайтами под управлением одной инсталляции Django, назначьте этому параметру имя сайта или любую другую строку, которая будет уникальна для этого экземпляра Django, это поможет избежать коллизии ключей. Можете оставить значение пустым, если вас это не волнует.

Система кэширует каждую страницу, которая не содержит GET или POST параметров. Таким образом, если пользователь запрашивает страницу и передаёт в запросе параметры GET или POST, система не пытается получить закэшированную версию страницы. Если вы намереваетесь использовать данный способ кэширования, следует помнить эту особенность при проектировании своего приложения.

Механизм кэширования поддерживает ещё один параметр — CACHE_MIDDLEWARE_ANONYMOUS_ONLY. Если вы определили этот параметр и назначили ему значение True, то в этом случае механизм будет производить кэширование только анонимных запросов. Такое поведение позволяет просто и эффективно отключить кэширование для любых пользовательских страниц, таких как интерфейс администратора Django. Следует отметить, что если вы используете CACHE_MIDDLEWARE_ANONYMOUS_ONLY, вам следует удостовериться, что вы активировали AuthenticationMiddleware и что оно указано до CacheMiddleware в параметре MIDDLEWARE_CLASSES.

Наконец, следует отметить, что CacheMiddleware автоматически устанавливает несколько заголовков в каждом HttpResponse:

Заголовок Last-Modified указывает дату и время когда была запрошена новая (некэшированная) версия страницы.

Заголовок Expires указывает дату и время плюс значение CACHE_MIDDLEWARE_SECONDS до окончания выдачи закэшированной версии страницы.

Заголовок Cache-Control определяет максимальный срок для страницы, относительно параметра CACHE_MIDDLEWARE_SECONDS.

Кэширование отдельного представления

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

Кэширование выполняется с помощью декоратора, который оборачивается вокруг функции представления и изменяет её поведение для использования механизма кэширования. Такой декоратор имеет имя cache_page и находится в модуле django.views.decorators.cache, например:

from django.views.decorators.cache import cache_page

def my_view(request, param):
    # ...
my_view = cache_page(my_view, 60 * 15)

Аналогично, если вы используете Python версии 2.4 и выше, вы можете использовать особый синтаксис для декоратора. Нижеприведённый пример эквивалентен предыдущему:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request, param):
    # ...

Декоратор cache_page принимает один аргумент, время жизни страницы в кэше, в секундах. В предыдущем примере, результат работы представления my_view() будет помещён в кэш на 15 минут. Следует отметить, что мы написали 60 * 15 лишь в целях лучшей читаемости. Можно было сразу написать 900.

Кэш для каждого представления, как и кэш для отдельного сайта, зависит от URL. Если несколько URL указывают на одно и то же представление, каждый URL будет кэшироваться отдельно. Продолжая пример с my_view(), если ваш файл со схемой URL выглядит так:

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', my_view),
)

то запросы к /foo/1/ и /foo/23/ будут закэшированы отдельно, как вы могли ожидать. Но после того как будет произведено кэширование определённого URL, все последующие запросы к нему будут использовать кэш.

Определение кэша для отдельного представления в файле со схемой URL

Примеры из предыдущей секции имели жёстко определённый факт кэширования представления, потому что cache_page() изменяет функцию my_view(). Этот подход связывает ваше представление с механизмом кэширования, который не идеален по нескольким причинам. Например, вам может потребоваться повторно использовать функции представлений в другом сайте, который не имеет механизма кэширования, или может потребоваться передать представления людям, которые пожелают использовать их без применения кэширования. Решением таких проблем является указание кэширования для определённых представления прямо в файле со схемой URL.

Это сделать легко: просто оберните функцию представления с помощью cache_page(). Ниже приведён старый пример файла со схемой URL:

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', my_view),
)

Здесь приведён тот же пример, но уже с функцией cache_page():

from django.views.decorators.cache import cache_page

urlpatterns = ('',
    (r'^foo/(\d{1,2})/$', cache_page(my_view, 60 * 15)),
)

При использовании этого подхода не следует забывать об импорте функции cache_page() в файле со схемой URL.

API низкого уровня

Изредка кэширование всей страницы не даёт ожидаемой пользы и, если честно, является неудобным.

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

Для случаев подобных этому, Django предоставляет простой интерфейс низкого уровня к механизму кэширования, который располагается в модуле django.core.cache. Вы можете использовать API низкого уровня для хранения объектов в кэше на любом уровне гранулярности, который вам потребуется. Вы можете поместить в кэш любой объект языка Python, который может быть сериализован с помощью модуля pickle: строки, словари, списки моделей объектов и так далее. (Почти каждый объект языка Python может быть сериализован, обратитесь к документации Python по модулю pickle.)

Вот так происходит импортирование API:

>>> from django.core.cache import cache

Простые методы set(key, value, timeout_seconds) и get(key):

>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'

Аргумент timeout_seconds является необязательным, а его значение по умолчанию было указано ранее.

Если объект не существует в кэше или сам кэш недоступен, то cache.get() возвращает None:

# Ждём 30 секунд пока 'my_key' "протухнет"...

>>> cache.get('my_key')
None

>>> cache.get('some_unset_key')
None

Мы не рекомендуем хранить строковое значение None в кэше, потому что вы не будете иметь возможность отличить ваше сохранённое None от отсутствия значения в кэше, которое тоже возвращается как None.

Функция cache.get() может принимать аргумент default. Он определяет, какое значение возвращать в случае отсутствия объекта в кэше:

>>> cache.get('my_key', 'has expired')
'has expired'

Для получения нескольких значений из кэша за один запрос надо использовать метод cache.get_many(). Этот метод обратиться к кэшу один раз при получении значений, если это позволяет механизм кэша. Метод cache.get_many() возвращает словарь с запрошенными ключами, теми которые не «протухли»:

>>> cache.set('a', 1)
>>> cache.set('b', 2)
>>> cache.set('c', 3)
>>> cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

Если запрашиваемое значение не существует или «протухло», то оно не будет включено в словарь:

>>> cache.get_many(['a', 'b', 'c', 'd'])
{'a': 1, 'b': 2, 'c': 3}

Наконец, вы можете явно удалять значения из кэша с помощью метода delete(). Это простой способ исключения определённого объекта из кэша:

>>> cache.delete('a')

Метод delete() не возвращает никакого значения и работает вне зависимости от существования указанного объекта в кэше.

Кэши верхнего уровня

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

Приведём несколько примеров:

Ваш провайдер может кэшировать определённые страницы, таким образом, если вы обращаетесь к странице сайта http://example.com/, ваш провайдер отдаст вам страницу, не выполняя запрос к сайту. Владельца сайта не будут знать об этом механизме кэширования. Провайдер находится между вами и сайтом, прозрачно выполняя все операции по кэшированию.

Ваш сайт может находится за прокси кэшем, таким как Squid (http://www.squid-cache.org/), который кэширует страницы для улучшения производительности сайта. В этом случае, каждый запрос сначала будет обработан прокси и затем, при необходимости, будет передан вашему приложению.

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

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

Например, скажем вы работаете с веб интерфейсом к электронной почте и содержимое страницы «Мой почтовый ящик» очевидно зависит от авторизованного пользователя. Если провайдер слепо закэшироует ваш сайт, то первый же посетитель этой страницы получит страницу, которая предназначалась (и была помещена в кэш) для вас. Это нехорошо.

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

Использование заголовков Vary

Заголовок Vary определяет какой из заголовков запроса должен рассматриваться механизмом кэширования. Например, если содержимое страницы зависит от языковой настройки пользователя, страница укажет на это с помощью заголовка.

По умолчанию механизм кэширования Django создаёт записи в кэше, используя заданный путь (т.е., /stories/2005/jun/23/bank_robbed/). Это означает, что каждый запрос по этому URL будет использовать версию страницы из кэша, независимо от разницы между агентами пользователя (user-agent), такими как cookie или языковые настройки. Тем не менее, если эта страница генерирует различное содержание, основываясь на информации из заголовков запроса, таких как cookie или язык или другой агент пользователя, вам потребуется использовать заголовок Vary, чтобы механизм кэширования знал эту зависимость.

Для использования этого в Django следует использовать удобный декоратор для представления vary_on_headers, вот такой:

from django.views.decorators.vary import vary_on_headers

# Синтаксис для Python 2.3.
def my_view(request):
    # ...
my_view = vary_on_headers(my_view, 'User-Agent')

# Синтаксис для Python 2.4+.
@vary_on_headers('User-Agent')
def my_view(request):
    # ...

В данном случае механизм кэширования (такой как в Django) сохраняет отдельную версию страницы для каждого уникального агента пользователя.

Преимущество в использовании декоратора vary_on_headers вместо ручного указания заголовка Vary (как-то так response['Vary'] = 'user-agent') в том, что декоратор добавляет информацию к заголовку Vary (а он может уже существовать), а не перезаписывает уже имеющиеся в нём данные.

Вы можете передать несколько заголовков в vary_on_headers():

@vary_on_headers('User-Agent', 'Cookie')
def my_view(request):
    # ...

Это указывает кэшу верхнего уровня, что ему надо учитывать оба варианта, что означает любую комбинацию агента пользователя и cookie. Например, запрос от агента Mozilla с cookie foo=bar будет рассматриваться отдельно от запроса агента Mozilla и cookie foo=ham.

Так как обычно все используют cookie для различения запросов, нашлось место и для декоратора vary_on_cookie. Представления, приведённые ниже, эквивалентны:

@vary_on_cookie
def my_view(request):
    # ...

@vary_on_headers('Cookie')
def my_view(request):
    # ...

Регистр заголовков, передаваемых в vary_on_headers(), не имеет значения. Заголовок User-Agent эквивалентен user-agent.

Вы также можете напрямую использовать вспомогательную функцию django.utils.cache.patch_vary_headers. Эта функция устанавливает или добавляет заголовок Vary, например:

from django.utils.cache import patch_vary_headers

def my_view(request):
    # ...
    response = render_to_response('template_name', context)
    patch_vary_headers(response, ['Cookie'])
    return response

Метод patch_vary_headers() принимает экземпляр HttpResponse в качестве первого аргумента и список или кортеж регистронезависимых имён заголовков в качестве второго аргумента.

Другие заголовки для механизма кэширования

Другими проблемами при использовании кэширования являются секретность данных и вопрос хранения данных в каскадных кэшах.

Пользователь обычно сталкивается с двумя видами кэшей: кэш браузера (частный кэш) и кэш провайдера (общий кэш). Общий кэш используется рядом пользователей и находится под контролем третьего лица. Это вызывает проблему надёжности данных. Скажем, вам не захочется, чтобы номер вашего банковского счета был сохранён в общем кэше. Таким образом веб приложения должны иметь способ указать кэшу, какие данные являются частными, а какие нет.

Решением будет указание кэшу, что страница будет частной. Для того, чтобы сделать это в Django следует использовать декоратор cache_control:

from django.views.decorators.cache import cache_control

@cache_control(private=True)
def my_view(request):
    # ...

Этот декоратор заботится об автоматической отправке соответствующих HTTP заголовков.

Существует несколько способов управления параметрами кэша. Например, HTTP позволяет приложения выполнять следующее:

Определять максимальное время кэширования для страницы.

Указывать должен ли кэш всегда проверять наличие новых версий и использовать сохранённые данные, только если не было изменений. (Некоторые кэши могут доставлять сохранённые данные даже в том случае, когда оригинальная страница имеет изменения, просто по той причине, что ещё не истёк срок действия закэшированной копии.)

Используйте декоратор cache_control в Django для определения этих параметров кэширования. В этом примере cache_control указывает, что требуется проверять наличие изменений в оригинальной странице при каждом доступе к ней и сохранять в кэше копии максимум 3600 секунд:

from django.views.decorators.cache import cache_control
@cache_control(must_revalidate=True, max_age=3600)
def my_view(request):
    ...

Любая разрешённая HTTP директива для Cache-Control разрешена и для cache_control(). Приведём полный список:

public=True

private=True

no_cache=True

no_transform=True

must_revalidate=True

proxy_revalidate=True

max_age=num_seconds

s_maxage=num_seconds

Совет

Для более подробной информации о HTTP директивах Cache-Control обратитесь к спецификации на http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.

Замечание

Механизм кэширования всегда устанавливает элемент max_age заголовка кэша в соответствии со значением из CACHE_MIDDLEWARE_SETTINGS. Если вы используете своё значение для max_age в декораторе cache_control(), декоратор будет иметь преимущество и значения заголовков будут объединены корректно.

Оптимизация

Django поставляется с несколькими другими механизмами, которые могут помочь в оптимизации вашего приложения:

django.middleware.http.ConditionalGetMiddleware добавляет поддержку для современных браузеров для условных GET откликов, основанную на заголовках ETag и Last-Modified.

django.middleware.gzip.GZipMiddleware сжимает ответ для всех современных браузеров, снижая нагрузку на сеть и время ожидания пользователя.

Порядок в MIDDLEWARE_CLASSES

При использовании CacheMiddleware важно располагать его в правильном месте в параметре MIDDLEWARE_CLASSES, потому что механизм кэширования должен получить информацию о заголовке, относительно которого ему придётся принимать решение о кэшировании страниц.

Следует размещать CacheMiddleware после других значений параметра, которые могут добавлять что-то в заголовок Vary, включая следующие:

SessionMiddleware, который добавляет Cookie.

GZipMiddleware, который добавляет Accept-Encoding.

Глава 14. Средства от других разработчиков

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

Одним из множества преимуществ использования Python является философия «всё включено»: когда вы устанавливаете Python, он поставляется с большой стандартной библиотекой пакетов, которые вы можете начать использовать немедленно, без необходимости скачивать что-нибудь ещё. Django старается следовать этой философии и включает в себя все свои стандартные библиотеки модулей, полезных для задач веб разработки. Эта глава рассказывает об этих модулях.

Стандартная библиотека Django

Стандартная Django находится в пакете django.contrib. В каждом субпакете находится отдельная часть дополнительного функционала. Эти части не всегда зависят друг от друга, но некоторые могут.

Нет жёсткого требования для типов функциональности в django.contrib. Некоторые из пакетов включают в себя модели (и следовательно требуют, чтобы вы установили их таблицы в базу данных), но другие могут состоять полностью из кода или шаблонов.

Единственная общая особенность пакетов django.contrib — если вы полностью удалите пакет django.contrib, вы сможете использовать основные компоненты Django без проблем. Когда разработчики добавляют новую функциональность в Django они используют это правило при рассмотрении вопроса о том, должна ли эта функциональность быть размещена в django.contrib или где-то в другом месте.

Библиотека django.contrib содержит следующие пакеты:

admin: Автоматический сайт администратора. Обратитесь к главам « Интерфейс администратора Django » и глава « Интернационализация ».

auth: Среда аутентификации и авторизации. Обратитесь к главе « Сессии, пользователи и регистрация ».

comments: Среда управления комментариями. Обратитесь к разделу « Среда управления комментариями » данной главы.

contenttypes: Среда подключения к «типам» данных, в которой каждая установленная модель Django является отдельным типом данных. Эта среда используется другими модулями библиотеки и в основном ориентировано на очень продвинутых Django разработчиков. Эти разработчики должны получить больше информации по этому модулю, читая исходный код в django/contrib/contenttypes/.

csrf: Защита против Cross-Site Request Forgery (CSRF). Смотрите далее секцию « Защита от CSRF ».

flatpages: Среда для управления простым «плоским» HTML представлением в базе данных. Смотрите далее секцию « Статические страницы ».

humanize: Набор шаблонных фильтров, полезных для «очеловечивания» данных. Смотрите далее секцию « Очеловечивание данных ».

markup: Набор шаблонных фильтров, которые реализуют ряд стандартных языков разметки. Смотрите далее секцию « Фильтры разметки ».

redirects: Среда управления перенаправлениями. Смотрите далее секцию « Перенаправления ».

sessions: Среда управления сессиями. Смотрите главу « Сессии, пользователи и регистрация ».

sitemaps: Среда генерации XML файлов для карты сайта. Смотрите главу « Генерация данных, отличных от HTML ».

sites: Среда управления множеством сайтов на основе единой базы данных и Django. Обратитесь к следующей секции « Сайты ».

syndication: Среда управления средствами трансляции в форматах RSS и Atom. Обратитесь к секции « Средства трансляции ».

Остальная часть этой главы посвящена деталям каждого модуля пакета django.contrib, которые мы ещё не рассматривали в нашей книге.

Среда управления комментариями

Перевод статьи http://code.djangoproject.com/wiki/UsingFreeComment.

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

Настройка

Система комментариев является приложением Django. Следовательно, требуется прописать его в параметре INSTALLED_APPS файла конфигурации проекта:

INSTALLED_APPS = (
    [...]
    'django.contrib.comments',
)

Если вы используете свои представления и точно не используете базовые представления, вам потребуется добавить нижеприведённую строчку в начала соответствующего файла urls.py. Базовые представления сами подключают этот класс, при их использовании об этом можно не беспокоиться.

from django.contrib.comments.models import FreeComment

Добавьте следующий шаблон URL в глобальный файл привязки:

urlpatterns = patterns('',
        [...]
        (r'^comments/', include('django.contrib.comments.urls')),
}

Потребуется обновить вашу базу данных, чтобы там появилась необходимая таблица. Для этого следует остановить веб сервер и выполнить python manage.py syncdb.

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

{% load comments %}

Информация о количестве комментариев

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

django.views.generic.list_detail.object_list

В шаблоне object_list вы можете делать следующее для того, чтобы добавить информацию о количестве комментариев для каждой записи блога в списке. Нижеприведённый пример предполагает, что вы работаете над приложением blog с классом entry, у которого есть поля title и summary, а также метод get_absolute_url(), который возвращает абсолютный URL для страницы с записью блога.

Замечание

Следует отметить, что имя класса должно всегда быть указано в нижнем регистре. Например, если класс имеет имя Entry, все обращения к нему будут идти как к blog.entry.

<ul>
  {% for object in object_list %}
    {% get_free_comment_count for blog.entry object.id as comment_count %}
    <li>
      <h2><a href="{{ object.url }}">{{ object.title }}</a></h2>
      <p class="description">{{ object.summary}}</p>
      <p class="details">
        <a href="{{ object.get_absolute_url }}">
          {{ comment_count }} Comments
        </a>
      </p>
    </li>
  {% endfor %}
</ul>

django.views.generic.date_based.archive_index

Базовое представление archive_index работает почти так как описанное в предыдущей секции, только итерация происходит по объектам из коллекции latest, а не по object_list:

<ul>
  {% for object in latest %}
    {% get_free_comment_count for blog.entry object.id as comment_count %}
    [...]
  {% endfor %}
</ul>

В архивах, отсортированных по дате (таких как archive_year или archive_month), коллекция, по которой производится итерация называется «object_list». archive_index является только базовым представлением с latest.

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

Обычно добавление комментариев разрешено пользователям на страницах с детальной информацией об объекте. Можно разрешить добавление комментариев откуда угодно, но для простоты данный пример не будет так делать. При использовании любых базовых представлений для отображения детальной информации, таких как django.views.generic.list_detail.object_detail или django.views.generic.date_based.object_detail, у вас будет идентификатор объекта в object.id, таким образом получение информации о количестве комментариев для этого объект будет несложным:

{% get_free_comment_count for blog.entry object.id as comment_count %}

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

{% get_free_comment_list for blog.entry object.id as comment_list %}

Каждый объект object в comment_list имеет следующие поля:

comment.person_name: Имя комментатора.

comment.submit_date: Дата и время отправки комментария. Вы можете пропустить эту информацию через фильтр date для форматирования строки с датой. Это будет показано в примере далее.

comment.comment: Текст комментария. Не забывайте экранировать его с помощью фильтра escape для предотвращения инъекций вредоносного кода. Это будет показано в примере далее.

comment.is_public: Разрешение на публикацию комментария (TODO: надо понять как мы даём это разрешение).

comment.ip_address: IP адрес комментатора.

comment.approved: Разрешение на публикацию от представителя сайта (TODO: узнать как оно реализовано).

А также следующие встроенные методы:

comment.get_absolute_url: Метод возвращает абсолютный URL для текущего комментария. Например, если комментарий был написан для записи блога размещённой по ссылке /blog/some-slug/, то будет возвращён URL /blog/some-slug/#c4, где 4 — это идентификатор комментария.

comment.get_content_object: Метод возвращает объект для которого было создан текущий комментарий.

Система комментирования не позволяет вам добавлять другие типы данных, такие как адрес электронной почты комментатора. Это может быть исправлено в будущем.

Пример:

{% get_free_comment_count for blog.entry object.id as comment_count %}

<h2><a href="{{ object.url }}">{{ object.title }}</a></h2>

<em>{{ object.description }}</em>

<div class="article_menu">
  <b>Added on {{ object.add_date|date:"F j, Y" }}</b>
  <a href="{{ object.get_absolute_url }}#comments">{{ comment_count }}
    Comment{{ comment_count|pluralize }}</a>
</div>

{% get_free_comment_list for blog.entry object.id as comment_list %}

<h2 id="comments">Comments</h2>
{% for comment in comment_list %}
<div class="comment_{% cycle odd,even %}" id="c{{ comment.id }}">
  <span class="comnum">
    <a id="c{{ comment.id }}" href="#c{{ comment.id }}">
      #{{ forloop.counter }}
    </a>
  </span>
  <p>
    <b>{{ comment.person_name|escape }}</b> commented,
    on {{ comment.submit_date|date:"F j, Y" }}
    at {{ comment.submit_date|date:"P" }}:
  </p>
  {{ comment.comment|escape|urlizetrunc:40|linebreaks }}
</div>
{% endfor %}

<h2>Post a comment</h2>
{% free_comment_form for blog.entry object.id %}

Шаблоны

Django поставляется со встроенными стандартными шаблонами для системы комментирования.

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

Шаблон с формой отправки комментария

Этот шаблон (freeform.html) содержит код формы, с помощью которой пользователь отправляет комментарий. В вышеприведённом примере, он подключался так:

<h2>Post a comment</h2>
{% free_comment_form for blog.entry object.id %}

Пример шаблона:

{% if display_form %}
  <form action="/comments/postfree/" method="post">
    <p>Your name: <input type="text" id="id_person_name" name="person_name" /></p>
    <p>Comment:<br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
    <input type="hidden" name="options" value="{{ options }}" />
    <input type="hidden" name="target" value="{{ target }}" />
    <input type="hidden" name="gonzo" value="{{ hash }}" />
    <p><input type="submit" name="preview" value="Preview comment" /></p>
  </form>
{% endif %}

Если вы не желаете использовать страницу для предварительного просмотра комментария, просто замените name="preview" на name="post".

Шаблон для предварительного просмотра

Этот шаблон (free_preview.html)используется при просмотре пользователем своего комментария перед его отправкой.

Пример:

<h1>Preview your comment</h1>

<form action="/comments/postfree/" method="post">
  {% if comment_form.has_errors %}
    <p><strong style="color: red;">Please correct the following errors.</strong></p>
  {% else %}
    <div class="comment">
      {{ comment.comment|escape|urlizetrunc:"40"|linebreaks }}
      <p class="date small">Posted by <strong>{{ comment.person_name|escape }}</strong></p>
    </div>

    <p><input type="submit" name="post" value="Post public comment" /></p>

    <h1>Or edit it again</h1>
  {% endif %}

  {% if comment_form.person_name.errors %}
    {{ comment_form.person_name.html_error_list }}
  {% endif %}

  <p><label for="id_person_name">Your name:</label> {{ comment_form.person_name }}</p>

  {% if comment_form.comment.errors %}
    {{ comment_form.comment.html_error_list }}
  {% endif %}

  <p>
    <label for="id_comment">Comment:</label>
    <br />
    {{ comment_form.comment }}
  </p>

  <input type="hidden" name="options" value="{{ options }}" />
  <input type="hidden" name="target" value="{{ target }}" />
  <input type="hidden" name="gonzo" value="{{ hash }}" />

  <p>
    <input type="submit" name="preview" value="Preview revised comment" />
  </p>
</form>

Шаблон для отправленного комментария

Этот шаблон (posted.html) отображается после того как пользователь отправит комментарий. Вы можете получить доступ к прокомментированному объекту через контекстную переменную object.

Пример:

<h1>Comment posted successfully</h1>

<p>Thanks for contributing.</p>

{% if object %}
        <ul>
                <li><a href="{{ object.get_absolute_url }}">View your comment</a></li>
        </ul>
{% endif %}

Wrapping post_free_comment to change the redirect url

В настоящее время django.contrib.comments.views.comments в методе post_free_comment() имеет жёстко определённый адрес перенаправления на posted.html. Если вы желаете, чтобы отправка комментария возвращала пользователя на комментируемую страницу, вы можете переопределить метод post_free_comment() и добавить перенаправление в другое место после отправки комментария. URL может передаваться в виде скрытого параметра в шаблоне freeform.html. Для этого потребуется создать обработчик в views.py:

from django.contrib.comments.views.comments import post_free_comment
from django.http import HttpResponseRedirect

def my_post_free_comment(request):
    if request.has_key('url') and not request.has_key('preview'):
        response = post_free_comment(request)

        # Check there's a url to redirect to, and that post_free_comment worked
        if len(request['url'].strip()) > 0 and
               isinstance(response, HttpResponseRedirect):
            return HttpResponseRedirect(request['url'])

            # Fall back on the default post_free_comment response
            return response

        return post_free_comment(request)

После этого потребуется перенаправить запросу на это представление вместо оригинального. Для этого, перед строкой:

(r'^comments/', include('django.contrib.comments.urls.comments')),

надо добавить строку:

(r'^comments/postfree/', 'views.my_post_free_comment'),

Теперь надо добавить в форму скрытое поле с URL. Но как получить текущий URL в шаблонной переменной? Для этого потребуется активировать шаблонный контекстный процессор request добавив следующий код в файл settings.py:

from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS
TEMPLATE_CONTEXT_PROCESSORS += (
     'django.core.context_processors.request',
)

После активации контекстного процессора шаблонам будет доступна переменная request. Используя эту переменную вы можете выполнить request.get_full_path для получения текущего URL. Внесём эти изменения в freeform.html:

{% load i18n %}
{% if display_form %}
  <form action="/comments/postfree/" method="post">
    <p>
      <label for="id_person_name">{% trans "Your name:" %}</label>
      <input type="text" id="id_person_name" name="person_name" />
    </p>
    <p>
      <label for="id_comment">{% trans "Comment:" %}</label><br />
      <textarea name="comment" id="id_comment" rows="10" cols="60"></textarea>
    </p>
    <p>
      <input type="hidden" name="options" value="{{ options }}" />
      <input type="hidden" name="target" value="{{ target }}" />
      <input type="hidden" name="gonzo" value="{{ hash }}" />
      <input type="hidden" name="url" value="{{ request.get_full_path }}" />
      <input type="submit" name="preview" value="{% trans "Preview comment" %}" />
    </p>
  </form>
{% endif %}

В free_preview.html добавим:

<input type="hidden" name="url" value="{{ request.url }}" />

перед

<p><input type="submit" name="preview" value="Preview revised comment" /></p>

Этого должно быть достаточно!

Другие примеры

Список свежих комментариев. Нижеприведённый код отображает в виде списка последние комментарии для всего вашего сайта, не обращая внимания на приложения:

<h1>Recent comments</h1>

<p>
  {% if has_previous %}
    <a href="?page={{ previous }}">Previous</a> |
  {% endif %}

  Page {{ page }} of {{ pages }}

  {% if has_next %}
    | <a href="?page={{ next }}">Next</a>
  {% endif %}
</p>

{% for comment in object_list %}
  <div class="comment" id="c{{ comment.id }}">
    <h3>
      <a href="{{ comment.get_absolute_url }}">
        {{ comment.person_name|escape }}
	<span class="small quiet">
	  {{ comment.submit_date|date:"F j, Y" }} at {{ comment.submit_date|date:"P" }}
	</span>
      </a>
    </h3>
    {{ comment.comment|escape|urlizetrunc:"40"|linebreaks }}
  </div>
{% endfor %}

<p>
  {% if has_previous %}
    <a href="?page={{ previous }}">Previous</a> |
  {% endif %}

  Page {{ page }} of {{ pages }}

  {% if has_next %}
    | <a href="?page={{ next }}">Next</a>
  {% endif %}
</p>

Сайты

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

Сценарий 1: Использование данных на множестве сайтов

Как мы рассказывали в главе « Введение в Django », сайты LJWorld.com и Lawrence.com управляются одной новостной компанией: газетой Lawrence Journal-World в городе Lawrence, штат Канзас. Сайт LJWorld.com сфокусирован на новостях, в то время как сайта Lawrence.com сосредоточен на местных развлечениях. Но иногда корреспонденты желают разместить свою статью на обоих сайтах.

Мозгодробящим способом решения этой задачи будет использование раздельных баз данных для каждого сайта и наличие требования к корреспондентам о необходимости размещения их статьи дважды: на одном и на другом сайтах. Но этот метод неэффективен и приводит к дублированию информации в базе данных.

Есть ли решение получше? Оба сайта используют единую базу данных для статей и статья ассоциирована с одним или несколькими сайтами с помощью отношения «многие-ко-многим». Среда управления сайтами предоставляет таблицу базы данных через которую статьи ассоциируются с сайтами.

Сценарий 2: Хранение информации о сайте в одном месте

Сайты LJWorld.com и Lawrence.com имеют функциональность для уведомления пользователей по электронной почте о появившихся новостях. Это очень просто: пользователь регистрируется на форме сайта и немедленно получает по электронной почте сообщение: «Спасибо за вашу подписку.»

Будет неэффективно и избыточно реализовывать код регистрации дважды, таким образом сайты должны использовать единый код. Но сообщение должно различаться для каждого сайта. Используя объекты Site, мы можем абстрагировать ваше уведомление так, что оно будет использовать атрибуты объекта name (т.е., LJWorld.com) и domain (т.е., www.ljworld.com).

Среда управления сайтами предоставляет место для хранения атрибутов name и domain для каждого сайта вашего проекта Django и это означает, что вы можете повторно использовать эти значения в общем виде.

Как это использовать

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

Модель Site, которая находится в модуле django.contrib.sites, имеет атрибуты name и domain.

Параметр SITE_ID определяет идентификатор в базе данных для объекта Site, который ассоциирован с определённым файлом настроек.

То, как будут использоваться эти две концепции, полностью зависит от вас, но Django автоматически использует их в ряде случаев.

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

Добавьте значение django.contrib.sites в параметр INSTALLED_APPS.

Выполните команду manage.py syncdb для установки таблицы django_site в вашу базу данных.

Добавьте один или более объектов Site, через интерфейс администратора Django или через Python API. Создайте объект Site для каждого сайта/домена, который управляется с помощью проекта Django.

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

Возможности

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

Повторное использование данных на сайтах

Для повторного использования данных на множестве сайтов, как было описано в « Сценарий 1: Использование данных на множестве сайтов », следует создать поле ManyToManyField в модели Site, например:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

Эта инфраструктура необходима вам для ассоциирования статей со множеством сайтов в вашей базе данных. Используя это, вы можете использовать один и тот же код функции представления для множества сайтов. Продолжая работу с примером модели Article, так может выглядеть код представления article_detail:

from django.conf import settings

def article_detail(request, article_id):
    try:
        a = Article.objects.get(id=article_id, sites__id=settings.SITE_ID)
    except Article.DoesNotExist:
        raise Http404
    # ...

Эта функция представления может быть использована повторно, потому что она динамически проверяет сайт для статьи, учитывая значения параметра SITE_ID.

Например, предположим что SITE_ID для LJWorld.com имеет значение 1, а для Lawrence.com — значение 2. Если данное представление будет вызвано с использованием конфигурационного файла от LJWorld.com, то выборка статей будет ограничена теми записями, у которых в списке сайтов есть LJWorld.com.

Ассоциирование данных с единственным сайтом

Аналогично, вы можете ассоциировать модель к Site как многие-к-одному с помощью ForeignKey.

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

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site)

Такой подход имеет те же преимущества, аналогичные описанным в секции « Фильтры разметки ».

Изменение логики представления в зависимости от текущего сайта

На низком уровне, вы можете использовать среду управления сайтами в ваших представлениях для выполнения определённых действия, основываясь на текущем сайте, например:

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Делаем что-то.
    else:
        # Делаем что-то другое.

Конечно, руки бы оторвать за такой код с жёстким определением идентификаторов. Более явным способом выполнения тех же действий является проверка домена текущего сайта:

from django.conf import settings
from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get(id=settings.SITE_ID)
    if current_site.domain == 'foo.com':
        # Делаем что-то.
    else:
        # Делаем что-то другое.

Идея получения объекта Site для определения значения параметра конфигурации SITE_ID довольно стандартна, поэтому менеджер модели Site (Site.objects) имеет метод get_current(). Данный пример эквивалентен предыдущему:

from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Делаем что-то.
    else:
        # Делаем что-то другое.

Замечание

В последнем примере нет необходимости импортирования django.conf.settings.

Отображение текущего домена

При использовании методики «Не повторяйся» название вашего сайта и его доменное имя, как было показано в секции « Сценарий 2: Хранение информации о сайте в одном месте », являются всего лишь ссылками на атрибуты name и domain объекта Site. Например:

from django.contrib.sites.models import Site
from django.core.mail import send_mail

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    current_site = Site.objects.get_current()
    send_mail('Thanks for subscribing to %s alerts' % current_site.name,
        'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
        'editor@%s' % current_site.domain,
        [user_email])
    # ...

Продолжая наш пример с LJWorld.com и Lawrence.com, на последнем сообщение имеет заголовок «Thanks for subscribing to lawrence.com alerts.» На LJWorld.com, соответственно, такой — «Thanks for subscribing to LJWorld.com alerts.» Аналогичное поведение применяется и для тела сообщения.

Более гибким (но и более сложным) способом реализации такого поведения будет использование шаблонной системы Django. Предположим, что сайты LJWorld.com и Lawrence.com хранят свои шаблоны в разных каталогах (TEMPLATE_DIRS), тогда переложим ответственность на шаблонную систему:

from django.core.mail import send_mail
from django.template import loader, Context

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    send_mail(subject, message, 'do-not-reply@example.com', [user_email])
    # ...

В данном случае вам потребуется создать шаблоны subject.txt и message.txt в обоих каталогах с шаблонами. Как упоминалось ранее, это даст вам большую гибкость, но и работы прибавится.

Хорошей идеей будет максимально возможное развитие объекта Site, чтобы удалить ненужную сложность и избыточность.

Текущий домен в URL

Метод get_absolute_url() удобен для получения URL без доменного имени, но в некоторых случаях может потребоваться отобразить полный URL. Для этого можно использовать среду управления сайтами. Вот простой пример:

>>> from django.contrib.sites.models import Site
>>> obj = MyModel.objects.get(id=3)
>>> obj.get_absolute_url()
'/mymodel/objects/3/'
>>> Site.objects.get_current().domain
'example.com'
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'

CurrentSiteManager

Если объект Site играет ключевую роль в вашем приложении, рассмотрите использование CurrentSiteManager в ваших моделях. Это менеджер модели, описанный в приложении « Справочник определений модели », который автоматически обрабатывает свои запросы так, чтобы они включали только объекты ассоциированные с текущим сайтом.

Для использование CurrentSiteManager его следует явно подключить в модель. Например:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

При использовании этой модели, вызов Photo.objects.all() возвратит все объекты Photo из базы данных, а вызов Photo.on_site.all() — только те объекты Photo, которые относятся к текущему сайту (это определяется с помощью параметра SITE_ID).

Другими словами, эти два оператора эквивалентны:

Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()

Как же CurrentSiteManager определяет какое поле объекта Photo относится к Site? По умолчанию он смотрит на поле с именем site. Если ваша модель имеет поля ForeignKey или ManyToManyField с именами отличными от site, вам потребуется явно передать имя в виде параметра в CurrentSiteManager. Нижеприведённая модель, у которой такое поле называется publish_on, демонстрирует такой случай:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

Если вы попытаетесь использовать CurrentSiteManager, передав имя несуществующего поля, Django вызовет исключение ValueError.

Замечание

Вероятно вы пожелаете иметь обычный (не привязанный к сайтам) Manager для ваших моделей, даже если вы используете CurrentSiteManager. Как описано в приложении « Справочник определений модели », если вы определите менеджер вручную, тогда Django не будет создавать менеджер автоматически objects = models.Manager().

Также определённые части Django, а именно интерфейс администратора и базовые представления, используют тот менеджер, который был определён первым в модели. Таким образом, если потребуется, чтобы ваш сайт администратора имел доступ ко всем объектам (а не только к тем, которые относятся к сайту), следует поместить objects = models.Manager() в модели до определения CurrentSiteManager.

Как Django использует среду управления сайтами

Несмотря на то, что использование среды управления сайтами необязательно, это настоятельно рекомендуется делать, потому что Django пользуется некоторыми преимуществами среды. Даже если Django используется для управления единственным сайтом, вы должны потратить несколько секунд на создание объекта Site с правильными значениями атрибутов name и domain, а затем указать на этот объект с помощью параметра SITE_ID.

Опишем, как Django использует среду управления сайтами:

В среде управления перенаправлением (смотрите далее секцию « Перенаправления ») каждый объект перенаправления ассоциирован с определённым сайтом. Когда Django производит поиск для перенаправления пользователя, оно принимает во внимание текущее значение SITE_ID.

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

В системе управления статическими страницами (смотрите далее секцию « Статические страницы ») каждая такая страница ассоциирована с определённым сайтом. При создании статической страницы вы указываете для неё атрибут site, а система управления проверяет текущий SITE_ID при её отображении.

В системе управления трансляциями (обратитесь к разделу « Средства трансляции ») шаблоны для title и description автоматически получают доступ к переменной {{ site }}, которая является объектом Site. Также атрибут domain объекта Site используется обработчиком URL в случае, если вы не указали полностью определённым домен.

В системе аутентификации и авторизации (обратитесь к разделу « Аутентификация пользователей ») представление django.contrib.auth.views.login передаёт текущее имя сайта в шаблон через {{ site_name }}.

Статические страницы

Часто, при использовании веб приложений с базой данных, появляется необходимость вывода нескольких статических страниц, таких как страница с информацией о сайте или страница политики безопасности. В таком случае можно использовать обычный веб сервер, например, Apache, для вывода этих страниц в виде обычных HTML файлов. Но такой подход добавит дополнительный уровень сложности в ваше приложение, потому что потребуется заботиться о конфигурации Apache, предоставлении доступа на редактирование для вашей команды и вы не сможете воспользоваться преимуществами использования шаблонной системы Django для управления видом страниц.

Решением этой проблемы является использование специального приложения, которое находится в пакете django.contrib.flatpages. Это приложение позволяет вам управлять статическими страницами через интерфейс администратора Django и указывать шаблоны для таких страниц с помощью шаблонной системы Django. Это приложение использует модели Django для своей работы, значит оно хранит страницы в базе данных, как и остальные ваши данные. Вы можете получить доступ к статическим страницам с помощью стандартного API для взаимодействия с базой данных.

Статические страницы выбираются по URL и сайту. При создании статической страницы вы указываете с каким URL она ассоциирована и с каким сайтом. (Более подробно о системе управления сайтами рассказано в секции « Сайты ».)

Использование

Для установки приложения выполните следующие шаги:

Добавьте django.contrib.flatpages в параметр конфигурации INSTALLED_APPS. Пакет django.contrib.flatpages зависит от пакета django.contrib.sites, удостоверьтесь, что оба этих пакета указаны в INSTALLED_APPS.

Добавьте django.contrib.flatpages.middleware.FlatpageFallbackMiddleware в параметр конфигурации MIDDLEWARE_CLASSES.

Выполните команду manage.py syncdb для установки двух необходимых таблиц в вашу базу данных.

Приложение создаёт две таблицы в базе данных: django_flatpage и django_flatpage_sites. Таблица django_flatpage содержит соответствия URL и страницы. Таблица django_flatpage_sites является таблицей типа «многие-ко-многим», которая связывает страницу с одним или более сайтами.

Приложение поставляется с единственной моделью FlatPage, определённой в django/contrib/flatpages/models.py:

from django.db import models
from django.contrib.sites.models import Site

class FlatPage(models.Model):
    url = models.CharField(max_length=100)
    title = models.CharField(max_length=200)
    content = models.TextField()
    enable_comments = models.BooleanField()
    template_name = models.CharField(max_length=70, blank=True)
    registration_required = models.BooleanField()
    sites = models.ManyToManyField(Site)

Рассмотрим поля модели:

url: URL, с которым ассоциирована страница, исключая доменное имя и включая начальный слэш (т.е., /about/contact/).

title: Заголовок страницы. Приложение не делает ничего особенного с заголовком. Это вам решать как отобразить его в шаблоне.

content: Содержимое страницы (т.е., HTML код страницы). Приложение не делает ничего особенного с содержимым. Это вам решать как отобразить его в шаблоне.

enable_comments: Разрешение создания комментариев для страницы. Приложение никак не реагирует на значение этого поля. Вы можете проверять значение этого поля в вашем шаблоне и отображать форму для комментария по необходимости.

template_name: Имя шаблона, который используется для отображения страницы. Необязательное поле. Если такого поля нет, то приложение будет использовать шаблон flatpages/default.html.

registration_required: Указание на необходимость регистрации пользователя для посещения страницы. Интегрируется с системой аутентификации пользователя, которая была описана в разделе « Аутентификация пользователей ».

sites: Сайт, для которого предназначена страницы. Интегрируется с системой управления сайтами, которая рассмотрена в разделе « Сайты ».

Вы можете создавать страницы как через интерфейс администратора Django, так и через API работы с базой данных. Более подробно это описано в разделе « Добавление, изменение и удаление ».

После установки приложения вся работа будет выполняться с помощью класса FlatpageFallbackMiddleware. При каждом вызове ошибки 404, в качестве последнего действия, этот класс будет проверять наличие в базе данных статических страниц для требуемого URL. Главное, проверка производится с учётом сайта.

Если будет найдено совпадение, то будет произведена загрузка шаблона страницы (в крайнем случае будет загружен flatpages/default.html). В шаблон будет передана одна контекстная переменная, flatpage, которая будет ссылаться на объект. При обработке шаблона будет использоваться RequestContext.

Если не будет найдено совпадение, запрос будет обработан как обычно.

Замечание

Данный обработчик активируется только при ошибке 404 (страница не найдена), а не в случае ошибки 500 (ошибка сервера) или каких-то других. Следует отметить, что порядок значений MIDDLEWARE_CLASSES важен. Рекомендуется размещать FlatpageFallbackMiddleware в конце списка.

Добавление, изменение и удаление

Вы можете добавлять, изменять и удалять страницы двумя способами:

Через интерфейс администратора

Если вы активировали автоматический интерфейс администратора Django, вы должны увидеть на его главной странице раздел «Flatpages». Редактирование статических страниц производится аналогично редактированию любого объекта системы.

Через Python API

Как было описано ранее, статические страницы представлены в виде стандартной Django модели, которая расположена в django/contrib/flatpages/models.py. Следовательно, вы можете получить доступ к объектам через API работы с базой данных, например:

>>> from django.contrib.flatpages.models import FlatPage
>>> from django.contrib.sites.models import Site
>>> fp = FlatPage(
...     url='/about/',
...     title='About',
...     content='<p>About this site...</p>',
...     enable_comments=False,
...     template_name='',
...     registration_required=False,
... )
>>> fp.save()
>>> fp.sites.add(Site.objects.get(id=1))
>>> FlatPage.objects.get(url='/about/')
<FlatPage: /about/ -- About>

Использование шаблонов

По умолчанию статические страницы генерируются с использованием шаблона flatpages/default.html, но вы можете указать свой шаблон с помощью поля template_name объекта FlatPage.

Ответственность за создание шаблона flatpages/default.html лежит на вас. Необходимо создать каталог flatpages в вашем каталоге шаблонов.

Шаблоны статических страниц получают одну контекстную переменную, flatpage, которая содержит объект.

Пример шаблона flatpages/default.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
    "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>{{ flatpage.title }}</title>
</head>
<body>
{{ flatpage.content }}
</body>
</html>

Перенаправления

Средства Django для перенаправления позволяются вам легко управлять этим процессом, храня всю информацию в базе данных и рассматривая перенаправления как и любую другую модель. Например, вы можете использовать это средства для указания Django, что надо «перенаправлять любой запрос к /music/ на /sections/arts/music/.» Это становится очень полезным когда возникает необходимость поменять местами части вашего сайта. Веб разработчики должны делать всё для того, чтобы все ссылки работали.

Использование

Для установки приложения выполните следующие шаги:

Добавьте django.contrib.redirects в параметр конфигурации INSTALLED_APPS.

Добавьте django.contrib.redirects.middleware.RedirectFallbackMiddleware в параметр конфигурации MIDDLEWARE_CLASSES.

Выполните команду manage.py syncdb для установки единственной необходимой таблицы в вашу базу данных.

Команда manage.py syncdb создаёт таблицу django_redirect в вашей базе данных. Это простая таблица с полями site_id, old_path и new_path.

Вы можете создавать перенаправления как через интерфейс администратора Django, так и через API для работы с базой данных. Подробности описаны в разделе « Добавление, изменение и удаление ».

После установки средств управления перенаправлениями всю работу будет выполнять класс RedirectFallbackMiddleware. При каждом вызове ошибки 404, в качестве последнего действия, этот класс будет проверять наличие в базе данных соответствующего перенаправления для запрашиваемого URL. Следует отметить, что производится проверка для перенаправления по указанному в old_path пути для соответствующего сайта, SITE_ID. (Обратитесь к разделу « Сайты » для подробностей по параметру SITE_ID и среде управления сайтами.) Затем выполняются следующие действия:

Если найдено совпадение и поле new_path содержит что-нибудь, то производится перенаправление по пути из поля new_path.

Если найдено совпадение, а поле new_path не содержит ничего, производится отправка заголовка HTTP 410 (Gone).

Если совпадение не найдено, запрос продолжает обрабатываться как обычно.

Класс RedirectFallbackMiddleware обрабатывает только ошибки 404, остальное игнорируется.

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

Добавление, изменение и удаление

Вы можете добавлять, изменять и удалять перенаправления двумя способами:

Через интерфейс администратора

Если вы активировали автоматический интерфейс администратора Django, вы должны увидеть на его главной странице раздел «Redirects». Редактирование перенаправлений производится аналогично редактированию любого объекта системы.

Через Python API

Как было описано ранее, статические страницы представлены в виде стандартной Django модели, которая расположена в django/contrib/redirects/models.py. Следовательно, вы можете получить доступ к объектам через API работы с базой данных, например:

>>> from django.contrib.redirects.models import Redirect
>>> from django.contrib.sites.models import Site
>>> red = Redirect(
...     site=Site.objects.get(id=1),
...     old_path='/music/',
...     new_path='/sections/arts/music/',
... )
>>> red.save()
>>> Redirect.objects.get(old_path='/music/')
<Redirect: /music/ ---> /sections/arts/music/>

Защита от CSRF

Пакет django.contrib.csrf защищает от атак типа «подделка HTTP запросов» (Cross-Site Request Forgery), далее CSRF.

CSRF, также известная как «session riding» FIXME, является дырой в системе безопасности веб сайта. Она случается когда вредоносный сайт заставляет пользователя, незаметно для него, загрузить URL с сайта, на котором пользователь уже авторизован. Это непросто понять сначала, поэтому мы разберём пару примеров.

Простой пример

Допустим вы зашли в свою почту на сайте example.com. Интерфейс почтовой системы имеет кнопку Log out, которая вызывает URL example.com/logout. Таким образом для того, чтобы выйти из своей почты достаточно посетить страницу example.com/logout.

Вредоносный сайт может заставить вас посетить URL example.com/logout с помощью скрытого тега <iframe> на своей странице. Таким образом, если вы были авторизованы на почтовом интерфейсе example.com и посетили вредоносную страницу, которая выполнила через <iframe> обращение к example.com/logout, ваша авторизация на почтовом интерфейсе будет прекращена.

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

Более сложный пример

В предыдущем примере сайт example.com был частично скомпрометирован, потому что он позволял изменять состояние (т.е. отменять авторизацию пользователя) через GET запрос. Более практично требовать POST запрос для изменения состояния на сайте. Но даже это не спасёт полностью от CSRF.

Предположим, что на сайте example.com провели обновление функциональность и теперь для отмены авторизации пользователя требуется нажать кнопку на форме, которая отправит POST запрос к example.com/logout. Более того, форма содержит скрытое поле:

<input type="hidden" name="confirm" value="true" />

Это даёт гарантию, что обычный POST запрос к example.com/logout не отменит авторизацию пользователя, для этого ещё должна передаваться переменная confirm со значением true.

Несмотря на дополнительную безопасность этот механизм всё ещё может быть скомпрометирован CSRF — вредоносной странице просто потребуется сделать чуть больше работы. Атакующие могут создать целую форму для вашего сайта, спрятав её в невидимом <iframe>, а затем использовать JavaScript для автоматической отправки этой формы.

Предотвращение

Как может ваш сайт защитить себя от этого типа атак? Первым шагом будет проверка того, что все GET запросы не содержат побочные эффекты. Затем, если вредоносный сайт включает одну из ваших страниц в <iframe>, это не будет приводить к негативному эффекту.

Остались POST запросы. Вторым шагом будет добавление в каждую форму скрытого поля, значение которого генерируется из идентификатора пользователя. Затем, при обработке формы на стороне сервера, следует проверять это поле и выдавать ошибку, если проверка не удастся.

Именно это и делает Django для предотвращения CSRF. А подробности далее.

Использование класса CSRF

Пакет django.contrib.csrf содержит только один модуль — middleware.py. Модуль содержит класс Django, CsrfMiddleware, которые реализовывает защиту от CSRF.

Для активации защиты от CSRF следует добавить django.contrib.csrf.middleware.CsrfMiddleware в параметр MIDDLEWARE_CLASSES вашего файла конфигурации. Этот модуль нужен для обработки отклика после SessionMiddleware, таким образом CsrfMiddleware должен быть помещён до SessionMiddleware в списке (потому что классы при обработки отклика применяются от конца списка к началу). Также следует обрабатывать отклик до его компрессии или других изменений. Таким образом, CsrfMiddleware должно быть указано после GZipMiddleware. Обратитесь к секции « Порядок в MIDDLEWARE_CLASSES ».

Если вам интересно, то вот как класс CsrfMiddleware работает. Он делает следующие две вещи:

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

Для всех POST запросов использующих механизм сессии класс производит проверку наличия и правильности csrfmiddlewaretoken. Если такого поля у передаваемой формы нет, то пользователь получает ошибку 403, на которой написано: «Cross Site Request Forgery detected. Request aborted.»

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

Данный класс умышленно нацелен только на POST запросы (и соответствующие формы с методами POST). Как мы объясняли, GET запросы не могут иметь побочных эффектов, проверить это — ваша задача.

POST запросы, не использующие механизм сессий, не защищаются. Их и не надо защищать, потому что вредоносные сайты могут выполнить такой запрос в любом случае.

Чтобы исключить внесение изменений в не-HTML запросы класс проверяет заголовок Content-Type отклика перед его модификацией. Только страницы с типами text/html или application/xml+xhtml подвергаются обработке.

Ограничения класса

Класс CsrfMiddleware требует наличия механизма управления сессиями для своей работы. (Обратитесь к разделу « Сессии, пользователи и регистрация » для подробностей.) Если вы используете свой собственный механизм управления сессиями или аутентификацией, которые изменяют cookie сессий, данный класс вам не поможет.

Если ваше приложение создаёт HTML страницы и формы необычным способом (т.е., если оно отправляет фрагменты HTML в JavaScript оператор document.write), вы можете пропустить фильтр, который добавляет скрытое поле к форме. В этом случае отправка формы всегда будет неудачна. (Это будет происходить, потому что класс CsrfMiddleware использует регулярное выражения для добавления поля csrfmiddlewaretoken в ваш HTML перед отправкой страницы клиенту, а регулярное выражение иногда не может обработать уродский HTML.) Если вы предполагаете возможность такой ситуации, просто просмотрите код в вашем браузере и проверьте наличие поля csrfmiddlewaretoken у формы.

Для получения информации по CSRF обратитесь к http://en.wikipedia.org/wiki/CSRF.

Очеловечивание данных

Это приложение содержит набор шаблонных фильтров Django, которые полезны для придания «человеческого вида» данным. Для активации этих фильтров добавьте django.contrib.humanize в конфигурационный параметр INSTALLED_APPS. Затем следует использовать {% load humanize %} в шаблоне для получения доступа к фильтрам.

apnumber

Для чисел от 1 до 9 этот фильтр возвращает письменную форму.

Примеры:

1 преобразуется в «one».

2 преобразуется в «two»

10 преобразуется в «10».

Вы можете передавать как целое, так и строковое представление целого.

intcomma

Этот фильтр преобразовывает целое в строку с разделением разрядов запятой.

Примеры:

4500 станет «4,500».

45000 станет «45,000».

450000 станет «450,000».

4500000 станет «4,500,000».

Вы можете передавать как целое, так и строковое представление целого.

intword

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

Примеры:

1000000 станет «1.0 million».

1200000 станет «1.2 million».

1200000000 станет «1.2 billion».

Поддерживаются значение до одного квадриллиона (1,000,000,000,000,000)/

Вы можете передавать как целое, так и строковое представление целого.

ordinal

Этот фильтр преобразовывает целые в их порядковое представление.

Примеры:

1 становится «1st».

2 становится «2nd».

3 становится «3rd»

Вы можете передавать как целое, так и строковое представление целого.

Фильтры разметки

Следующая коллекция шаблонных фильтров обрабатывает общие языки разметки:

textile: Обрабатывает Textile (http://en.wikipedia.org/wiki/Textile_(markup_language)).

markdown: Обрабатывает Markdown (http://en.wikipedia.org/wiki/Markdown).

restructuredtext: Обрабатывает ReStructured Text (http://en.wikipedia.org/wiki/ReStructuredText).

В каждом случае фильтр ожидает отформатированную разметку в виде строки и возвращает строковое представление размеченного текста. Например, фильтр textile преобразовывает текст в формате Textile в HTML:

{% load markup %}
{{ object.content|textile }}

Для активации этих фильтров следует добавить django.contrib.markup в конфигурационный параметр INSTALLED_APPS. Затем следует использовать {% load markup %} в шаблоне для получения доступа к фильтрам. Более подробную документацию можно получить, читая исходный код файла django/contrib/markup/templatetags/markup.py.

Глава 15. Компоненты

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

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

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

Каждый компонент этой системы отвечает за выполнение определённой задачи. Если вы читаете эту книгу с самого начала, вы уже встречались с такими компонентами:

Все инструменты для работы с сессиями и пользователями, описанные в главе « Сессии, пользователи и регистрация », появились благодаря этой системе. Точнее, компоненты были подключены к вашей функции представления с помощью модулей request.session и request.user.

Кэш для сайта, описанный в главе « Кэширование », на самом деле является таким компонентом, который пропускает через себя вызов вашей функции представления в случае, когда это представление уже было помещено в кэш.

Сторонние приложения, такие как flatpages, redirects и csrf, описанные в главе « Средства от других разработчиков », делают всю свою волшебную работу через систему компонентов.

Эта глава подробно рассматривает что такое компоненты, как они работают и объясняет как вы можете самостоятельно создавать такие компоненты.

Что такое компоненты?

Компонент является обычным классом языка Python, который соответствует определённому API. Перед изучением формальных аспектов API, давайте рассмотрим очень простой пример.

Сайты с высокой нагрузкой часто нуждаются в установке Django за балансировщиком нагрузки (обратитесь к главе « Развёртывание Django »). Это может вызвать некоторые трудности, одной из которых является то, что каждый IP адрес от которого идёт запрос (request.META['REMOTE_IP']) будет теперь указывать на балансировщик, а не актуальный IP адрес. Балансировщики нагрузки решают эту проблему установкой специального заголовка, X-Forwarded-For, назначая ему IP адрес машины, которая сделала запрос.

Ниже приведён код небольшого компонента, который позволяет сайтам, работающим за балансировщиком, получать правильный IP адрес из request.META['REMOTE_IP']:

class SetRemoteAddrFromForwardedFor(object):
    def process_request(self, request):
        try:
            real_ip = request.META['HTTP_X_FORWARDED_FOR']
        except KeyError:
            pass
        else:
            # HTTP_X_FORWARDED_FOR может быть списком IP адресов,
	    # разделённых запятой. Берём только первый.
            real_ip = real_ip.split(",")[0]
            request.META['REMOTE_ADDR'] = real_ip

Если этот компонент установлен (обратитесь к следующей секции), значение X-Forwarded-For каждого запроса будет автоматически помещено в request.META['REMOTE_ADDR']. Это означает, что вашему приложению больше не требуется определять работает ли оно за балансировщиком или нет. Оно может просто использовать request.META['REMOTE_ADDR'] и всё будет работать как надо.

Действительно, этот функционал нужен многим и должен быть встроен в Django. Так и есть, он находится в django.middleware.http, и вы можете узнать о нём больше в следующем разделе.

Установка

Если вы читали книгу с самого начала вы уже встречались с рядом примеров установки компонентов. Многие из примеров в предыдущих главах требовали наличия определённых компонентов. Для полноты изложения, опишем здесь как производится установка компонентов.

Для активации компонента его имя следует добавить в кортеж MIDDLEWARE_CLASSES в вашем файле конфигурации. В параметре MIDDLEWARE_CLASSES каждый компонент представлен строкой: полный путь к имени класса. Например, ниже представлен стандартный MIDDLEWARE_CLASSES созданный с помощью django-admin.py startproject:

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.doc.XViewMiddleware'
)

Django вообще не требует установки компонентов, т.е. MIDDLEWARE_CLASSES может быть вообще пустым, если вы этого пожелаете, но мы рекомендуем активировать компонент CommonMiddleware, мы позже объясним зачем.

Порядок указания модулей имеет значение. На этапах обработки запроса и представления Django применяет компоненты в порядке, указанном в параметре MIDDLEWARE_CLASSES, а на этапах отклика и исключения Django применяет компоненты в обратном порядке. Таким образом, Django рассматривает MIDDLEWARE_CLASSES как вид обработчика для функции представления. Обратитесь к разделу « Как Django обрабатывает запрос » для получения подробной информации.

Методы

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

Конструктор

Используйте __init__(self) для регистрации класса компонента в системе.

В целях повышения производительности класс каждого активированного компонента инициализируется только один раз для серверного процесса. Это означает, что __init__(self) вызывается только один раз, при запуске сервера.

Стандартной причиной для реализации метода __init__(self) является проверка действительно ли нужен данный компонент. Если __init__(self) вызывает исключение django.core.exceptions.MiddlewareNotUsed, тогда Django удаляет компонент из стека компонентов. Вы можете использовать эту особенность для проверки некоторой части программного обеспечения, которое необходимо для работы компонента, для проверки работает ли сервер в режиме отладки или для других подобных ситуаций.

Если для компонента определён метод __init__(self), то данный метод не должен принимать никаких дополнительных аргументов, кроме self.

Препроцессор запроса

Этот метод вызывается при получении запроса — перед тем как Django приступает к обработке URL для определения функции представления, которую требуется запустить. Метод получает объект HttpRequest и может изменять его.

Метод process_request() должен возвращать или None или объект HttpResponse:

Если метод возвратил None, Django продолжает обработку этого запроса, используя для этого другие компоненты, а затем и соответствующее представление.

Если метод возвратил объект HttpResponse, Django не будет вызывать другие компоненты и соответствующее представление, а просто вернёт этот объект.

Препроцессор представления

Этот метод вызывается после препроцессора запроса и после того как Django определил какое именно представление следует использовать для обработки этого запроса, но прежде чем это представление будет использовано.

Аргументы, передаваемые в это представление, показаны в таблице « Аргументы передаваемые в process_view() ».

Таблица 15.1. Аргументы передаваемые в process_view()

Аргумент | Комментарий
request | Объект HttpRequest
view | Представление, которое используется Django для обработки данного запроса. Это функциональный объект, а не просто имя функции, представленное в виде строки.
args | Список неименованных аргументов, который будет передан в представление, без аргумента request (который всегда является первым аргументом представления).
kwargs | Словарь именованных аргументов, который будет передан в представление.

Подобно методу process_request(), данный метод должен возвращать либо None, либо объект HttpResponse.

Если возвращается None, Django продолжает обработку данного запроса, используя остальные компоненты, а затем и соответствующее представление.

Если возвращается объект HttpResponse, Django не будет вызывать другие компоненты и соответствующее представление. Django немедленно вернёт полученный объект.

Постпроцессор отклика

Этот метод вызывается после вызова функции представления и генерации отклика. В этом месте, постпроцессор может модифицировать содержимое отклика. Один очевидный случай применения — компрессия контента, т.е., применение утилиты gzip над отдаваемым HTML.

Назначение параметров должно быть понятно из их названия: request — объект запроса, response — объект отклика, возвращённый из представления.

В отличие от препроцессоров запроса и представления, которые могут возвращать None, постпроцессор process_response() должен возвращать объект HttpResponse. Этот отклик может быть оригинальным, т.е. тем, который был передан в метод (возможно, изменённый) или совершенно новым.

Постпроцессор исключения

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

Параметры метода: request — объект, с которым мы работаем, а exception — объект Exception, созданный функцией представления.

Метод process_exception() должен возвращать либо None, либо HttpResponse.

Если возвращается None, Django продолжает обработку данного исключения встроенным механизмом.

Если возвращается объект HttpResponse, Django будет использовать этот отклик вместо того, который отдал встроенный механизм обработки исключений.

Замечание

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

Вы также можете найти ряд примеров предоставленных сообществом Django на wiki: http://code.djangoproject.com/wiki/ContributedMiddleware.

Встроенные компоненты

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

Компонент поддержки аутентификации

Класс компонента django.contrib.auth.middleware.AuthenticationMiddleware.

Этот компонент включает механизм поддержки аутентификации. Он добавляет атрибут request.user, представляющий текущего авторизованного пользователя, в каждый входящий объект HttpRequest.

Данный компонент описан в главе « Сессии, пользователи и регистрация ».

Стандартный компонент

Класс компонента: django.middleware.common.CommonMiddleware.

Этот компонент добавляет несколько удобств для перфекционистов:

Запрещает доступ для пользовательских агентов, перечисленных в параметре DISALLOWED_USER_AGENTS: Если этот параметр определён в файле конфигурации, он должен быть представлен в виде списка объектов скомпилированных регулярных выражений, которые будут применяться к заголовкам пользовательских агентов для каждого входящего запроса. Ниже показан пример с частью конфигурационного файла:

import re

DISALLOWED_USER_AGENTS = (
    re.compile(r'^OmniExplorer_Bot'),
    re.compile(r'^Googlebot')
)

Следует отметить использование import re, оно необходимо для компилирования регулярных выражений (обратите внимание на re.compile()). Так как файл конфигурации является кодом на языке Python, можно спокойно использовать оператор include в нём.

Выполняет перезапись URL, основываясь на параметрах APPEND_SLASH и PREPEND_WWW: Если параметр APPEND_SLASH имеет значение True, то URL у которых отсутствует завершающий слэш (но нет точки в последнем компоненте пути) будут перенаправляться на тот же URL только с завершающим слэшом. Таким образом, foo.com/bar будет перенаправлен на foo.com/bar/, а foo.com/bar/file.txt пройдёт не изменённым.

Если параметр PREPEND_WWW имеет значение True, то URL, которые не имеют префикса www., будут перенаправлены на URL с этим префиксом.

Оба параметра нужны для нормализации URL. Философия такова, что каждый URL должен существовать в одном и только одном месте. С технической точки зрения URL example.com/bar является отдельным представлением от example.com/bar/, которое в свою очередь — от www.example.com/bar/. Индексаторы поисковых движков будут рассматривать эти URL как отдельные, что отрицательно скажется на рейтинге вашего сайта на этом поисковике. Так что нормализация URL приносит пользу.

Обработка ETags на основе параметра USE_ETAGS: Etags являются оптимизацией HTTP уровня для условного кэширования страниц. Если параметр USE_ETAGS имеет значение True, Django будет вычислять Etag для каждого запроса, выполняя MD5-хэширование содержимого страницы, и заботиться об отправки откликов «Не изменилось», если это так.

Следует отметить существование также компонента для условного GET, который поддерживает Etags и кое-что ещё.

Компонент компрессии

Класс компонента: django.middleware.gzip.GZipMiddleware.

Этот компонент автоматически сжимает содержимое страниц для браузеров, которые поддерживают алгоритм gzip (все современные браузеры). Использование этого компонента может значительно снизить объём трафика, потребляемого вашим сервером. Недостатком применения этого компонента является возрастание времени отклика из-за затрат на компрессию страницы.

Обычно мы предпочитаем иметь высокую скорость отклика, а не экономию трафика, но если вы предпочитаете обратно, просто активируйте этот компонент.

Компонент условного GET

Класс компонента: django.middleware.http.ConditionalGetMiddleware.

Компонент предоставляет поддержку условного GET. Если отклик содержит заголовки Last-Modified или ETag, а запрос содержит If-None-Match или If-Modified-Since, то отклик заменяется на 304 («Не изменён»). Поддержка ETag зависит от параметра USE_ETAGS и ожидает наличия заголовка ETag в отклике. Как упоминалось ранее, заголовок ETag устанавливается стандартным компонентом.

Данный компонент также удаляет содержимое любого отклика для запроса HEAD ??? FIXME ??? и устанавливает заголовки отклика Date и Content-Length для всех запросов.

Компонент поддержки обратного прокси

Класс компонента: django.middleware.http.SetRemoteAddrFromForwardedFor.

Этот компонент мы разобрали ранее в секции « Что такое компоненты? ». Он устанавливает request.META['REMOTE_ADDR'], используя request.META['HTTP_X_FORWARDED_FOR'], если последний установлен. Это полезно, когда вы находитесь за обратным прокси, который преобразовывает параметр REMOTE_ADDR каждого запроса в 127.0.0.1.

Опасность!

Компонент не производит проверку HTTP_X_FORWARDED_FOR.

Если вы не находитесь за обратным прокси сервером, не используйте этот компонент.Любой может сфабриковать значение HTTP_X_FORWARDED_FOR и, соответственно, подменить свой IP адрес.

Используйте этот компонент только в том случае, когда вы абсолютно доверяете значениям в HTTP_X_FORWARDED_FOR.

Компонент поддержки сессий

Класс компонента: django.contrib.sessions.middleware.SessionMiddleware.

Этот компонент обеспечивает поддержку сессий. Подробности приведены в главе « Сессии, пользователи и регистрация ».

Компонент для кэширования сайта

Класс компонента: django.middleware.cache.CacheMiddleware.

Этот компонент обеспечивает кэширование каждой страницы Django проекта. Подробности приведены в главе « Кэширование ».

Компонент поддержки транзакций

Класс компонента: django.middleware.transaction.TransactionMiddleware.

Этот компонент «подключает» операции COMMIT или ROLLBACK к фазе запроса/отклика. Если функция представления отрабатывает успешно, то выполняется COMMIT. Если представление вызывает исключение, то выполняется ROLLBACK.

Место определения этого компонента в стеке компонентов имеет значение. Компоненты вызываемые до данного компонента выполняются по стратегии коммит-по-сохранению (стандартное поведение Django). Компоненты вызываемые после — будут включены в транзакцию в которой будет работать функция представления.

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

Компонент X-View

Класс компонента: django.middleware.doc.XViewMiddleware.

Этот компонент добавляет определённый HTTP заголовок X-View в запросы HEAD, которые идут от IP адресов, указанных в параметре INTERNAL_IPS. Компонент используется системой автоматического документирования Django.

Глава 16. Интеграция с унаследованным

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

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

Интеграция с унаследованной базой данных

Слой Django, предназначенный для поддержки баз данных, генерирует SQL схему базы данных по коду модели, но когда вы сталкиваетесь с унаследованной базой данных, то у вас уже есть схема. В этом случае вам потребуется создать модели для каждой существующей таблицы. Для решения такой задачи Django поставляется с утилитой, которая может генерировать код модели по описанию таблицы в базе данных. Данная утилита называется inspectdb и вы можете вызывать её с помощью команды:

python manage.py inspectdb

Использование inspectdb

Утилита inspectdb просматривает базу данных, указанную в вашем конфигурационном файле, определяет представление модели для каждой таблицы и выводит код модели на стандартный вывод.

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

Создать проект Django с помощью команды python manage.py startproject mysite, где mysite является именем проекта.

Отредактировать конфигурационный файл проекта, mysite/settings.py, внеся в него правильные параметры соединения с базой данных. За подробностями обратитесь к главе « Модели ».

Создать приложение внутри проекта с помощью команды python manage.py startapp myapp, где myapp является именем приложения.

Выполнить команду python manage.py inspectdb. Команда выведет код модели для каждой таблицы базы данных. Просмотрите этот вывод.

Сохранить вывод в файл mysite/myapp/models.py:

python mysite/manage.py inspectdb > mysite/myapp/models.py

Отредактировать файл mysite/myapp/models.py, приведя его в нужный вид.

Очистка сгенерированных моделей

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

Каждая таблица базы данных описывается в виде модели. Это означает, что потребуется провести рефакторинг моделей для преобразования таблиц типа «многие-ко-многим» в объекты ManyToManyField.

Каждая созданная модель имеет атрибуты для каждого поля, включая поле id. Однако, следует помнить, что Django автоматически добавляет поле id primary key, если модель не содержит первичного ключа. Следовательно, вы пожелаете удалить строки подобные этой:

id = models.IntegerField(primary_key=True)

Такие строки не только избыточны, но они также могут вызвать проблемы в случае, если ваше приложение будет добавлять «новые» записи в эти таблицы. Утилита inspectdb не может надежно определить является ли поле автоинкрементным, таким образом данная задача ложится на вас, используйте AutoField когда это необходимо.

Каждый тип поля (т.е., CharField, DateField) определяется с помощью описания данного поля (т.е., VARCHAR, DATE). Если утилита inspectdb не может точно определить тип поля, она использует TextField и добавляет комментарий «Данный тип поля является предположением.» («This field type is a guess.») после поля. Обращайте внимания на такие поля и изменяйте их тип при необходимости.

Если для поля таблицы невозможно подобрать эквивалент в виде поля модели, можно вообще его не указывать. Слой Django для работы с моделями не требует, чтобы было описано каждое поле таблицы.

Если имя поля таблицы совпадает с зарезервированным словом языка Python (например, pass, class или for), утилита inspectdb добавит суффикс _field к имени поля и установит атрибуту db_column реальное значение имени поля.

Например, если таблица содержит целочисленное поле с именем for, сгенерированная модель будет содержать такое определение для этого поля:

for_field = models.IntegerField(db_column='for')

Утилита inspectdb добавит комментарий «Поле было переименовано, так как имя конфликтовало с зарезервированным словом языка Python.» («Field renamed because it was a Python reserved word.») после поля.

Если в базе данных есть таблицы, которые ссылаются на другие таблицы (как это происходит в большинстве баз данных), вам может потребоваться изменить порядок описания моделей в файле. Например, если модель Book ссылается на модель Author, то модель Author должна быть определена до модели Book. Если требуется создать ссылку на модель, которая ещё не была определена, вы можете использовать имя модели, а не сам объект модели.

Утилита inspectdb определяет первичные ключи для таблиц следующих баз данных: PostgreSQL, MySQL и SQLite. Следовательно, в модели будет внесено primary_key=True, там где это необходимо. Для остальных баз данных вам потребуется вручную внести это определение в определённое поле каждой модели, так как Django требует определения первичного ключа для каждой модели.

Определение ссылок работает только с PostgreSQL и с некоторыми типами таблиц MySQL. В остальных случаях такие поля будут сгенерированы как «IntegerField's, assuming the foreign-key column was an INT column».

Интеграция с системой аутентификации

Существует возможность интеграции Django с имеющейся системой аутентификации — с другим источником имён и паролей или методов аутентификации.

Например, ваша компания может уже использовать LDAP, в котором хранятся имена и пароли для каждого сотрудника. Администратору сети и пользователям быстро надоест использовать разные логины для работы с LDAP и Django приложениями.

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

Описание источников аутентификации

С технической точки зрения, Django поддерживает список источников аутентификации. Когда кто-нибудь вызывает django.contrib.auth.authenticate() (описанный в главе « Сессии, пользователи и регистрация »), Django пытается проверить пользователя, используя все источники аутентификации. Django перебирает каждый источник, пока не найдёт совпавший.

Список источников аутентификации, предназначенных для использования, определён в параметре AUTHENTICATION_BACKENDS. Параметр должен хранить кортеж путей, которые указывают на классы, которые в свою очередь знают, как выполнять процедуру аутентификации. Эти классы могут быть где угодно, главное, чтобы их можно было найти через $PYTHONPATH.

По умолчанию, параметр AUTHENTICATION_BACKENDS содержит:

('django.contrib.auth.backends.ModelBackend',)

Это простая схема аутентификации, которая проверяет пользователя по базе данных.

Порядок определения источников в параметре AUTHENTICATION_BACKENDS имеет значение. Таким образом, если пользователь верно определён в нескольких источниках, Django остановит свой поиск при первом совпадении.

Реализация источников аутентификации

Источник аутентификации — это обычный класс, который имеет два метода: get_user(id) и authenticate(**credentials).

Метод get_user() принимает параметр id, который может быть именем пользователя, идентификатором в базе данных, да всем чем угодно, и возвращает объект User.

Метод authenticate() принимает удостоверение личности в виде именованных аргументов. Обычно это выглядит так:

class MyBackend(object):
    def authenticate(self, username=None, password=None):
        # Check the username/password and return a User.

Но также может принимать элемент FIXME???, например:

class MyBackend(object):
    def authenticate(self, token=None):
        # Check the token and return a User.

Другими словами, метод authenticate() должен проверять полученное удостоверение личности и должен возвращать объект User, который соответствует переданному удостоверению, если последнее является верным. В противном случае метод должен возвращать None.

Интерфейс администратора Django плотно объединён с собственным объектом User, описанным в главе « Сессии, пользователи и регистрация ». Для взаимодействия с этой системой лучше всего создавать объект User для каждого пользователя, который существует в вашем источнике (т.е., для вашего LDAP, вашей внешней SQL базе данных и так далее.). Либо вы разработаете для этого скрипт, либо ваш метод authenticate() сделает это при первом входе пользователя в систему.

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

from django.conf import settings
from django.contrib.auth.models import User, check_password

class SettingsBackend(object):
    """
    Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD.

    Use the login name, and a hash of the password. For example:

    ADMIN_LOGIN = 'admin'
    ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
    """
    def authenticate(self, username=None, password=None):
        login_valid = (settings.ADMIN_LOGIN == username)
        pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
        if login_valid and pwd_valid:
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # Create a new user. Note that we can set password
                # to anything, because it won't be checked; the password
                # from settings.py will.
                user = User(username=username, password='get from settings.py')
                user.is_staff = True
                user.is_superuser = True
                user.save()
            return user
        return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Интеграция с унаследованными веб приложениями

Существует возможность выполнения Django приложений на том же веб сервере, на котором работает приложение, основанное на другой технологии. Наиболее прямолинейным способом решения такой задачи является использование конфигурационного файла Apache, httpd.conf, для определения какие URL с помощью какой технологии обрабатывать. (Следует отметить, что глава « Развёртывание Django » описывает процесс установки приложения на Apache, имеет смысл прочитать сначала тот раздел перед попыткой такой интеграции.)

Решение заключается в том, что Django будет использоваться для определённого шаблона URL только если так будет сказано в httpd.conf. Стандартная установка на сервер, описанная в главе « Развёртывание Django », предполагает, что Django поддерживает целый домен:

<Location "/">
    SetHandler python-program
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
    PythonDebug On
</Location>

Здесь, строка <Location "/"> означает «обрабатывать каждый URL, который начинается от корня домена».

Удобно ограничивать эту директиву определённым каталогом. Например, у вас есть унаследованное PHP приложение, которое поддерживает множество страниц текущего домена и вы желаете установить интерфейс администратора Django в каталог /admin/ без влияния на PHP приложение. Для этого, следует определить директиву <Location> для /admin/:

<Location "/admin/">
    SetHandler python-program
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
    PythonDebug On
</Location>

После этого, только URL, которые начинаются с /admin/ будут обрабатываться Django. Остальные страницы будут использовать ранее существовавшую инфраструктуру.

Следует отметить, что подключение Django к ограниченному URL никак не влияет на внутренний процесс Django, который обрабатывает URL. Django работает с абсолютным URL (т.е., /admin/people/person/add/), а не «урезанную» версию URL (т.е., /people/person/add/). Это означает, что корневая схема URL должна содержать ведущий /admin/.

Глава 17. Расширение интерфейса администратора

Перевод © Попов Руслан <radz • yandex • ru>

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Интерфейс администратора Django был представлен в главе « Интерфейс администратора Django », а теперь пришло время вернуться к нему и изучить его подробнее.

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

Несколько последних разделов главы « Интерфейс администратора Django » описывают несколько простых способов настройки определённых частей интерфейса администратора. Давайте вспомним, о чём там было написано. Там описывался процесс настройки списков и форм редактирования для приведения интерфейса к стилю вашего сайта.

Также там обсуждалось, когда и зачем надо использовать интерфейс администратора (см. главу « Когда и почему надо использовать интерфейс администратора, а когда не надо »). Интерфейс администратора позволяет решать такие общие задачи с минимальной настройкой под них или вообще без неё. Но такой подход означает, что интерфейс не может работать в других режимах редактирования.

Мы обсудим режимы для которых не предназначен интерфейс администратора позже. Давайте сначала разберёмся в философии.

Чтобы избежать траты времени на отладку в случае необъяснимого сбоя в работе интерфейса администратора, рекомендую проверить имена методов в моделях. Например, метод email() в какой-нибудь модели может доставить массу неприятных минут.

Дзен администрирования

По своей сути интерфейс администратора Django предназначен для единственной работы — доверенные пользователи редактируют структурированную информацию.

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

Доверенные пользователи ...

Интерфейс администратора предназначен для использования людьми, которым вы, разработчик, доверяете. Это не просто означает «людей, которые были аутентифицированы», это означает, что Django предполагает, что ваши редакторы будут делать всё правильно.

Это, в свою очередь, означает, что не существует утверждённого процесса редактирования информации — если вы доверяете вашим пользователям, то никто не нуждается в подтверждении его действий. Следующим предположением является то, система прав доступа, мощная, не имеет поддержки для ограничения доступа на по объектной основе. Если вы доверяете кому-то редактировать его данные, то значит вы верите в то, что этот пользователь не будет редактировать чужие данные без соответствующего разрешения.

... редактируют ...

Основное назначение интерфейса администратора Django — позволить пользователям редактировать данные. С первого взгляда это кажется очевидным, но снова тут не всё так просто.

Например, несмотря на то, что интерфейс администратора достаточно удобен для просмотра данных, это не учитывали при его разработке. Вспомните недостаток права «может просматривать» (см. главу « Сессии, пользователи и регистрация »). Django предполагает, что раз пользователям позволено просматривать информацию через административный интерфейс, следовательно им можно редактировать её.

Следующим важным элементом является недостаток пошагового процесса FIXME??? Если процесс требует выполнения серии действий, нет никакой возможности заставить пользователя выполнить эти действия в строго определённом порядке. Интерфейс администратора Django нацелен на «редактирование», а не на сопутствующую активность. Также такое упразднение рабочего процесса нарушает принцип доверия: философия интерфейса администратора такова, что рабочий процесс является добровольным действием пользователя, а не чем-то, что реализовано в коде.

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

... структурированную информацию

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

Full Stop

Теперь должно быть очевидно, что интерфейс администратора Django не пытается делать всё для всех. Вместо этого мы тщательно сфокусировались на одной вещи и делаем её очень хорошо.

Когда наступает пора расширения функциональности интерфейса администратора Django, большая часть описанной философии применяется и здесь (следует отметить, что такие возможности «расширения» отсутствовали среди наших целей). Так как собственные представления могут реализовывать любую функциональность и так как они могут легко быть визуально интегрированы в интерфейс администратора (как описано в следующей секции), встроенные возможности для настройки интерфейса являются чем-то ограниченным по дизайну FIXME???

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

Наконец, мы должны упомянуть, что Django разработчики работают над новой версией административного интерфейса, который предоставит гораздо большие возможности по его настройке. Пока вы читаете эту книгу, эти новые возможности могут быть уже внедрены в дистрибутив Django. Просто спросите кого-нибудь из сообщества Django, был ли интегрирована ветка «newforms-admin» в общий репозиторий кода.

Настройка шаблонов интерфейса

В поставке Django поставляется набор инструментов для настройки встроенных шаблонов интерфейса администратора, которые мы кратко рассмотрим, но для остальных задач (таких как порядок работы с интерфейсом или гранулированность прав доступа), вам потребуется прочитать раздел « Создание своих представлений ».

Сейчас давайте рассмотрим несколько быстрых способов настройки вида (и, частично, поведения) интерфейса администратора. В главе « Интерфейс администратора Django » описано несколько наиболее общих задач: «ребрендинг» интерфейса (для тех островолосых боссов, которые ненавидят голубой цвет) и реализация собственных форм.

Далее появляется цель, для которой надо изменить шаблоны для определённого элемента. Каждое представление интерфейса администратора — списки изменений, формы редактирования, страницы подтверждения операции удаления и страницы с историей — имеет ассоциированный шаблон, который может быть переопределён рядом способов.

Первый способ — можно переопределить шаблон глобально. Представление ищет шаблон, используя стандартный механизм загрузки шаблонов, таким образом, если вы создадите шаблоны в одном из ваших шаблонных каталогов, Django подгрузит их вместо стандартных. Эти глобальные шаблоны перечислены в таблице « Глобальные шаблоны интерфейса администратора »:

Таблица 17.1. Глобальные шаблоны интерфейса администратора

Представление | Имя базового шаблона
Список изменений | admin/change_list.html
Форма редактирования | admin/change_form.html
Страница подтверждение операции удаления | admin/delete_confirmation.html
История объекта | admin/object_history.html

Однако большую часть времени вам будет требоваться изменить шаблон для единственного объекта или приложения, т.е., не глобально. Следовательно, каждое представление сначала ищет специфичные для модели или приложения шаблоны. Эти представления производят поиск в таком порядке:

admin/<app_label>/<object_name>/<template>.html

admin/<app_label>/<template>.html

admin/<template>.html

Например, представление формы редактирования для модели Book в приложении books ищет шаблоны так:

admin/books/book/change_form.html

admin/books/change_form.html

admin/change_form.html

Настройка шаблонов для отдельных моделей

Большую часть времени вы будете использовать первый вариант для переопределения шаблона для отдельной модели. Это лучше всего делать через расширение базового шаблона и добавления информации в один из блоков, определённых в этом шаблоне.

Например, нам требуется добавить небольшой текст помощи наверх страница с книгами. Это может выглядеть так как показано на рисунке « Изменённая форма редактирования »:

Рисунок 17.1. Изменённая форма редактирования

Это очень легко сделать — просто создайте шаблон с именем admin/bookstore/book/change_form.html и добавьте в него следующий код:

{% extends "admin/change_form.html" %}

{% block form_top %}
  <p>Insert meaningful help message here...</p>
{% endblock %}

Все эти шаблоны определяют ряд блоков, которые вы можете переопределять. Как и в случае с большинством программ, лучшей документацией является код, мы рекомендуем вам просмотреть шаблоны административного интерфейса (они находятся в django/contrib/admin/templates/), там много вкусного и свежего.

Свой JavaScript

Чаще всего изменения в шаблоны отдельных моделей вносят для того, чтобы добавить свой JavaScript на страницу — возможно, для реализации специализированного элемента или для выполнения какой-то работы на стороне клиента.

К счастью, это тоже не сложно. Каждый шаблон интерфейса администратора определяет {% block extrahead %}, который вы можете использовать для внедрения дополнительной информации в элемент <head>. Например, если вам требуется подключить библиотеку jQuery к странице с историей, делайте так:

{% extends "admin/object_history.html" %}

{% block extrahead %}
    <script src="http://media.example.com/javascript/jquery.js"
            type="text/javascript"></script>
    <script type="text/javascript">

        // code to actually use jQuery here...

    </script>
{% endblock %}

Замечание

Мы не знаем зачем вам понадобилась jQuery на странице с историей, но данный пример можно применить к любому шаблону интерфейса администратора.

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

Создание своих представлений

Любой, кто ищет способ добавить своё поведение в интерфейс администратора Django, вероятно немного расстраивается. «Всё, что вы рассказали, относится к визуальному преображению интерфейса» — слышится их плач. «Как же мне изменить функционирование интерфейса?»

Первое, что необходимо понять, — в этом нет магии. Следовательно интерфейс администратора не делает ничего особенного — это просто набор представлений, которые находятся в django.contrib.admin.views, для манипулирования данными.

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

Следуя традиции, давайте добавим представление «отчет издателя» к нашему книжному приложению из главы « Интерфейс администратора Django ». Мы реализуем представление, которое будет отображать список книг, разбитых по издателям — очень типичный пример своего представления.

Сначала, определим наше представление в схеме URL. Следует вставить эту строку:

(r'^admin/books/report/$', 'mysite.books.admin_views.report'),

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

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^admin/bookstore/report/$', 'bookstore.admin_views.report'),
    (r'^admin/', include('django.contrib.admin.urls')),
)

По какой причине надо определять в схеме URL своё представление до подключения представлений административного интерфейса? Следует вспомнить, что Django обрабатывает шаблоны URL по порядку. Шаблоны административного интерфейса совпадают почти со всем, что доходит до них. Таким образом, если мы изменим порядок этих двух строк, Django найдёт для URL соответствующее совпадение с представлением административного интерфейса, которое не будет работать так как надо. В этом конкретном случае, представление попытается подгрузить список изменений для модели Report приложения books, которого не существует.

Пришло время написать код нашего представления. Для простоты мы будем загружать информацию обо всех книгах в контекст и позволим шаблону управлять группировкой с помощью тега {% regroup %}. Создайте файл books/admin_views.py с таким кодом:

from mysite.books.models import Book
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.contrib.admin.views.decorators import staff_member_required

def report(request):
    return render_to_response(
        "admin/books/report.html",
        {'book_list' : Book.objects.all()},
        RequestContext(request, {}),
    )
report = staff_member_required(report)

Раз мы передали ответственность за группировку на шаблон, представление будет очень простым. Тем не менее, существует несколько тонких моментов, которые следует разъяснить:

Мы используем декоратор staff_member_required из django.contrib.admin.views.decorators. Он аналогичен декоратору login_required, который был рассмотрен в главе « Сессии, пользователи и регистрация », но этот декоратор также проверяет вхождение пользователя в группу «staff» и, следовательно, разрешает доступ к интерфейсу администратора.

Этот декоратор защищает все встроенные представления административного интерфейса и организует логику аутентификации вашего представления аналогично логике остального интерфейса.

Мы используем шаблон из каталога admin/. Пока явно не сказано обратное, хорошим тоном является хранение всех административных шаблонов в каталоге admin/. Мы также разместили наш шаблон в подкаталоге books — это тоже является хорошим тоном.

Мы используем RequestContext в качестве третьего параметра (context_instance) для render_to_response(). Это делает информацию о текущем пользователе доступной в шаблоне.

Для получения подробностей о RequestContext обратитесь к главе « Расширения для шаблонной системы ».

И в конце следует сделать шаблон для этого представления. Мы будем расширять встроенные шаблоны административного интерфейса так, чтобы визуально наше представление не отличалось от встроенных:

{% extends "admin/base_site.html" %}

{% block title %}List of books by publisher{% endblock %}

{% block content %}
<div id="content-main">
  <h1>List of books by publisher:</h1>
  {% regroup book_list|dictsort:"publisher.name" by publisher as books_by_publisher %}
  {% for publisher in books_by_publisher %}
    <h3>{{ publisher.grouper }}</h3>
    <ul>
      {% for book in publisher.list|dictsort:"title" %}
        <li>{{ book }}</li>
      {% endfor %}
    </ul>
  {% endfor %}
</div>
{% endblock %}

Путём расширения admin/base_site.html мы сделаем наше представление подобным стандартным, результат показан на рисунке « Наше представление »:

Рисунок 17.2. Наше представление

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

Мы завершим эту главу несколькими идеями по настройке административных представлений.

Переопределение встроенных представлений

Временами стандартные административные представления не вставляют. Вы легко можете подменить любое представление на своё — просто «перекройте» его своим URL. В результате ваше представление будет вызываться вместо стандартного.

Например, мы может заменить встроенное представление формы редактирования, на то, которое будет позволять пользователю просто указывать ISBN книги. По ISBN коду мы затем сможем запросить информацию о книге с сайта http://isbn.nu и создать объект автоматически.

Код для такого представления опущен, пусть это будет вашим домашним заданием, но важной частью является шаблон URL:

(r'^admin/bookstore/book/add/$', 'mysite.books.admin_views.add_by_isbn'),

Если эта строка указана в схеме URL до подключения административных представлений, то представление add_by_isbn полностью заменит стандартное представление.

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

Глава 18. Интернационализация

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Кравцов Даниил <danilaru • ms • tusur • ru>

Редактирование © Попов Руслан <radz • yandex • ru>

Django изначально разрабатывался в центре США (буквально: Лоуренс, штат Канзас, это в 65 км от географического центра континентального США). Несмотря на это, подобно большинству проектов с открытыми исходными текстами сообщество Django включает людей со всего мира. По мере того как это сообщество охватывало разных людей, аспекты интернационализации и локализации становились чрезвычайно важными. Так как множество разработчиков имеют в лучшем случае нечёткое понимание этих терминов, мы кратко их рассмотрим.

Интернационализация относится к процессу проектирования программ для потенциального использования с любым языком. Это включает в себя выделение текста (как элементы интерфейса, так и сообщения об ошибках) для будущего перевода, абстракцию даты и времени для вывода в различных стандартах, предоставляя поддержку для различных временных зон, а самое главное — уверенность, что код не содержит предположений о стране проживания пользователей. Вы часто видите термин «internationalization» сокращённый как I18N (число 18 — это количество букв между первой «I» и последней «N»).

Локализация относится к процессу фактического перевода программ подвергшихся интернационализации для использования в конкретной локали. Вы иногда будете встречать аббревиатуру L10N для обозначения термина «локализация».

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

Та же среда интернационализации, использованная для осуществления такого перевода, доступна вам для перевода вашего приложения.

С точки зрения разработчика, вам потребуется добавить минимальное количество обработчиков в ваш исходный код и в шаблоны. Такие обработчики называются строками перевода. Они указывают Django, что «Этот текст нужно переводить на язык пользователя, если перевод этого текста доступен для данного языка.»

Django принимает во внимание эти обработчики при переводе приложений на лету, с учётом пользовательских настроек.

По существу: Django делает две вещи:

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

Использует эту информацию для перевода приложений для определённого пользователя, согласно его языковым настройкам.

Замечание

Механизм переводов Django использует GNU gettext (http://www.gnu.org/software/gettext/) через стандартный модуль gettext, поставляемый с интерпретатором Python.

Если вам не требуется интернационализация

Обработчики интернационализации Django включены по умолчанию, что приводит к небольшому повышению расходования ресурсов. Если вы не желаете использовать интернационализацию, вам нужно назначить параметру USE_I18N значение False в вашем файле настроек. В этом случае Django применит некоторую оптимизацию и не будет загружать механизм интернационализации.

Возможно вы также пожелаете удалить django.core.context_processors.i18n из параметра TEMPLATE_CONTEXT_PROCESSORS.

Определение переводимых строк в коде

Строки перевода указывают, что «Этот текст надо перевести». Эти строки могут использоваться в коде и в шаблонах. Вашей задачей будет выделение таких строк, а система будет переводить выделенные вами строки и только их.

Стандартные функции для перевода

Помечайте строки для перевода, используя функцию _(). (Да, имя функции — символ нижнего подчеркивания). Для подключения этой функции используйте:

from django.utils.translation import gettext_lazy as _

В этом примере, текст, «Welcome to my site.» помечен для перевода:

def my_view(request):
    output = _("Welcome to my site.")
    return HttpResponse(output)

Функция django.utils.translation.gettext() идентична _(). Этот пример эквивалентен предыдущему:

from django.utils.translation import gettext
def my_view(request):
    output = gettext("Welcome to my site.")
    return HttpResponse(output)

Большинство разработчиков используют _(), так как она короче.

Механизм переводов работает и для вычисляемых значений. Этот пример идентичен двум предыдущим:

def my_view(request):
    words = ['Welcome', 'to', 'my', 'site.']
    output = _(' '.join(words))
    return HttpResponse(output)

Механизм переводов работает и для переменных. И снова идентичный пример:

def my_view(request):
    sentence = 'Welcome to my site.'
    output = _(sentence)
    return HttpResponse(output)

(Недостаток использования переменных или вычисляемых значений, как показано в предыдущих двух примерах, в том, что стандартная утилита поиска текста подлежащего переводу, make-messages.py, не сможет найти такие строки. Подробности по этой утилите будут дальше.)

Строки, передаваемые в _() или gettext(), могут содержать символы подстановки, указанные в соответствии со стандартом языка Python, например:

def my_view(request, n):
    output = _('%(name)s is my name.') % {'name': n}
    return HttpResponse(output)

Эта технология позволяет осуществлять перевод, привязанный к особенностям языка, перемещая символы подстановки в тексте. На пример, строка на английском «Adrian is my name.», может переведена на испанский как «Me llamo Adrian.», используя символ подстановки установленный после переводимого текста, а не перед ним.

По этой причине, вы должны использовать именованную подстановку вместо позиционной (%s или %d). Если вы используете позиционную подстановку, переводчик не сможет поменять местами подстановки.

Пометка строк для позднего перевода

Используйте функцию django.utils.translation.gettext_noop() для того, чтобы отметить строки для перевода, но без осуществления перевода в данный момент. Строки помеченный таким образом не переводятся до самого последнего момента.

Используйте этот подход, если у вас есть неизменные строки, которые должны быть сохранены на оригинальном языке — такие как строки в базе данных — но которые должны быть переведены в последний момент, например, когда строка показывается пользователю.

Отложенный перевод

Используйте функцию django.utils.translation.gettext_lazy() для отложенного перевода строк — это когда значение получают до того, как будет вызвана функция.

Например, для пометки атрибута поля help_text как подлежащего переводу сделайте следующее:

from django.utils.translation import gettext_lazy

class MyThing(models.Model):
    name = models.CharField(help_text=gettext_lazy('This is the help text'))

В данном примере функция gettext_lazy() сохраняет отложенную ссылку на строку, а не её перевод. Сам перевод будет выполнен в момент использования строки, например, при обработке шаблона на сайте администратора Django.

Если вам не нравится длинное имя функции, вы можете использовать _ (символ подчёркивания), вот так:

from django.utils.translation import gettext_lazy as _

class MyThing(models.Model):
    name = models.CharField(help_text=_('This is the help text'))

Всегда используйте отложенные переводы в моделях (иначе они не будут переведены корректно для каждого отдельного пользователя). Будет хорошей идеей добавлять перевод к именам полей и таблиц тоже. Это означает, что необходимо явно указывать опции verbose_name и verbose_name_plural в классе Meta:

from django.utils.translation import gettext_lazy as _

class MyThing(models.Model):
    name = models.CharField(_('name'), help_text=_('This is the help text'))
    class Meta:
        verbose_name = _('my thing')
        verbose_name_plural = _('mythings')

Образование множественного числа

Используйте функцию django.utils.translation.ngettext() для определения сообщений, которые имеют различные формы единственного и множественного числа, например:

from django.utils.translation import ngettext
def hello_world(request, count):
    page = ngettext(
        'there is %(count)d object',
        'there are %(count)d objects', count
    ) % {'count': count}
    return HttpResponse(page)

Функция ngettext() принимает три аргумента: строку для единственного числа, строку для множественного числа и количество объектов (которые передаются языку перевода в виде переменной count.

Определение переводимых строк в шаблоне

Для использования переводов в шаблонах Django применяются два шаблонных и немного отличный от Python синтаксис. Чтобы получить доступ к этим тегам, поместите {% load i18n %} в начале вашего шаблона.

Тег {% trans %} помечает строку для перевода:

<title>{% trans "This is the title." %}</title>

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

<title>{% trans "value" noop %}</title>

Нельзя использовать шаблонные переменные в теге {% trans %} — разрешены только строковые константы, в одиночных или двойных кавычках. Если ваш перевод требует использования переменных (символов подстановки), используйте {% blocktrans %}, например:

{% blocktrans %}This will have {{ value }} inside.{% endblocktrans %}

Для перевода шаблонного выражения — скажем, использование шаблонного фильтра — следует связать выражение с локальной переменной для использования внутри блока перевода:

{% blocktrans with value|filter as myvar %}
  This will have {{ myvar }} inside.
{% endblocktrans %}

Если вам необходимо связать более одного выражения внутри тега {% blocktrans %}, объедините выражения с помощью and:

{% blocktrans with book|title as book_t and author|title as author_t %}
  This is {{ book_t }} by {{ author_t }}
{% endblocktrans %}

Для множественного числа, укажите обе, единственную и множественную, формы с помощью тега {% plural %}, который используется внутри {% blocktrans %} и {% endblocktrans %}, например:

{% blocktrans count list|length as counter %}
  There is only one {{ name }} object.
{% plural %}
  There are {{ counter }} {{ name }} objects.
{% endblocktrans %}

С технической точки зрения все блоки и встроенные переводы используют соответствующие функции gettext() и ngettext().

Когда вы используете RequestContext (см. главу « Расширения для шаблонной системы »), ваш шаблон получает доступ к 3 переменным режима перевода:

{{ LANGUAGES }} — это список кортежей, в котором первый элемент является идентификатором языка, а второй — названием этого языка (родное название).

{{ LANGUAGE_CODE }} — это текущий, предпочитаемый пользователем, язык (т.е., en-us). Подробности далее в разделе «How Django Discovers Language Preference».

{{ LANGUAGE_BIDI }} — это текущей метод записи языка. Если True, значит пишут справа налево (иврит, арабский язык). Если False, значит слева направо (английский, французский, русский).

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

{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_current_language_bidi as LANGUAGE_BIDI %}

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

{% some_special_tag _("Page not found") value|yesno:_("yes,no") %}

В этом примере, тег и фильтр увидят уже переведённую строку (т.е., строка будет переведена до передачи шаблонной функции), поэтому им не надо знать о переводе.

Создание файлов с переводами

После того как вы пометили строки подлежащие переводу, вам следует написать (или получить) перевод для этих строк. В этом разделе мы расскажем как это делается.

Создание файлов с сообщениями

Первый шагом будет создание файла сообщений для нового языка. Файл сообщений — это обычный текстовый файл для одного языка, который содержит информацию обо всех доступных вариантах перевода строк и о том как они должны быть представлены для данного языка. Такие файлы должны иметь расширение .po.

Django поставляется с утилитой, bin/make-messages.py, которая автоматизирует создание и управление такими файлами.

Для создания или обновления файла сообщений выполните команду bin/make-messages.py -l de, где de — это идентификатор языка для которого создаётся файл. В данном случае, идентификатор языка указан в формате локали. Например, pt_BR используется для бразильского варианта португальского языка, а at — для австрийского варианта немецкого языка. Взгляните на идентификаторы языков в каталоге django/conf/locale/, чтобы получить информацию о поддерживаемых.

Скрипт должен быть запущен из одного из трёх мест:

Корневой каталог django (но не тот который получен из Subversion, а тот который указан в $PYTHONPATH).

Корневой каталог вашего проекта.

Корневой каталог вашего приложения.

Скрипт обрабатывает целое дерево, над которым он был запущен, и получает все строки, помеченные для перевода. Он создаёт (или обновляет) файл сообщений в каталоге conf/locale. В примере для немецкого языка, файл будет создан так: conf/locale/de/LC_MESSAGES/django.po.

Если скрипт был запущен над деревом проекта или приложения, он сделает то же самое, но каталог будет другим: locale/LANG/LC_MESSAGES (следует отметить отсутствие префикса conf). Перед первым применением скрипта следует создать каталог locale.

Нет утилиты gettext?

Если у вас нет установленной утилиты gettext, то bin/make-messages.py создаст пустые файлы. В этом случае следует либо установить утилиту или просто скопировать файл сообщений для английского языка (conf/locale/en/LC_MESSAGES/django.po) и использовать его в качестве отправной точки. Это просто пустой файл с переводами строк.

Формат .po файлов прямолинеен. Каждый .po файл содержит небольшой кусок метаданных, таких как контактная информация сопровождающего данный перевод, остальное место в файле занимает список сообщений — простое соответствие между переводимыми строками и собственно переводами для этих строк.

Например, если ваше приложение содержит строку «Welcome to my site.», в таком виде:

_("Welcome to my site.")

то утилита bin/make-messages.py создаст .po файл, который будет содержать кусок — сообщение:

#: path/to/python/module.py:23
msgid "Welcome to my site."
msgstr ""

Кратко рассмотрим этот кусок:

msgid является переводимой строкой, которая определена в исходном коде. Не изменяйте её.

msgstr является переводом для строки. Изначально это поле пустое. Это ваша задача заполнить его. Не забывайте о необходимости сохранять кавычки вокруг текста.

Для удобства каждое сообщение включает в себя имя файла и номер строки откуда была получена для перевода данная строка.

Длинные сообщения являются особым случаем. Сразу после msgstr (или msgid) указывается пустая строка. Затем содержимое строки записывается на следущей линии или линиях. Такие строки впоследствии объединяются. Не забывайте оставлять завершающие пробелы в конце линий, иначе строка будет объединена без этих пробелов!

Например, ниже представлен многострочный перевод (взято из испанской локализации, которая поставляется с Django):

msgid ""
"There's been an error. It's been reported to the site administrators via e-"
"mail and should be fixed shortly. Thanks for your patience."
msgstr ""
"Ha ocurrido un error. Se ha informado a los administradores del sitio "
"mediante correo electrónico y debería arreglarse en breve. Gracias por su "
"paciencia."

Не забывайте о завершающих пробелах.

Помните о своей кодировке

При создании .po файлов в вашем текстовом редакторе, сначала исправьте строку с указанием кодировки (произведите поиск по CHARSET) и установите правильное значение кодировки. Обычно UTF-8 подходит для большинства языков, но gettext должна обработать любую кодировку, которую вы укажете.

Для повторного получения строк из исходного кода и шаблонов для перевода новых строк и обновления всех файлов сообщений для всех языков, выполните это:

make-messages.py -a

Компилирования файлов сообщений

После того как вы создали файл сообщений и после каждого изменения его содержимого вам потребуется компилировать его в более эффективную форму с помощью gettext. Для этого следует применять утилиту bin/make-messages.py.

Эта утилита «пробегает» по всем доступным .po файлам и создаёт .mo файлы, которые являются бинарным представлением первых. В том же каталоге из которого вы запускали bin/make-messages.py выполните это:

bin/compile-messages.py

И всё. Ваш перевод готов для использования.

Как Django получает языковые настройки

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

С точки зрения разработчика Django имеет очень гибкую модель принятия решения о том, какой язык следует использовать: для всего проекта, для отдельного пользователя или для обоих случаев.

Для определения языковой настройки на уровне всего проекта установите LANGUAGE_CODE в вашем файле настроек. Django использует эту настройку в случае, если никаких других переводов не найдено.

Если требуется запустить Django так, чтобы он использовал ваш родной язык и для этого языка есть файл переводов, просто установите LANGUAGE_CODE.

Если вы хотите позволить каждому отдельному пользователю указывать его собственные языковые настройки, используйте LocaleMiddleware. Пакет LocaleMiddleware позволяет осуществлять выбор языка, основываясь на данных из запроса. Он настраивает содержимое страницы для каждого пользователя.

Для использования LocaleMiddleware добавьте django.middleware.locale.LocaleMiddleware в параметр MIDDLEWARE_CLASSES. Так как порядок объявления пакетов в этом параметре имеет значение, вы должны руководствоваться следующими правилами:

Удостоверьтесь, что этот пакет находится среди главных пакетов.

Пакет должен идти после SessionMiddleware, потому что LocaleMiddleware использует данные из сессии.

Если вы используете CacheMiddleware, поместите LocaleMiddleware после него (иначе пользователи смогут получить закэшированные данные в неправильной локали).

Например, ваш параметр MIDDLEWARE_CLASSES может выглядеть так:

MIDDLEWARE_CLASSES = (
   'django.middleware.common.CommonMiddleware',
   'django.contrib.sessions.middleware.SessionMiddleware',
   'django.middleware.locale.LocaleMiddleware'
)

Пакет LocaleMiddleware пытается определить языковую настройку пользователя по следующему алгоритму:

Сначала проверяется содержимое django_language из сессии пользователя.

Если не удалось, проверяется cookie django_language.

Если не удалось, проверяется HTTP заголовок Accept-Language. Этот заголовок отправляется браузером и указывает серверу какой именно язык вы предпочитаете, в порядке приоритета. Django перебирает все указанные в заголовки языки пока не найдёт первый для которого есть перевод.

Если не удалось, Django использует значение глобального параметра LANGUAGE_CODE.

В каждом из шагов ожидается, что языковая настройка будет указана в стандартном формате в виде строки. Например, бразильский вариант португальского языка имеет обозначение pt-br. Если базовый язык доступен, а вариант нет, Django использует базовый язык. Например, если пользователь указал de-at (австрийский вариант немецкого языка), но для Django доступен только немецкий, значит будет использован немецкий язык.

Могут быть выбраны только те языки, которые указаны в параметре LANGUAGES. Таким образом, с помощью этого параметра можно ограничивать набор языков, поддерживаемых приложением:

LANGUAGES = (
    ('de', _('German')),
    ('en', _('English')),
)

Данный пример ограничивает набор языков доступный для автоматического выбора немецким и английским языками (а также любыми их вариантами, такими как de-ch или en-us).

Если вы определяете свой LANGUAGES, можно помечать названия языков в качестве строк подлежащих переводу. Но в этом случае следует использовать функцию-заглушку gettext(), а не из модуля django.utils.translation. Нельзя импортировать django.utils.translation в конфигурационном файле, так как этот модуль сам зависит от файла конфигурации, можем получить бесконечный цикл.

Решением этой проблемы будет использование функции-заглушки, например так:

_ = lambda s: s

LANGUAGES = (
      ('de', _('German')),
      ('en', _('English')),
)

Такой подход позволяет утилите bin/make-messages.py находить такие строки для перевода, но перевод не будет осуществляться налету. Таким образом вам потребуется не забыть сменить язык в настоящей функции gettext() в любом коде, который использует LANGUAGES.

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

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

Технические идентификаторы сообщений хорошо распознаются, они написаны заглавными буквами. Не следует переводить идентификаторы сообщений, просто предоставьте корректный вариант для английского значения. Например, для DATETIME_FORMAT (или DATE_FORMAT или TIME_FORMAT, это будет форматированная строка, которую вы желаете использовать для вашего языка. Формат идентичен формату строки, используемому шаблонным тегом {{ now }}.

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

def hello_world(request, count):
    if request.LANGUAGE_CODE == 'de-at':
        return HttpResponse("You prefer to read Austrian German.")
    else:
        return HttpResponse("You prefer to read another language.")

Следует отметить, что в случае статических переводов (т.е., без использования пакета) язык определён в settings.LANGUAGE_CODE, а при использовании динамического перевода (т.е., через пакет) — в request.LANGUAGE_CODE.

Представление set_language

Для вашего удобства Django поставляется с представлением django.views.i18n.set_language, которое устанавливает языковую настройку пользователя и перенаправляет браузер обратно на предыдущую страницу.

Для активации этого представления добавьте следующую строку в схему URL:

(r'^i18n/', include('django.conf.urls.i18n')),

Следует отметить, что данный пример делает это представление доступным на /i18n/setlang/.

Это представление вызывается через метод GET[15] с указанием параметра language в строке запроса. Если поддержка сессий активна, представление сохраняет информацию о выбранном языке в сессии пользователя. В противном случае, сохранение производится в cookie django_language.

После определения выбора пользователя Django перенаправляет браузер, следуя алгоритму:

Проверяется параметр next в строке запроса.

Если такого параметра нет или он пуст, проверяется заголовок Referer.

Если заголовок пуст (браузер мог запретить его отправку), тогда пользователь будет перенаправлен на корень сайта.

Ниже представлен пример HTML шаблона:

<form action="/i18n/setlang/" method="post">
<input name="next" type="hidden" value="/next/page/" />
<select name="language">
{% for lang in LANGUAGES %}
<option value="{{ lang.0 }}">{{ lang.1 }}</option>
{% endfor %}
</select>
<input type="submit" value="Go" />
</form>

[15] В Django 1.x изменили на POST.

Использование переводов в ваших проектах

Django ищет переводы по следующему алгоритму:

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

Затем просматривается каталог locale в каталоге проекта. Если файл с переводами будет найден, он будет установлен.

Наконец, просматривается базовый каталог django/conf/locale.

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

Замечание

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

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

$APPPATH/locale/<language>/LC_MESSAGES/django.(po|mo)

$PROJECTPATH/locale/<language>/LC_MESSAGES/django.(po|mo)

Все пути перечисленные в параметре LOCALE_PATHS файла конфигурации проверяются по порядку для <language>/LC_MESSAGES/django.(po|mo).

$PYTHONPATH/django/conf/locale/<language>/LC_MESSAGES/django.(po|mo).

Для создания файлов с сообщениями следует использовать утилиту bin/make-messages.py. Потребуется только запускать её из правильного места — в каталоге в котором находится conf/locale (в случае дерева исходных текстов) или locale/ (в случае сообщений приложения или проекта). Далее следует использовать compile-messages.py для создания бинарного django.mo, который используется gettext.

Файлы сообщения для приложения немного сложнее обнаружить, им требуется пакет LocaleMiddleware. Если вы не используете этот пакет, будут обрабатываться только файлы сообщений Django и проекта.

Наконец, вы должны как-то организовать структуру файлов с переводами. Если ваши приложения будут распространяться среди других пользователей и будут использоваться в других проектах, может потребоваться использовать переводы на уровне приложений. Но одновременное наличие переводов на уровне приложений и проекта могут породить неприятные проблемы с make-messages.py. Эта утилита просматривает все каталоги, начиная от текущего, и помещает идентификаторы сообщений в файл сообщений проекта, а эти идентификаторы уже могут находиться в файле приложений.

Простейший способ решения этой проблемы — не хранить приложения внутри каталога проекта (предоставляя им собственные переводы). Тогда выполнение make-messages.py на уровне проекта соберёт строки относящиеся только к проекту и которые не будут распространяться независимо от проекта.

Переводы и JavaScript

Добавление возможности перевода строк в JavaScript порождает ряд проблем:

Код на JavaScript не имеет доступа к реализации gettext.

Код на JavaScript не имеет доступа к .po или .mo файлам. Они должны быть предоставлены сервером.

Каталоги с переводами для JavaScript должны быть малы насколько это возможно.

Django предоставляет интегрированное решения для этих проблем. Оно передаёт переводы в JavaScript, таким образом вы можете использовать gettext в JavaScript.

Представление javascript_catalog

Основным решением для этих проблем является представление javascript_catalog(), которое создаёт библиотеку кода JavaScript с функциями, имитирующими поведение интерфейса gettext, и с массивом переведённых строк. Эти строки получаются из приложения, проекта или ядра Django, в соответствии с тем, что вы определили в info_dict или в URL.

Вы можете использовать это так:

js_info_dict = {
    'packages': ('your.app.package',),
}

urlpatterns = patterns('',
    (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
)

Каждая строка в packages должна быть в формате, аналогичном параметру INSTALLED_APPS, и должна ссылаться на пакет, который содержит каталог locale. Если вы укажите несколько пакетов, все эти каталоги будут объединены в единый. Это полезно когда работаете с JavaScript, который использует строки из разных приложений.

Вы можете создать представление динамически, указав пакеты в схеме URL:

urlpatterns = patterns('',
    (r'^jsi18n/(?P<packages>\S+?)/$, 'django.views.i18n.javascript_catalog'),
)

Учитывая это, вы указываете пакеты в виде списка их имён, разделённых знаками + в URL. Это особенно полезно, если ваши страницы используют код из разных приложений, которые часто меняются и у вас нет желания помещать переводы в единый большой файл. С точки зрения безопасности эти значеним могут быть только в django.conf или в любом пакете указанном в параметре INSTALLED_APPS.

Использование каталога с переводами

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

<script type="text/javascript" src="/path/to/jsi18n/"></script>

Этим способом пользуется административный сайт для получения каталога с переводами от сервера. После загрузки каталога ваш JavaScript код может использовать стандартный интерфейс gettext:

document.write(gettext('this is to be translated'));

Есть даже интерфейс ngettext и функция интерполяции строк:

d = {
    count: 10
};
s = interpolate(ngettext('this is %(count)s object', 'this are %(count)s objects', d.count), d);

Функция interpolate() поддерживает как позиционную интерполяцию, так и именованную. Таким образом предыдущий код может быть переписан так:

s = interpolate(ngettext('this is %s object', 'this are %s objects', 11), [11]);

Синтаксис интерполяции позаимствован из Python. Вы не должны злоупотреблять интерполяцией, это всё ещё JavaScript, так что код будет повторять подстановки с помощью регулярного выражения. Это не так быстро как строковая интерполяция в Python, так что не используйте это лишний раз.

Создание каталога

Создание и обновление каталогов с переводами происходит аналогично другим видам каталогов. Единственное отличие в том, что следует указывать параметр -d djangojs:

make-messages.py -d djangojs -l de

Эта команда создаёт или обновляет каталог с переводами для JavaScript кода для немецкого языка. После обновления каталогов, просто выполните compile-messages.py аналогично предыдущим примерам.

Замечание для тех, кто уже работал с gettext

Если вы знакомы с gettext, вам следует помнить о особенностях использования этого пакета в Django:

Домен переводов определён как django или djangojs. Домен используется для различения различных программ, которые хранят свои данные в общей библиотеке сообщений (обычно в /usr/share/locale/). Домен django используется интерпретатором Python и в шаблонных строках. Домен загружается в глобальные каталоги переводов. Домен djangojs используется только для каталогов JavaScript и должен содержать минимально возможный объём информации.

Django использует только gettext() и gettext_noop(). Это происходит потому что Django всегда работает со строками DEFAULT_CHARSET. Не существует значительного преимуществу в использовании ugettext(), так как вы будете всегда нуждаться в UTF-8 (по-моему это наш случай FIXME).

Django не использует xgettext(). Оно использует обёртки Python для xgettext() и msgfmt(). Это гораздо удобнее.

Глава 19. Безопасность

Данная глава временно взята из первой версии книги и подлежит корректировке. Вы можете помочь с этим!

Перевод © Попов Руслан <radz • yandex • ru>

Интернет может быть страшным местом.

В наши дни, high-profile security gaffes seem to crop up on a daily basis. Мы видели вирусы, распространяющиеся с огромной скоростью, рои взломанных компьютеров становились оружием, бесконечная гонка вооружений в войне со спаммерами, и множество сообщений о взломанных веб сайтах.

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

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

Несмотря на всё это, сначала сделаем важное заявление: Мы не пытаемся предоставить полное руководство для каждого известного эксплоита FIXME и, соответственно, мы не будем пытаться объяснить каждую уязвимость подробно. Вместо этого, мы будет предоставлять краткое описание проблем на примере Django.

Тема безопасности в вебе

Если вы вынесите только одну вещь из этой главы, то пусть это будет — «Никогда, ни при каких условиях, не доверяйте никаким данным полученным от пользователей».

Вы никогда не будете знать, кто находится на другой стороне HTTP соединения. Это может быть один из ваших пользователей, но так же легко это может быть гнусный хакер, ищущий дыру в вашем сайте.

Любые данных из любого формата, которые приходит от пользовательских браузеров, должны рассматриваться с достаточным объёмом паранойи. Под этими данными понимаются как «внутренние» (т.е., переданные через формы), так и «внешние» (т.е., HTTP заголовки, cookie и другая информация HTTP запроса). Нет никаких сложностей в подмене метаинформации запроса, которую браузеры автоматически добавляют к нему.

Каждая из описываемых в данной главе уязвимостей происходит напрямую из-за доверия к данным, которые пришли по проводам и которые не были проверены перед их использованием. Вы должны постоянно спрашивать себя: «Откуда пришли эти данные?»

Внедрение SQL

SQL инъекция — это стандартная уязвимость, в которой атакующая сторона изменяет параметры веб страницы (такие как GET/POST данный или URL) для внедрения определённых кусков SQL запроса, которые наивное веб приложение выполняет напрямую над своей базой данных. Вероятно, это наиболее опасная и, к сожалению, наиболее часто встречающаяся уязвимость.

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

def user_contacts(request):
    user = request.GET['username']
    sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username
    # execute the SQL here...

Замечание

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

И хотя такой код не выглядит опасным, он таковым является.

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

SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';

Так как мы не проверяем введённую строку на наличие SQL команд, атакующий добавил выражение OR и это приведёт к выдаче всех записей из соответствующей таблицы.

Однако, существует как минимум ещё одна опасность. Представьте, что может случиться, если атакующий введёт '; DELETE FROM user_contacts WHERE 'a'='a'. Вот так будет выглядеть сконструированный запрос:

SELECT * FROM user_contacts WHERE username = '';
DELETE FROM user_contacts WHERE 'a' = 'a';

Ой! А куда делся наш список контактов?

Решение

Несмотря на то, что данная проблема является неочевидной, решение для неё будет простым: никогда не доверяйте пользовательским данным и всегда экранируйте всё, что вы используете при конструировании SQL запроса.

Всё это Django делает на уровне API для работы с базой данных. Он автоматически экранирует все специальные SQL параметры, учитывая соглашение об использовании кавычек для используемого вами сервера баз данных (т.е., PostgreSQL или MySQL).

Например, в данном вызове API:

foo.get_list(bar__exact="' OR 1=1")

Django выполнит соответствующее экранирование и результирующий оператор будет выглядеть так:

SELECT * FROM foos WHERE bar = '\' OR 1=1'

т.е., очень безобидно.

Это поведение характерно для всего API с некоторыми исключениями:

Аргумент where у метода extra() (см. приложение « Справочник по API взаимодействия с базой данных »). Этот параметр принимает сырой SQL, так было заложено при его разработке).

Вручную созданные запросы с помощью API низкого уровня.

В каждом из этих случаев, несложно защитить себя от ошибок. Надо просто использовать привязку переменных вместо конструирования запросов. Таким образом, предыдущий пример должен быть переписан так:

from django.db import connection

def user_contacts(request):
    user = request.GET['username']
    sql = "SELECT * FROM user_contacts WHERE username = %s;"
    cursor = connection.cursor()
    cursor.execute(sql, [user])
    # ... do something with the results

Низкоуровневый метод execute() принимает с SQL строку с символами подстановки (%s) и автоматически экранирует и подставляет параметры из списка, переданного вторым аргументом. Вы должны всегда конструировать свои SQL запросы таким способом.

К сожалению, вы не можете свободно использовать привязку переменных. Например, нельзя таким способом определять идентификаторы (т.е., имя таблицы или полей таблиц). Следовательно, если вам потребуется динамически создать список таблиц по, скажем, содержимому переменной POST, вам придётся экранировать их имена в своём коде. Django предоставляет функцию, django.db.backend.quote_name(), которая осуществляет экранирование идентификатора в соответствии с принятой схемой квотирования для текущей базы данных.

Межсайтовый скриптинг (XSS)

Данный тип уязвимости проявляется в веб приложениях, которые не производят экранирование информации, введённой пользователем, перед её помещением в HTML страницы. Это позволяет атакующему вставить определённый HTML код на вашу страницу, обычно это теги <script>.

Злобные хакеры часто используют XSS для кражи cookie и информации о сессии или вводят пользователей в заблуждение, вынуждая их вводить важную информацию (так называемый фишинг — рыбалка).

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

def say_hello(request):
    name = request.GET.get('name', 'world')
    return render_to_response("hello.html", {"name" : name})

Это представление просто получает имя из параметра GET и передаёт его в шаблон hello.html, который может выглядеть так:

<h1>Hello, {{ name }}!</h1>

Таким образом, если мы перейдём по URL http://example.com/hello/name=Jacob, то результатом будет страница, которая будет содержать:

<h1>Hello, Jacob!</h1>

Подождите, а что произойдёт, если перейти на http://example.com/hello/name=<i>Jacob</i>? Тогда мы получим это:

<h1>Hello, <i>Jacob</i>!</h1>

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

Проблема может стать ещё серьёзнее в случае, если вы сохраняете эти данные в базе данных и затем отображаете их на своём сайте. Например, на MySpace однажды нашли уязвимость, которая позволила провести атаку такого типа. Один пользователь вставил в свой профиль JavaScript, который автоматически добавлял этого пользователя в качестве друга, если кто-то посещал его страницу с профилем. За несколько дней у него появились миллионы друзей.

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

Компании MySpace очень повезло, что этот код не принялся автоматически удалять пользовательские аккаунты, изменять их пароли, заваливать сайт спамом или выполнять какой-нибудь ещё кошмарный сценарий, который позволяет данная уязвимость.

Решение

Решение простое — всегда экранируйте любую информацию с помощью тега escape (или его эквивалента) при отображение на вашем сайте информации, введённой пользователем.

Почему Django не делает это автоматически?

«Пусть Django автоматически экранирует все переменные, отображаемые в шаблонах» — эта тема достаточно часто возникает в листе рассылки разработчиков.

Пока шаблонная система Django избегает такого поведения, потому что при этом бы незаметно изменилось бы то, что должно работать прямолинейно — отображать переменные. Это очень хитрая задача, чтобы оценить её реализацию. К тому же, добавление скрытого неявного поведения идёт против основных идеалов Django (да и Python тоже), но обеспечение безопасности также является важной задачей.

Всё, что мы хотим этим сказать, — возможно когда-нибудь Django дорастёт до некоторой формы автоматического (или почти автоматического) экранирования в будущем. Рекомендуем периодически проверять официальную документацию на предмет нововведений. Она всегда будет содержать более актуальную информацию, чем эта книга.

Даже если эта функциональность будет добавлена в Django, вы всё ещё будете должны иметь привычку спрашивать себя каждый раз — «Откуда пришли эти данные?». Никакое автоматическое решение не защитит ваш сайт от XSS атак на 100%.

Подделка HTTP запросов

Атаки типа «подделка HTTP запросов» (CSRF) случаются когда вредоносный веб сайт принуждает пользователей неявно загружать URL с сайта, на котором они были аутентифицированы, а следовательно, пользуется их правами.

Djagno обладает встроенными инструментами для защиты от этого типа атак. Сама атака и эти инструменты описаны в главе « Средства от других разработчиков ».

Подделка сессий

Подделка сессий не является каким-то видом атаки, это целый класс атак, нацеленных на данные пользовательской сессии. Они могут принимать ряд различных форм:

Человек посередине — атакующий читает данные сессии в момент когда они проходят мимо него по сети.

Подделка сессии — атакующий использует идентификатор сессии (возможно полученный с помощью атаки «человек посередине») для того, чтобы притвориться другим пользователем.

Примером этих двух форм будет ситуация когда атакующий, сидя в кофейне, используя локальную WiFi сеть будет ловить идентификаторы сессий. Затем эти идентификаторы будут использоваться, чтобы представиться оригинальным пользователем.

Подделка cookie — атакующий переопределяет данные (они могут быть в режиме «только для чтения») в cookie. Глава « Сессии, пользователи и регистрация » детально описывает работу cookie, одной из ярких особенностей которых является простота незаметного изменения содержимого браузером или злонамеренными пользователями.

Существует длинная история о сайтах, которые хранили cookie подобные IsLoggedIn=1 или даже LoggedInAsUser=jacob. Чрезвычайно просто воспользоваться таким подарком.

Проще говоря, не стоит доверять данным, сохранённым в cookie.

Фиксация сессии — атакующий принуждает пользователя обновить идентификатор пользовательской сессии.

Например, PHP позволяет передавать идентификаторы сессий через URL (т.е., http://example.com/?PHPSESSID=fa90197ca25f6ab40bb1374c510d7a32). Атакующий просто подставляет такой идентификатор в URL ссылки, на которую нажмёт пользователь, заставляя последнего подключиться к указанной сессии.

Фиксация сессии используется при «рыбалке», принуждая пользователя вводить свою персональную информацию в аккаунт, которым владеет атакующий. Последний может просмотреть свой аккаунт и получить введённые данные.

Отравление сессии FIXME — атакующий внедряет в пользовательскую сессию потенциально опасные данные. Обычно это происходит при отправке форм, которые использует пользователь для установки данных сессии.

Каноническим примером будет сайт, который сохраняет простые пользовательские настройки (подобные цвету фона страницы) в cookie. Атакующий может подвести пользователя к нажатию ссылки для отправки «цвета», который на самом деле содержит данные для XSS атаки. Если переданные данные не экранируются на стороне сервера, пользователь может снова внедрить вредоносный код в свою среду.

Решение

Существует целый ряд основных принципов, следование которым может защитить вас от этих атак:

Никогда не позволяйте информации о сессии быть в URL.

Среда Django для работы с сессиями, описанная в главе « Сессии », просто не позволяет так делать.

Не используйте данные в cookie напрямую. Вместо этого, храните идентификатор сессии, которая в свою очередь хранится в базе данных (например).

Если вы используете встроенную среду для работы с сессиями (т.е., request.session), это будет выполняться автоматически. Среда использует единственный cookie, в котором хранится единственный идентификатор сессии, а данные самой сессии хранятся в базе данных.

Не забывайте экранировать данные сессии, если вы отображаете их в шаблоне. Обратитесь к секции « Межсайтовый скриптинг (XSS) » и запомните, что он применяется к любой информации, которую создал пользователь, а также к любой информации, полученной от браузера. Вы должны рассматривать информацию из сессии, как информацию полученную от пользователя.

Не позволяйте атакующим перехватывать идентификаторы сессий.

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

Следует отметить, что ни один из этих принципов не защитит вас от атаки «человек посередине». Этот тип атак практически невозможно распознать. Если ваш сайт позволяет авторизованным пользователям просматривать важные данные, вы должны всегда передавать их с помощью протокола HTTPS. Также, если ваш сайт использует SSL, вы должны назначить True значению SESSION_COOKIE_SECURE — это заставит Django отправлять cookie с идентификатор сессии по протоколу HTTPS.

Внедрение E-mail заголовка

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

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

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

Если этот заголовок не экранируется во время генерации почтового сообщения, атакующий может отправить что-нибудь вида hello\ncc:spamvictim@example.com (где \n является символом перевода строки). Это может привести к созданию следующих заголовков:

To: hardcoded@example.com
Subject: hello
cc: spamvictim@example.com

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

Решение

Мы можем предотвратить такую атаку также как мы предотвращаем внедрение SQL — всегда экранируйте или проверяйте данные, которые вводит пользователь.

Встроенные в Django функции отправки электронной почты (django.core.mail) просто не позволяют использовать символы перевода строки в любом из полей, используемых для создания заголовков (from, to и subject). Если вы попытаетесь использовать django.core.mail.send_mail() совместно с полем subject, которое содержит символ перевода строки, то Django вызовет исключение BadHeaderError.

Если вы не используете встроенные в Django функции для отправки электронной почты, вам потребуется проверять заголовки на наличие там символа перевода строки. При их наличии надо будет выдавать ошибку или тихо удалять их оттуда. Рекомендуем посмотреть на класс SafeMIMEText в django.core.mail.

Directory Traversal

Directory Traversal FIXME является другим типом атак на внедрение своей информации. В этом случае злонамеренный пользователь принуждает систему прочитать и/или записать файлы, доступа к которым веб сервер иметь не должен.

Примером может быть представление, которое читает файлы с диска без аккуратной проверки их имени:

def dump_file(request):
    filename = request.GET["filename"]
    filename = os.path.join(BASE_PATH, filename)
    content = open(filename).read()

    # ...

Несмотря на то, что данное представление выглядит надёжным, так как доступ к файлу ограничен BASE_PATH и использованием os.path.join(), если атакующий использует в имени файла символы .. (переход в родительский каталог), он может получить доступ к файлам вне BASE_PATH. Имеет значение только время, которое придётся затратить на поиск правильного числа таких команд, скажем так: ../../../../../etc/passwd.

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

Другой вариант FIXME??? этой проблемы находится в коде, который динамически подгружает модули, основываясь на URL или другой информации из запроса. Широко освещённый пример пришёл из мира Ruby on Rails. До середины 2006 года Rails напрямую использовал URL, подобные http://example.com/person/poke/1, для загрузки модулей и вызова методов. В результате получалось так, что специально созданный URL мог автоматически загружать определённый код, включая скрипт для очистки базы данных!

Решение

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

Замечание

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

Хорошим примером экранирования можно считать представления из django.views.static. Вот его примерный код:

import os
import posixpath

# ...

path = posixpath.normpath(urllib.unquote(path))
newpath = ''
for part in path.split('/'):
    if not part:
        # strip empty path components
        continue

    drive, part = os.path.splitdrive(part)
    head, part = os.path.split(part)
    if part in (os.curdir, os.pardir):
        # strip '.' and '..' in path
        continue

    newpath = os.path.join(newpath, part).replace('\\', '/')

Django не читает файлы (пока вы не начнёте использовать функцию static.serve()), таким образом данная уязвимость не влияет на код Django.

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

Открытые сообщения об ошибках

Во время разработки наличие возможности просматривать трассировочную информацию и ошибки в браузере является чрезвычайно полезной. Django предоставляет «красивые» и информативные отладочные сообщения.

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

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

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

Решение

Django использует простой флаг, который управляет отображением сообщений об ошибках. Если параметр DEBUG установлен в True, то сообщения об ошибках будут отображаться в браузере. В противном случае, Django отобразит сообщение с ошибкой 500 («Internal server error»), используя предоставленный вами для этого случая шаблон. Этот шаблон должен находиться в файле 500.html и располагаться в корне одного из ваших шаблонных каталогов.

Однако разработчикам потребуется получать сообщения об ошибках от развёрнутого на боевом сервере сайта. Для этого каждая ошибка будет отправляться с приложением полной трассировочной информации на почтовые адреса, указанные в параметре ADMINS.

При использовании Apache в качестве веб сервера и mod_python необходимо проверить наличие следующей строки в файле конфигурации Apache:

PythonDebug Off

эта строка подавляет сообщения обо всех ошибках, которые возникают до передачи управления Django.

Заключительное слово о безопасности

Мы надеемся, что всё рассказанное здесь не слишком испугало вас. Интернет может быть диким и опасным миром, но немного предусмотрительности может вам помочь.

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

Глава 20. Развёртывание Django

Перевод © Чепиков Александр <achepikau • gmail • com>

Редактирование © Попов Руслан <radz • yandex • ru>

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

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

Причина очевидна, если взглянуть на исходные данные этой задачи: небольшая «семейная» газета из Канзаса вряд ли была в состоянии приобрести высококлассное серверное оборудование, поэтому авторы Django сконцентрировались на выжимании наилучшей производительности из доступных им ресурсов. Более того, разработчики Django долгое время работали и как системные администраторы — просто потому что имеющегося в наличии оборудования не хватало для принятия на работу системного администратора на полную ставку — несмотря на то, что их сайты обрабатывали к тому времени десятки миллионов обращений в сутки.

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

Но возможность масштабирования «вниз» — это только половина успеха. Django должен масштабироваться и «вверх», чтобы удовлетворить запросы крупных компаний и корпораций. Поэтому Django придерживается общего подхода для LAMP-подобных веб приложений, который часто называют «ничего общего».

Как расшифровывается LAMP?

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

Linux (Операционная система);

Apache (Веб сервер);

MySQL (Сервер базы данных);

PHP (Язык программирования).

Впоследствии, однако, эта аббревиатура стала скорее обозначением каких-то совокупностей общих типов программ с открытым кодом, нежели конкретного набора приложений. Поэтому, хотя Django использует Python и не привязана к конкретной базе данных, философия LAMP сильно влияет на умонастроения разработчиков Django.

Было даже несколько (в основном юмористических) попыток придумать похожую аббревиатуру для описания технологий, используемых в Django. Авторам данной книги пришлись по душе сокращения LAPD (Linux, Apache, PostgreSQL и Django), PAID (PostgreSQL, Apache, Internet и Django), а так же девиз «Use Django and get PAID!», что буквально означает «Применяй Django и получай зарплату!».

Ничего общего

По своей сути данный подход означает, что приложение использует множество программных компонентов, слабо связанных между собой. Такая архитектура появилась, как альтернатива на превалирующий когда-то подход: монолитный сервер приложений, объединяющий язык программирования, базу данных, веб сервер и даже часть операционной системы в один процесс (например, Java).

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

Сообщество LAMP, однако, обращает внимание на то, что если разбить каждую часть веб приложения на отдельные компоненты, то вполне можно стартовать с недорогого сервера и просто добавлять такие же сервера по мере роста проекта. Если сервер базы данных за 3 тысячи долларов не справляется с нагрузкой, достаточно просто купить ещё один (второй, третий или четвертый) столько раз, сколько потребуется. Если потребовалось дополнительное дисковое пространство, несложно добавить еще один NFS сервер.

Для того, чтобы такой подход всё-таки работал, веб приложения не должны быть привязаны к одному серверу при обработке запроса, или даже части запроса. В сильно распределенных LAMP (и Django) системах по крайней мере полдюжины серверов могут использоваться для выдачи одной веб страницы! Вариантов построения таких систем множество, но все они сводятся к следующим принципам:

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

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

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

Как и следовало ожидать, Django соответствует этим принципам более или менее точно — ни один из компонентов Django не нарушает данного подхода — но знание его основ поможет, когда наступит время для увеличения размеров приложения.

Как это работает?

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

Хорошо, вместо того, чтобы отвечать непосредственно на вопрос, давайте посмотрим на крайне неполный список компаний, которые основали свой бизнес на этой основе. Вы могли уже встречать их названия: Amazon, Blogger, Craigslist, Facebook, Google, LiveJournal, Slashdot, Wikipedia, Yahoo, YouTube.

Перефразируя известную сцену из «Когда Гарри встретил Салли»: «У нас будет всё то же, что и у них!»

Что предпочитают разработчики Django?

Перед тем, как погрузиться в детали, сделаем небольшое отступление.

Сообщество open source знаменито своими так называемыми «религиозными войнами»; множество (цифровых) чернил пролито в спорах о текстовых редакторах (emacs против vi), операционных системах (Linux против Windows и против MacOS), серверов баз данных (MySQL против PostgreSQL) и, естественно, о языках программирования.

Мы стараемся оставаться в стороне от всех этих битв. На это просто нет времени.

Однако существует множество вариантов для развертывания Django и нас постоянно спрашивают от том, что же мы сами предпочитаем. Практически всегда мы воздерживаемся от комментариев, так как это приводит только к разжиганию нового витка вышеупомянутых споров. Но ради того, чтобы закончить с этим и полностью раскрыть карты, мы указываем наши предпочтения:

Операционная система — Linux (Ubuntu, если быть ещё точнее);

Веб сервер — Apache с поддержкой mod_python;

Сервер базы данных — PostgreSQL.

Но мы знаем многих пользователей Django, выбравших другие варианты развертывания и, тем не менее, достигших больших успехов.

Использование Django с Apache и mod_python

Apache с поддержкой mod_python на текущий момент является наиболее стабильной и надёжной средой при развёртывании Django на рабочем сервере.

mod_python (http://www.djangoproject.com/r/mod_python/) — это модуль для Apache, позволяющий встроить Python в ядро Apache и загрузить программы на Python в память веб сервера при его запуске. Исполняемый код хранится в памяти до конца работы процесса Apache, что дает значительный прирост производительности.

Для работы Django необходим Apache версии 2.x и mod_python версии 3.x, в качестве МП-модуля рекомендуется использовать Prefork, а не Worker.

Замечание

Обсуждение настройки Apache выходит далеко за рамки данной книги, поэтому при необходимости мы просто будем делать ссылки на некоторые детали. В сети Интернет есть много хороших ресурсов для тех, кто хочет узнать больше об этом веб сервере. Вот некоторые из них, которые нравятся нам самим:

Документация по Apache в свободном онлайн доступе: http://www.djangoproject.com/r/apache/docs/;

Книга Pro Apache, Third Edition (Apress, 2004), автор Peter Wainwright: http://www.djangoproject.com/r/books/pro-apache/;

Книга Apache: The Definitive Guide, Third Edition (O’Reilly, 2002), авторы Ben Laurie и Peter Laurie: http://www.djangoproject.com/r/books/apache-pra/.

Базовые настройки

Чтобы настроить Django для использования mod_python, сперва необходимо убедиться, что установлен Apache с поддержкой mod_python. Обычно это означает, что в конфигурационном файле Apache присутствует соответствующая директива LoadModule:

LoadModule python_module /usr/lib/apache2/modules/mod_python.so

Теперь добавим в этот файл следующие строки:

<Location "/">
    SetHandler mod_python
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
    PythonDebug On
</Location>

Вместо mysite.settings необходимо вписать имя модуля настроек Django для вашего сайта.

Такая директива означает, что Apache должен обрабатывать все URL, начинающиеся от корня сайта, используя обработчик mod_python от Django. Он передает ему значение переменной DJANGO_SETTINGS_MODULE, чтобы mod_python знал, какие настройки использовать.

Отметим, что мы использовали директиву <Location>, а не <Directory>. Последняя из них указывает на путь в файловой системе, тогда как <Location> используется при формировании структуры URL для вебсайта. Использование <Directory> в таком случае является бессмысленным.

Обычно Apache запускается от имени специального пользователя, которое отличается от вашего логина в систему и имеет другие настройки path и sys.path. Поэтому необходимо указать для mod_python, где искать ваш проект и файлы Django:

PythonPath "['/путь/к/проекту', '/путь/к/django'] + sys.path"

Вы можете добавить дополнительные директивы, такие, как PythonAutoReload Off, для увеличения производительности. Полный список опций можно найти в документации по mod_python.

Рекомендуем отключить опцию PythonDebug (PythonDebug Off) для рабочей версии приложения. Если оставить ее включенной, ваши пользователи увидят некрасивый и избыточный стек ошибок Python, когда возникнут проблемы с mod_python.

Перегрузите Apache и теперь любой запрос к вашему сайту (либо виртуальному хосту, если вы разместили директиву внутри блока <VirtualHost>) будет обрабатываться Django.

Замечание

Если разворачивать Django в подкаталоге, то есть глубже корневого URL, то он не сможет обрезать префикс URL из шаблонов URL. Например, если настройки Apache похожи на эти:

<Location "/mysite/">
    SetHandler mod_python
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
    PythonDebug On
</Location>

то все шаблоны URL должны начинаться с префикса /mysite/. По этой причине мы обычно рекомендуем разворачивать Django в корень домена или виртуального хоста. Либо, как вариант, можно просто «сдвинуть» все URL вниз на один уровень, используя промежуточный файл со схемой URL:

urlpatterns = patterns('',
    (r'^mysite/', include('normal.root.urls')),
)

Несколько проектов Django на одном сервере Apache

Запустить несколько проектов Django на одном сервере Apache несложно. Это может понадобится, например, когда у независимого веб разработчика есть несколько клиентов и только один сервер.

Для реализации данного подхода используется блок VirtualHost:

NameVirtualHost *

<VirtualHost *>
    ServerName www.example.com
    # ...
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
</VirtualHost>

<VirtualHost *>
    ServerName www2.example.com
    # ...
    SetEnv DJANGO_SETTINGS_MODULE mysite.other_settings
</VirtualHost>

Если необходимо установить два различных проекта в рамках одного и того же виртуального хоста, нужно принять меры, чтобы не допустить смешивания двух различных кэш-копий кода приложений. Используем директиву PythonInterpreter, чтобы указать в каждой директиве <Location> свой обработчик:

<VirtualHost *>
    ServerName www.example.com
    # ...
    <Location "/something">
        SetEnv DJANGO_SETTINGS_MODULE mysite.settings
        PythonInterpreter mysite
    </Location>

    <Location "/otherthing">
        SetEnv DJANGO_SETTINGS_MODULE mysite.other_settings
        PythonInterpreter mysite_other
    </Location>
</VirtualHost>

Значения параметров для директивы PythonInterpreter можно установить любые, главное, чтобы они отличались между собой для различных блоков <Location>.

Запуск тестового сервера под mod_python

Посколько mod_python кэширует загруженный Python код, то при развертывании Django приложений нужно помнить о необходимости рестартовать Apache каждый раз, когда в исходный код внесены изменения. Это быстро может надоесть, поэтому мы предлагаем способ бысто обойти это ограничение: просто добавьте директиву MaxRequestsPerChild 1 в файл конфигурации, чтобы заставить Apache полностью перегружать все при каждом запросе. Только не нужно делать этого на рабочем сервере, иначе мы запретим вам использовать Django.

Если вы относитесь к тем программистам, кто отлаживает код, используя беспорядочно разбросанные операторы print (как мы), учтите, что оператор print не работает в случае использования mod_python и результат его работы не появится в логе Apache, как, возможно, кое-кто ожидал. Для того, чтобы печатать отладочную информацию, рекомендуем использовать стандартный Python модуль logging. Более подробную информацию можно посмотреть тут: http://docs.python.org/lib/module-logging.html. Альтернативным вариантом может быть вывод отладочной информации в шаблонах страниц сайта.

Работа с Django и статическим контентом на одном сервере Apache

Django не рекомендуется использовать для обслуживания запросов к статическим файлам (контенту); эту работу лучше оставить для веб сервера. Рекомендуется использовать отдельный веб сервер (на котором не работает Django) для обработки запросов к статической информации. Более подробная информация см. ниже в разделе "Масштабирование FIXME".

Однако, если нет других вариантов, кроме работы со статическим контентом на том же сервере Apache, где развернут Django, необходимо отключить mod_python для отдельных частей сайта:

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

Вместо /media/ необходимо указать корневой URL для статических файлов на вашем сайте.

Можно использовать директиву <LocationMatch> и работать через регулярные выражения. Например, вот такая настройка конфигурации позволяет использовать Django от корня сайта, но полностью отключает Django для подкаталога media, а также для любого URL, оканчивающегося на .jpg, .gif или .png:

<Location "/">
    SetHandler mod_python
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE mysite.settings
</Location>

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

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

Во всех случаях необходимо настроить директиву DocumentRoot, чтобы Apache знал, где ему искать статические файлы.

Обработка ошибок

При использовании Apache/mod_python ошибки перехватываются на уровне Django — другими словами, они не передаются на уровень Apache и поэтому не появляются в логе ошибок Apache.

Исключением тут будет случай, когда что-то серьёзно запорчено при установке Django. Тогда в броузере будет выдаваться страница с ошибкой «Internal Server Error» (код ошибки 500) и полный стек ошибки Python записывается в лог ошибок Apache. Такое сообщение занимает несколько строк. (Да, это выглядит некрасиво и неудобно для изучения, но так работает mod_python).

Устранение ошибок, приводящих к падению Apache

Иногда Apache падает после установки Django. Если это произошло, то практически всегда это результат одного из двух возможных случаев, не связанных непосредственно с Django:

Возможно, в приложении используется модуль pyexpat (применяется для разбора XML), который может конфликтовать с версией этого же модуля, встроенной непосредственно в Apache. Подробнее см. статью «Expat заставляет Apache сбоить» по адресу http://www.djangoproject.com/r/articles/expat-apache-crash/.

Возможно, в текущей конфигурации одновременно используются mod_python и mod_php на одном и том же сервере Apache, а в качестве базы данных выступает MySQL. В некоторых случаях это может привести к конфликту версий для драйверов MySQL между Python и PHP. Полная информация по данной проблеме доступна в mod_python FAQ по адресу http://www.djangoproject.com/r/articles/php-modpython-faq/.

Если после этого проблемы с mod_python продолжаются, следующим хорошим ходом будет создание «скелета» сайта под mod_python без подлючения библиотек Django. Это самый простой способ изолировать проблемы, специфические именно для mod_python. В статье «Заставляем mod_python работать» эта процедура детально описывается: http://www.djangoproject.com/r/articles/getting-modpython-working/.

На следующем этапе рекомендуем сделать тестовый скрипт и включить в него всё, что вы используете в Django приложении — представления, модели, шаблоны URL, конфигурацию RSS и т.п. Включите этот код в тестовую функцию обработчика и вызывайте тестовый URL. Если это приводит к проблеме, то считаем, что проблема где-то в коде, использующем Django. Комментируя одну за одной строки с import, мы доберемся до конкретного модуля, вызывающего ошибку. Далее то же самое проделываем с кодом модуля, пока не локализуем ошибку. Системные утилиты, такие, как ldconfig в Linux, otool в Mac OS и ListDLLs (от SysInternals) в Windows, помогут найти общие зависимости и возможные конфликты версий.

Использование Django с Apache и mod_wsgi

Источники: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide, http://ericholscher.com/blog/2008/jul/8/setting-django-and-mod_wsgi/.

Есть несколько причин для перехода с mod_python на mod_wsgi:

mod_python больше не поддерживается разработчиком;

mod_wsgi требует меньше ресурсов для своей работы и обладает большими возможностями в настройке.

Поддерживаемые версии Apache

Поддерживаются следующие версии Apache — 1.3, 2.0 и 2.2.

При использовании Apache 1.3 можно динамически подгружать его модули. В случае, когда модули статически слинкованы с Apache, некоторые компоненты mod_wsgi будут работать некорректно.

При использовании Apache 2.0 и 2.2 нет разницы, работаете вы с prefork или worker MPM.

Чтобы иметь возможность mod_wsgi в режиме сервиса, надо использовать Apache 2.0 и 2.2, также соответствующие библиотеки Apache должны быть собраны с поддержкой потоков.

На Linux, если Apache устанавливается из репозитория, следует установить для него пакеты разработчика.

Для большинства дистрибутивов Linux, пакет разработчика для Apache 2.x называется apache2-dev, сам Apache находится в пакете apache2. Тем не менее, некоторые дистрибутивы различают пакеты разработчика по типу MPM, для которого они собраны. В таком случае, пакет может называться как apache2-worker-dev или apache2-prefork-dev. При использовании Apache 2.x не стоит устанавливать пакет apache-dev, так как он относится к Apache 1.3.

Поддерживаемые версии Python

Можно использовать Python версий 2.3-2.5. Используемая версия Python должна быть собрана с поддержкой потоков.

На Linux, если Python устанавливается из репозитария, следует установить для него пакеты разработчика.

Желательно, чтобы Python был доступен в виде разделяемой библиотеки. Если это не ваш случай, то mod_wsgi будет требовать больше памяти и могут возникнуть конфликты с mod_python (если оба модуля будут загружены в одно время), что может привести к падению Apache.

Установка модуля

Пример установки модуля на Ubuntu:

# wajig search mod_wsgi
libapache2-mod-wsgi - Python WSGI adapter module for Apache

# wajig install libapache2-mod-wsgi
Чтение списков пакетов... Готово
Построение дерева зависимостей
Чтение информации о состоянии... Готово
Предлагаемые пакеты:
  apache2-mpm-worker apache2-mpm-event
НОВЫЕ пакеты, которые будут установлены:
  libapache2-mod-wsgi
Необходимо скачать 113kБ архивов.
Получено:1 http://ru.archive.ubuntu.com intrepid/universe libapache2-mod-wsgi 2.3-1 [113kB]
Получено 113kБ за 1s (111kБ/c)
...
 * Reloading web server config apache2
#

Интерфейс к модулю

Создайте внутри вашего проекта каталог, а в каталоге файл django.wsgi. Например, в моём случае это будет так:

$ cd ~/development/cargo
$ mkdir apache
$ emacs apache/django.wsgi

Скопируйте нижеприведённый код в файл django.wsgi:

import os, sys
# место, где лежит джанго
sys.path.append('/home/rad/devel/django-trunk/')
# место, где лежит проект
sys.path.append('/home/rad/devel/')
# файл конфигурации проекта
os.environ['DJANGO_SETTINGS_MODULE'] = 'cargo.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

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

Конфигурация сервера

Откройте конфигурационный файл вашего сервер, например, /etc/apache2/sites-available/cargo.caml.ru и внесите в него изменения:

<VirtualHost *>
        DocumentRoot /var/www-cargo/
        ServerAdmin ruslаn.pоpоv@gmаil.cоm
        ServerName cargo

        WSGIScriptAlias / /home/rad/devel/cargo/apache/django.wsgi
        WSGIDaemonProcess cargo processes=2 maximum-requests=5 threads=1
        WSGIProcessGroup cargo

        ErrorLog "|/usr/sbin/rotatelogs /var/log/apache2/cargo.error.%Y-%m-%d.log 86400"
        CustomLog "|/usr/sbin/rotatelogs /var/log/apache2/cargo.access.%Y-%m-%d.log 86400" combined
        ServerSignature On

        Alias /css/     "/home/rad/django/cargo/css/"
        Alias /js/      "/home/rad/django/cargo/js/"
        Alias /pics/    "/home/rad/django/cargo/pics/"
        Alias /djangobook/pics/ "/home/rad/django/cargo/djangobook/pics/"
        Alias /media/   "/home/rad/django/cargo/media/"
        Alias /adminmedia/ "/home/rad/devel/django-trunk/django/contrib/admin/media/"
</VirtualHost>

Особое внимание надо уделить трём строкам, которые начинаются с WSGI.... Строка с WSGIScriptAlias ссылается на файл, который был создан ранее. Другие две строки необходимы в случае, когда вы размещаете на сервере несколько виртуальных сайтов.

Замечание

Следует принять во внимание то, что директива WSGIDaemonProcess и соответствующие элементы не доступны под ОС MS Windows, а также при использовании Apache 1.3. Подробности...

А самое главное, это не забыть проверить, что в Apache подгружается mod_wsgi:

# ls -l /etc/apache2/mods-enabled/ | grep wsgi
lrwxrwxrwx 1 root root 27 2009-02-18 22:41 wsgi.conf -> ../mods-available/wsgi.conf
lrwxrwxrwx 1 root root 27 2009-02-18 22:41 wsgi.load -> ../mods-available/wsgi.load

Приложение A. Учебные примеры

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение B. Справочник определений модели

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение C. Справочник по API взаимодействия с базой данных

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение D. Справочник по базовым представлениям

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение E. Параметры конфигурации

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение F. Встроенные шаблонные теги и фильтры

Перевод © Константин Хлызов <kostik.vento • gmail>

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

Справочник по стандартным тегам

autoescape

Добавлен в версии 1.0.

Управляет поведением встроенного механизма автоматического экранирования символов (autoescape). Принимает в качестве аргумента одно из значений on или off (соответственно включить либо выключить экранирование внутри данного блока).

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

Действие этого тега не распространяется на переменные, отмеченные как «безопасные» для экранирования либо кодом, который присвоил значение этой переменной, либо посредством применения фильтров safe или escape.

block

Определяет блок, который может быть переопределен шаблонами-потомками. Подробнее см. «Наследование шаблонов».

comment

Предписывает шаблонному процессору игнорировать все, что заключено между тегами {% comment %} и {% endcomment %}.

csrf_token

Добавлен в версии 1.1.2.

В версиях Джанго 1.1.X — тег-заглушка, возвращающий пустую строку для совместимости с будущими версиями. В версиях Джанго 1.2 и более поздних используется для защиты от подделки HTTP-запросов (см. документацию в разделе «Cross Site Request Forgeries»).

cycle

Добавлен в версии 1.0.

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

Внутри цикла каждый раз использует следующую из переданных строк:

{% for o in some_list %}
  <tr class="{% cycle 'row1' 'row2' %}">
    ...
  </tr>
{% endfor %}

Также можно использовать переменные. Например, если у вас есть две шаблонные переменные, rowvalue1 и rowvalue2, вы можете по очереди использовать их значения:

{% for o in some_list %}
  <tr class="{% cycle rowvalue1 rowvalue2 %}">
    ...
  </tr>
{% endfor %}

Да, в одном теге можно использовать и строки, и переменные:

{% for o in some_list %}
  <tr class="{% cycle 'row1' rowvalue2 'row3' %}">
    ...
  </tr>
{% endfor %}

В некоторых случаях вам может понадобиться использовать очередное значение циклической переменной вне цикла. Для этого требуется всего лишь дать имя тегу {% cycle %}, используя слово «as» следующим образом:

{% cycle 'row1' 'row2' as rowcolors %}

После такого объявления можно вставлять текущее значение цикла в любом месте шаблона:

<tr class="{% cycle rowcolors %}">...</tr>
<tr class="{% cycle rowcolors %}">...</tr>

В теге {% cycle %} можно указывать любое количество значений, разделенных пробелами. Значения, заключенные в одинарные (') или двойные (") кавычки, считаются строковыми литералами, а значения без кавычек — шаблонными переменными.

Обратите внимание, что переменные, переданные в цикл, при выводе не экранируются, поскольку по умолчанию теги не экранируют свое содержимое. Любой код (HTML или Javascript), переданный в переменной, будет выведен «как есть», что потенциально может привести к уязвимостям.

Если вам требуется экранировать переменные в цикле, вы должны явно это указать:

{% filter force_escape %}
  {% cycle var1 var2 var3 %}
{% endfilter %}

Для обратной совместимости тег {% cycle %} поддерживает устаревший ограниченный синтаксис из предыдущих версий Джанго. Не используйте старый синтаксис в своих новых проектах и на всякий случай посмотрите, как это было:

{% cycle row1,row2,row3 %}

В этом случае каждое значение интерпретируется как строковый литерал и вы никак не можете использовать переменные. И запятые внутри строк тоже не можете. Как и пробелы. Мы уже сказали вам не использовать этот синтаксис в новых проектах?

debug

Выводит полный набор отладочной информации, включая текущий контекст и импортированные модули.

extends

Сообщает, что данный шаблон является расширением родительского шаблона.

Используется двумя способами:

{% extends "base.html" %} (с кавычками) использует постоянное значение "base.html" в качестве имени расширяемого родительского шаблона.

{% extends variable %} использует значение переменной variable. Если значение является строкой, Джанго использует ее в качестве имени родительского шаблона. Если значение является объектом шаблона Template, в качестве родительского шаблона Джанго использует этот объект.

Подробнее см. «Наследование шаблонов».

filter

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

Фильтры могут быть применены последовательно друг за другом и им могут быть переданы аргументы — так же, как в синтаксисе переменных.

Пример использования:

{% filter force_escape|lower %}
  Этот текст будет экранирован (для HTML) и приведен к нижнему регистру.
{% endfilter %}

firstof

Выводит первую переменную из переданных, которая не приводится к False. Не экранирует вывод. Если все переменные приводятся к False, не выводит ничего.

Пример использования:

{% firstof var1 var2 var3 %}

Это эквивалентно следующему коду:

{% if var1 %}
  {{ var1|safe }}
{% else %}{% if var2 %}
  {{ var2|safe }}
{% else %}{% if var3 %}
  {{ var3|safe }}
{% endif %}{% endif %}{% endif %}

Допускается использование строкового литерала в качестве запасного значения, если все переданные переменные приведутся к False:

{% firstof var1 var2 var3 "запасное значение" %}

Обратите внимание, что переменные, указанные в теге, при выводе не экранируются, поскольку теги сами по себе не экранируют свое содержимое. Любой код (HTML или Javascript), переданный в переменной, будет выведен как есть, что потенциально может привести к уязвимостям.

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

{% filter force_escape %}
  {% firstof var1 var2 var3 "запасное значение" %}
{% endfilter %}

for

Организует цикл по всем элементам переданного массива (итератора). Например, выведем данные об атлетах, перечисленных в списке athlete_list:

<ul>
  {% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
  {% endfor %}
</ul>

Можно указать обратный порядок прохождения массива, используя конструкцию {% for obj in list reversed %}.

Добавлено в версии 1.0.

Если элементами перебираемого списка являются тоже списки, их можно распаковывать в отдельные переменные. Допустим, каждый элемент списка представляет собой пару координат (x,y), описывающих точки, тогда вы можете вывести список этих точек следующим образом:

{% for x, y in points %}
  Имеется точка с координатами {{ x }},{{ y }}
{% endfor %}

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

{% for key, value in data.items %}
  {{ key }}: {{ value }}
{% endfor %}

Цикл for определяет несколько переменных, доступных внутри цикла:

Таблица F.1. Переменные цикла

Переменная | Описание
forloop.counter | Номер текущей итерации цикла (начиная с 1).
forloop.counter0 | Номер текущей итерации цикла (начиная с 0).
forloop.revcounter | Число итераций цикла, оставшихся до окончания перебора (завершая единицей).
forloop.revcounter0 | Число итераций цикла, оставшихся до окончания перебора (завершая нулем).
forloop.first | True, если это первая итерация цикла.
forloop.last | True, если это последняя итерация цикла.
forloop.parentloop | Для вложенных циклов указывает на родительский цикл текущего цикла.

for ... empty

Добавлено с 1.1.

Тег for может содержать необязательную ветвь {% empty %}, которая будет выведена, если указанный массив пуст либо не найден:

<ul>
  {% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
  {% empty %}
    <li>Извините, в списке нет атлетов.</li>
  {% endfor %}
<ul>

Этот код делает то же самое, что и нижеприведенный, однако он короче, проще и порой быстрее:

<ul>
  {% if athlete_list %}
    {% for athlete in athlete_list %}
      <li>{{ athlete.name }}</li>
    {% endfor %}
  {% else %}
    <li>Извините, в списке нет атлетов.</li>
  {% endif %}
</ul>

if

Тег {% if %} вычисляет значение переменной и, если эта переменная приводится к True (т.е. существует, не является пустой и не равна логическому значению false), выводит содержимое блока между {% if %} и {% endif %}:

{% if athlete_list %}
  Число атлетов: {{ athlete_list|length }}
{% else %}
  Атлетов нет.
{% endif %}

В описанном случае, если массив athlete_list не пуст, переменная {{ athlete_list|length }} выведет число атлетов (длину массива).

Как вы уже увидели, тег if может иметь необязательную ветвь {% else %}, которая будет выведена, если условие не соблюдено.

Логические операторы

В тегах if можно использовать and или or для проверки нескольких переменных и not для отрицания данной переменной:

{% if athlete_list and coach_list %}
  Есть и атлеты, и тренеры.
{% endif %}

{% if not athlete_list %}
  Атлетов нет.
{% endif %}

{% if athlete_list or coach_list %}
  Кто-то из атлетов или тренеров точно есть.
{% endif %}

{% if not athlete_list or coach_list %}
  То ли нет атлетов, то ли есть тренеры (ну да, перевод
  с логического на русский выглядит довольно глупо,
  но мы не виноваты).
{% endif %}

{% if athlete_list and not coach_list %}
  Есть несколько атлетов и ни одного тренера.
{% endif %}

Добавлено с 1.2.

Внутри одного тега допускается использование одновременно условий and и or, при этом and имеет больший приоритет, чем or, например:

{% if athlete_list and coach_list or cheerleader_list %}

обрабатывается как:

if (athlete_list and coach_list) or cheerleader_list

Явное использование скобок в теге if не допускается. Для указания порядка действий используйте вложенные теги if.

В тегах if также допускаются операторы ==, !=, <, >, <=, >= и in, которые работают следующим образом:

Оператор ==

Равенство. Пример:

{% if somevar == "x" %}
  Это предложение будет выведено, если переменная somevar равна строке "x"
{% endif %}

Оператор !=

Неравенство. Пример:

{% if somevar != "x" %}
  Это предложение будет выведено, если переменная somevar не равна строке "x"
  или вообще не найдена в контексте.
{% endif %}

Оператор <

Меньше. Пример:

{% if somevar < 100 %}
  Это предложение будет выведено, если переменная somevar меньше 100.
{% endif %}

Оператор >

Больше. Пример:

{% if somevar > 0 %}
  Это предложение будет выведено, если переменная somevar больше 0.
{% endif %}

Оператор <=

Меньше или равно. Пример:

{% if somevar <= 100 %}
  Это предложение будет выведено, если переменная somevar меньше или равна 100.
{% endif %}

Оператор >=

Больше или равно. Пример:

{% if somevar >= 1 %}
  Это предложение будет выведено, если переменная somevar больше или равна 1.
{% endif %}

Оператор in

Содержится в. Этот оператор поддерживается многими контейнерами Python для проверки принадлежности данного значения данному контейнеру. Далее приводятся несколько примеров интерпретации выражения x in y:

{% if "bc" in "abcdef" %}
  Это предложение выводится, поскольку "bc" является подстрокой "abcdef"
{% endif %}

{% if "hello" in greetings %}
  Если greetings представляет собой список или множество, одним из
  элементов которого является "hello", это предложение будет выведено.
{% endif %}

{% if user in users %}
  Если users представляет собой множество типа QuerySet, это предложение
  будет выведено, если экземпляр user входит в это множество.
{% endif %}

Оператор not in

Не содержится в. Отрицание оператора in.

Операторы сравнения нельзя записывать цепочкой, как в Python или в математической записи. Например, вместо записи:

{% if a > b > c %}  (WRONG)

следует писать:

{% if a > b and b > c %}

Фильтры

В выражении if можно использовать фильтры, например:

{% if messages|length >= 100 %}
  Сегодня у вас туча сообщений!
{% endif %}

Составные выражения

Все вышеперечисленные операторы можно объединять в составные выражения. В этом случае важно учитывать порядок действий выражений (даны в порядке возрастания приоритета):

or

and

not

in

==, !=, <, >,<=, >=

(Точно так же, как и в Python). Таким образом, тег if в следующем примере:

		   {% if a == b or c == d and e %}

... будет интерпретирован как:

		   (a == b) or ((c == d) and e)

Если вам нужен другой порядок действий, пишите вложенные теги. Порой для ясности лучше так и делать (во имя тех, кто незнаком с порядком действий по умолчанию).

ifchanged

Проверяет, изменилось ли значение переменной на очередной итерации цикла.

Блочный тег ifchanged используется внутри циклов и имеет два варианта применения.

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

<h1>Архив за {{ year }} год</h1>

{% for date in days %}
  {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
  <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
{% endfor %}

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

{% for date in days %}
  {% ifchanged date.date %} {{ date.date }} {% endifchanged %}
  {% ifchanged date.hour date.date %}
    {{ date.hour }}
  {% endifchanged %}
{% endfor %}

Тег ifchanged также может иметь необязательную ветвь {% else %}, которая будет выведена, если значение не изменилось:

{% for match in matches %}
  <div style="background-color:
    {% ifchanged match.ballot_id %}
      {% cycle "red" "blue" %}
    {% else %}
      grey
    {% endifchanged %}
  ">{{ match }}</div>
{% endfor %}

ifequal

Выводит содержимое блока, если два переданных аргумента равны.

Пример:

{% ifequal user.id comment.user_id %}
  ...
{% endifequal %}

Как и с тегом {% if %} , можно использовать необязательную ветвь {% else %}.

Аргументы могут быть строковыми константами, то есть возможен такой вариант:

{% ifequal user.username "adrian" %}
  ...
{% endifequal %}

Аргументы сравниваются только с переменными шаблона или со строковыми константами. Нельзя проверить переменную на равенство с Python-объектами типа True или False. Если вам нужно проверить истинность или ложность переменной, лучше примените для этого тег if.

Добавлено с 1.2.

Альтернативой тегу ifequal является использование тега if с оператором ==.

ifnotequal

То же самое, что и ifequal, только проверяет аргументы на неравенство.

Добавлено с 1.2.

Альтернативой тегу ifnotequal является использование тега if с оператором !=.

include

Загружает шаблон и обрабатывает его в текущем контексте. Таким способом можно «включать» другие шаблоны в данный шаблон.

Имя шаблона может быть как переменной, так и строковой константой (в одинарных или двойных кавычках).

В следующем примере подключается содержимое шаблона "foo/bar.html":

{% include "foo/bar.html" %}

А в этом примере подключается содержимое шаблона, имя которого содержит переменная template_name:

{% include template_name %}

Включенный шаблон обрабатывается с учетом контекста шаблона, в который он включен. Следующий пример выводит "Hello, John":

Контекст: переменная person содержит значение "john".

Основной шаблон:

{% include "name_snippet.html" %}

Включаемый шаблон name_snippet.html:

Hello, {{ person }}

См. также: {% ssi %}.

load

Загружает набор пользовательских тегов.

Подробнее см. «Библиотеки пользовательских тегов и фильтров».

now

Выводит текущую дату, отформатированную в соответствии с указанной строкой.

Использует несколько расширенный формат PHP-функции date() (http://php.net/date).

Пример:

Сейчас {% now "jS F Y H:i" %}

Обратите внимание на необходимость экранировать буквы, входящие в набор символов формата, если они являются частью текста. В примере ниже буква «f» экранирована обратным слэшем, поскольку иначе будет восприниматься как символ формата, выводящий время. Букву «o» экранировать не требуется, поскольку она не относится к символам форматирования:

It is the {% now "jS o\f F" %}

Такой код выведет:

It is the 4th of September

regroup

Группирует список похожих элементов по одинаковому признаку.

Действие этого составного тега проще объяснить на примере: допустим, переменная people является списком людей, каждый из которых представлен словарем с ключами first_name, last_name и gender:

people = [
  {'first_name': 'George', 'last_name': 'Bush', 'gender': 'Male'},
  {'first_name': 'Bill', 'last_name': 'Clinton', 'gender': 'Male'},
  {'first_name': 'Margaret', 'last_name': 'Thatcher', 'gender': 'Female'},
  {'first_name': 'Condoleezza', 'last_name': 'Rice', 'gender': 'Female'},
  {'first_name': 'Pat', 'last_name': 'Smith', 'gender': 'Unknown'},
]

... и вы хотите вывести двухуровневый список, сгруппировав людей по полу, например, вот так:

* Male:
* George Bush
* Bill Clinton
* Female:
* Margaret Thatcher
* Condoleezza Rice
* Unknown:
* Pat Smith

Это можно сделать при помощи тега {% regroup %}. Код шаблона для этой операции показан ниже:

{% regroup people by gender as gender_list %}

<ul>
  {% for gender in gender_list %}
    <li>{{ gender.grouper }}
      <ul>
        {% for item in gender.list %}
          <li>{{ item.first_name }} {{ item.last_name }}</li>
        {% endfor %}
      </ul>
    </li>
  {% endfor %}
</ul>

Рассмотрим этот пример подробнее. Тег {% regroup %} принимает три аргумента: список для группировки, признак, по которому нужно сгруппировать элементы, и имя полученного списка. В данном случае мы группируем список people по атрибуту gender и полученный результат называем gender_list.

Результатом работы {% regroup %} является список (в данном случае — gender_list) групповых объектов. У каждого группового объекта есть два атрибута:

grouper — значение признака, по которому была произведена группировка (например, строка «Male» или «Female»).

list — список всех элементов в этой группе (в данном случае, список всех людей с полом «Male»).

Имейте в виду, что {% regroup %} не упорядочивает обрабатываемый список! В нашем примере мы предполагаем, что список people был изначально упорядочен по атрибуту gender. Если элементы списка people не упорядочены по атрибуту gender, перегруппировка простодушно покажет несколько групп для одного и того же пола. Допустим, что наш список people изначально был вот таким (заметьте, что в этом списке мужчины не находятся рядом):

people = [
  {'first_name': 'Bill', 'last_name': 'Clinton', 'gender': 'Male'},
  {'first_name': 'Pat', 'last_name': 'Smith', 'gender': 'Unknown'},
  {'first_name': 'Margaret', 'last_name': 'Thatcher', 'gender': 'Female'},
  {'first_name': 'George', 'last_name': 'Bush', 'gender': 'Male'},
  {'first_name': 'Condoleezza', 'last_name': 'Rice', 'gender': 'Female'},
]

При таком значении переменной people, вышеприведенный пример шаблона с тегом {% regroup %} выдаст следующее:

* Male:
* Bill Clinton
* Unknown:
* Pat Smith
* Female:
* Margaret Thatcher
* Male:
* George Bush
* Female:
* Condoleezza Rice

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

Если нужные данные представлены в виде списка словарей, то можно отсортировать данные прямо в шаблоне, применив фильтр dictsort:

{% regroup people|dictsort:"gender" by gender as gender_list %}

spaceless

Удаляет пробельные символы между HTML-тегами, в т.ч. символы табуляции и новой строки.

Пример использования:

{% spaceless %}
  <p>
    <a href="foo/">Foo</a>
  </p>
{% endspaceless %}

Этот шаблон выведет следующий HTML-код:

<p><a href="foo/">Foo</a></p>

Удаляются только пробелы между тегами, пробелы между тегами и текстом не удаляются. В следующем примере, пробелы вокруг слова Hello останутся нетронутыми:

{% spaceless %}
  <strong>
    Hello
  </strong>
{% endspaceless %}

ssi

Выводит на страницу содержимое указанного файла.

Подобно тегу include, {% ssi %} включает в данную страницу содержимое другого файла (указанный путь к файлу должен быть абсолютным):

{% ssi /home/html/ljworld.com/includes/right_generic.html %}

Если указан необязательный параметр parsed, содержимое указанного файла обрабатывается как шаблон с использованием текущего контекста:

{% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}

Перед применением тега {% ssi %} в целях безопасности необходимо определить константу ALLOWED_INCLUDE_ROOTS в настройках Джанго.

См. также тег {% include %}.

templatetag

Выводит одну из последовательностей символов, используемых для указания тегов в шаблоне.

Поскольку в шаблонной системе Джанго не предусмотрено понятие «экранирования», то для вывода последовательности символов, используемых для указания тегов в шаблоне, нужно использовать тег {% templatetag %}.

Аргумент тега указывает, какую именно последовательность символов нужно вывести:

Таблица F.2. Список аргументов

Аргумент | Вывод
openblock | {%
closeblock | %}
openvariable | {{
closevariable | }}
openbrace | {
closebrace | }
opencomment | {#
closecomment | #}

url

Возвращает абсолютный URL (т.е. URL без имени домена), соответствующий указанной функции представления и необязательным параметрам. Использование этого тега позволяет выводить ссылки, не нарушая принцип DRY указанием конкретных ссылок в шаблонах:

{% url path.to.some_view v1 v2 %}

Первый аргумент тега — путь к функции представления в виде package.package.module.function. Остальные аргументы необязательны и представляют собой разделенные пробелами значения аргументов для указанного URL. Пример выше показывает передачу позиционных аргументов функции. Можно также передавать именованные аргументы:

{% url path.to.some_view arg1=v1 arg2=v2 %}

Не используйте позиционные и именованные аргументы в одном вызове. Обязательно указывайте все аргументы, требуемые в схеме URL.

Рассмотрим пример. Допустим, у вас есть функция представления app_views.client, схема URL которой принимает ID клиента (в данном случае, client() — это метод в файле представлений app_views.py). Соответствующая строка в схеме URL может выглядеть так:

('^client/(\d+)/$', 'app_views.client')

Если схема URL данного приложения включена в схему URL всего проекта подобным образом:

('^clients/', include('project_name.app_name.urls'))

...то в шаблоне вы можете указать ссылку на это представление вот так:

{% url app_views.client client.id %}

Тег шаблона выведет строку /clients/client/123/.

Добавлено с 1.0.

В случае использования именованных URL-паттернов вместо указания пути к функции представления достаточно сослаться на имя паттерна в теге url.

Имейте в виду, что если URL, который вы пытаетесь вычислить, не существует, Джанго вызовет исключение NoReverseMatch, что приведет к отображению страницы ошибки вашего сайта.

Если вам требуется получить URL, не отображая его, немного измените вызов тега:

{% url path.to.view arg arg2 as the_url %}

<a href="{{ the_url }}">Я ссылаюсь на {{ the_url }}</a>

Синтаксис {% url ... as var %} не вызывает ошибку, если искомое представление отсутствует. На практике можно использовать такой вызов для ссылки на необязательные представления:

{% url path.to.view as the_url %}
{% if the_url %}
  <a href="{{ the_url }}">Ссылка на что-нибудь необязательное</a>
{% endif %}

Добавлено с 1.1.

Если вам нужно получить URL в определенном пространстве имен, укажите полное имя представления:

{% url myapp:view-name %}

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

Добавлено с 1.2.

Для обратной совместимости тег {% url %} поддерживает использование запятой для разделения аргументов. Не используйте этот синтаксис в новых проектах и на всякий случай посмотрите, как это выглядело:

{% url path.to.view arg,arg2 %}
{% url path.to.view arg, arg2 %}

В этом случае нельзя использовать внутри строковых аргументов запятые и знаки равенства. Мы уже сказали вам не использовать этот синтаксис в новых проектах?

widthratio

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

Пример:

<img src="bar.gif" height="10" width="{% widthratio this_value max_value 100 %}" />

Если this_value равно 175, а max_value равно 200, то ширина рисунка «bar.gif» будет 88 точек (поскольку 175/200 = .875; .875 * 100 = 87.5, округленно 88).

with

Добавлено с 1.0.

Кэширует составную переменную под отдельным именем. Полезно в случаях многократного обращения к «дорогостоящему» методу (использующему вычислительные ресурсы, например, доступ к базе данных).

Пример:

{% with business.employees.count as total %}
  {{ total }} employee{{ total|pluralize }}
{% endwith %}

Переменная, получившая значение (в данном примере total), доступна только между тегами {% with %} и {% endwith %}.

Справочник по стандартным фильтрам

add

Добавляет аргумент к обрабатываемому значению.

Например:

{{ value|add:"2" }}

Если value равно 4, результатом действия фильтра будет 6.

Добавлено с 1.2.

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

Например, если мы применим фильтр:

{{ first|add:second }}

где first содержит список [1, 2, 3], а second — список [4, 5, 6], результатом будет список [1, 2, 3, 4, 5, 6].

Замечание

Имейте в виду, что если оба значения представляют собой строки, которые приводятся к целым числам, то будет выполнено сложение, а не конкатенация (см. первый пример).

addslashes

Добавляет слэши перед кавычками. В частности, полезно для экранирования строк в формате CSV.

Пример:

{{ value|addslashes }}

Если value равно I'm using Django, фильтр выведет I\'m using Django.

capfirst

Делает первый символ переменной заглавным.

Например:

{{ value|capfirst }}

Если value равно django, фильтр выведет Django.

center

Центрирует значение в поле данной ширины.

Например:

"{{ value|center:"15" }}"

Если value равно "Django", результатом действия фильтра будет " Django ".

cut

Удаляет все вхождения переданного аргумента из обрабатываемого значения.

Например:

{{ value|cut:" "}}

Если value равно "String with spaces", фильтр выведет "Stringwithspaces".

date

Форматирует дату в заданном формате.

Формат может быть задан предопределенными константами: DATE_FORMAT, DATETIME_FORMAT, SHORT_DATE_FORMAT либо SHORT_DATETIME_FORMAT, а также строкой формата (см. тег now ). Имейте в виду, что действие предопределенных констант может зависить от локали.

Например:

{{ value|date:"D d M Y" }}

Если value является объектом datetime (т.е. результатом функции datetime.datetime.now()), фильтр выведет 'Wed 09 Jan 2008'.

Другой пример:

Допустим, параметр USE_L10N равен True, а LANGUAGE_CODE, например, "es", тогда результатом действия фильтра:

{{ value|date:"SHORT_DATE_FORMAT" }}

будет строка "09/01/2008" (т.к. "SHORT_DATE_FORMAT" в локали es использует строку формата "d/m/Y").

Если формат не указан:

{{ value|date }}

то будет использована строка формата, определенная в параметре DATE_FORMAT, без учета локализации.

Добавлено в 1.2.

Текущая локаль теперь влияет на предопределенные форматы.

default

Если значение вычисляется как False, выводит указанный аргумент как значение по умолчанию. В противном случае выводит само значение.

Например:

{{ value|default:"nothing" }}

Если value равно "" (пустая строка), фильтр выведет nothing.

default_if_none

Если (и только если) значение равно None, выводит указанный аргумент как значение по умолчанию. В противном случае выводит само значение.

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

Например:

{{ value|default_if_none:"nothing" }}

Если value равно None, фильтр выведет строку "nothing".

dictsort

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

Например:

{{ value|dictsort:"name" }}

Если value является списком:

[
  {'name': 'zed', 'age': 19},
  {'name': 'amy', 'age': 22},
  {'name': 'joe', 'age': 31},
]

то такой фильтр вернет список:

[
  {'name': 'amy', 'age': 22},
  {'name': 'joe', 'age': 31},
  {'name': 'zed', 'age': 19},
]

dictsortreversed

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

divisibleby

Возвращает True, если значение делится на указанный аргумент.

Например:

{{ value|divisibleby:"3" }}

Если value равно 21, фильтр выведет True.

escape

Заменяет символы, используемые в HTML, на соответствующие HTML-сущности, а именно:

< заменяется на &lt;;

> заменяется на &gt;;

' (одинарная кавычка) заменяется на &#39;;

" (двойная кавычка) заменяется на &quot;;

& заменяется на &amp;.

Замена производится непосредственно перед выводом результата, поэтому положение escape в цепочке фильтров не имеет значения: он всегда будет применен последним. Если порядок применения этого фильтра важен, используйте фильтр force_escape.

Применение фильтра escape к переменной, которая будет экранирована автоматически, в любом случае приведет к однократному экранированию. Таким образом, применение этого фильтра безопасно. Если требуется неоднократная замена HTML-символов, используйте фильтр force_escape.

Изменено в 1.0.

В связи с автоматическим экранированием поведение этого фильтра несколько изменено. Замены осуществляются только один раз, после применения всех имеющихся фильтров (расположенных как до, так и после данного фильтра).

escapejs

Добавлено в 1.0.

Экранирует символы для использования в JavaScript-строках. Этот фильтр не делает строку безопасной для HTML, а лишь защищает от синтаксических ошибок при генерации кода JavaScript/JSON.

Например:

{{ value|escapejs }}

Если value равно "testing\r\njavascript \'string" <b>escaping</b>", в результате получится "testing\\u000D\\u000Ajavascript \\u0027string\\u0022 \\u003Cb\\u003Eescaping\\u003C/b\\u003E".

filesizeformat

Форматирует значение в виде «человекопонятного» размера файла (т.е. 13 KB, 4.1 MB, 102 bytes, и т.п.).

Например:

{{ value|filesizeformat }}

Если value равно 123456789, фильтр выведет 117.7 MB.

first

Возвращает первый элемент списка.

Например:

{{ value|first }}

Если value содержит список ['a', 'b', 'c'], результатом действия фильтра будет 'a'.

fix_ampersands

Изменено с 1.0.

Используется редко, т.к. амперсанды экранируются автоматически. Подробнее см. escape .

Заменяет амперсанды на сущности &amp;.

Например:

{{ value|fix_ampersands }}

Если value равно Tom & Jerry, будет выведеноTom &amp; Jerry.

floatformat

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

Таблица F.3. Без аргументов

value | Шаблон | Вывод
34.23234 | {{ value|floatformat }} | 34.2
34.00000 | {{ value|floatformat }} | 34
34.26000 | {{ value|floatformat }} | 34.3

Если указан целочисленный положительный аргумент, floatformat округляет число до указанного числа знаков после запятой и выводит их все. Например:

Таблица F.4. Положительный аргумент

value | Шаблон | Вывод
34.23234 | {{ value|floatformat:3 }} | 34.232
34.00000 | {{ value|floatformat:3 }} | 34.000
34.26000 | {{ value|floatformat:3 }} | 34.260

Если аргумент отрицательный, то floatformat округляет число до указанного числа знаков после запятой и выводит их все, если имеется дробная часть. Например:

Таблица F.5. Отрицательный аргумент

value | Шаблон | Вывод
34.23234 | {{ value|floatformat:-3 }} | 34.232
34.00000 | {{ value|floatformat:-3 }} | 34
34.26000 | {{ value|floatformat:-3 }} | 34.260

Вызов floatformat без аргумента эквивалентен вызову с аргументом -1.

force_escape

Добавлено с 1.0.

Экранирует HTML-символы в строке (подробнее см. описание фильтра escape). Применяется немедленно и возвращает новую, экранированную строку, что полезно в тех редких случаях, кога требуется неоднократное экранирование или применение ряда фильтров к уже экранированной строке. Обычно же достаточно фильтра escape.

get_digit

Принимая аргумент-число, возвращает цифру соответствующего порядка: последнюю цифру, если аргумент равен 1, предпоследнюю, если 2 и т.д. Если значение или аргумент не являются целыми числами или аргумент меньше 1, не производит никакого эффекта.

Например:

{{ value|get_digit:"2" }}

Если value равно 123456789, фильтр вернет 8.

iriencode

Преобразует IRI (Internationalized Resource Identifier) в строку, которую можно включить в URL. Это полезно в случае, если в предполагаемом адресе содержатся символы, не входящие в набор ASCII.

Применение этого фильтра для строк, пропущенных через фильтр urlencode, безопасно.

Например:

{{ value|iriencode }}

Если value равно "?test=1&me=2", результат будет "?test=1&amp;me=2".

join

Соединяет элементы списка в строку, используя аргумент как разделитель (аналогично Python-функции str.join(list)).

Например:

{{ value|join:" // " }}

Если value является списком ['a', 'b', 'c'], фильтр выведет строку "a // b // c".

last

Добавлено в 1.0.

Возвращает последний элемент списка.

Например:

{{ value|last }}

Если value является списком ['a', 'b', 'c', 'd'], фильтр выведет строку "d".

length

Возвращает длину переменной. Работает для как для строк, так и для списков.

Например:

{{ value|length }}

Если value является списком ['a', 'b', 'c', 'd'], фильтр выведет 4.

length_is

Возвращает True, если длина значения равна указанному аргументу, в противном случае возвращает False.

Например:

{{ value|length_is:"4" }}

Если value является списком ['a', 'b', 'c', 'd'], фильтр выведет True.

linebreaks

Заменяет переносы строк в простом тексте на соответствующий HTML; новая строка заменяется на тег <br />, а новая строка с последующей пустой строкой — тегом </p>.

Например:

{{ value|linebreaks }}

Если value равно Joel\nis a slug, фильтр выведет <p>Joel<br />is a slug</p>.

linebreaksbr

Преобразует все переносы строк в тег <br />.

Например:

{{ value|linebreaksbr }}

Если value равно Joel\nis a slug, фильтр выведет Joel<br />is a slug.

linenumbers

Выводит текст с номерами строк.

Например:

{{ value|linenumbers }}

Если value равно:

one
two
three

фильтр выведет:

1. one
2. two
3. three

ljust

Выравнивает текст влево в поле заданной ширины. Аргумент: ширина поля.

Например:

"{{ value|ljust:"10" }}"

Если value равно Django, результат будет "Django ".

lower

Приводит строку к нижнему регистру.

Например:

{{ value|lower }}

Если value равно Still MAD At Yoko, фильтр выведет still mad at yoko.

make_list

Возвращает список, полученный из значения обрабатываемой переменной. Целое число будет преобразовано в список цифр, строка — в список букв.

Например:

{{ value|make_list }}

Если value является строкой "Joel", результатом будет список [u'J', u'o', u'e', u'l']. Если value является числом 123, результатом будет список [1, 2, 3].

phone2numeric

Преобразует телефонный номер (в т.ч. содержащий буквы) в числовой формат.

Входное значение не проверяется на правильность формата — любая строка будет преобразована.

Например:

{{ value|phone2numeric }}

Если value равно 800-COLLECT, фильтр вернет 800-2655328.

pluralize

Возвращает окончание множественного числа, если значение не равно 1. По умолчанию используется окончание 's'.

Пример:

You have {{ num_messages }} message{{ num_messages|pluralize }}.

Если num_messages равно 1, фильтр выведет You have 1 message.

Если num_messages равно 2, фильтр выведет You have 2 messages.

Если множественное число требует другого окончания (не 's'), укажите его в качестве аргумента фильтра.

Пример:

You have {{ num_walruses }} walrus{{ num_walruses|pluralize:"es" }}.

Если множественное число изменяет окончание слова, укажите через запятую окончание единственного числа и окончание множественного числа (фильтр заменит первое на второе).

Пример:

You have {{ num_cherries }} cherr{{ num_cherries|pluralize:"y,ies" }}.

pprint

Обёртка для функции pprint.pprint — применяется для отладки.

random

Возвращает случайный элемент в списке.

Например:

{{ value|random }}

Если value является списком ['a', 'b', 'c', 'd'], результатом может быть "b".

removetags

Удаляет [X]HTML-теги, перечисленные в строке-аргументе через пробел.

Например:

{{ value|removetags:"b span"|safe }}

Если value равно "<b>Joel</b> <button>is</button> a <span>slug</span>", фильтр выведет "Joel <button>is</button> a slug".

rjust

Выравнивает текст вправо в поле заданной ширины. Аргумент: ширина поля.

Например:

"{{ value|rjust:"10" }}"

Если value равно Django, фильтр выведет " Django".

safe

Строка, отмеченная этим фильтром, не будет экранирована перед выводом. Если автоматическое экранирование отключено, эффекта не имеет.

safeseq

Применяет фильтр safe к каждому элементу последовательности. Полезно при использовании других фильтров, воздействующих на последовательности, таких как join. Например:

{{ some_list|safeseq|join:", " }}

В этом случае нельзя использовать фильтр safe, т.к. он вначале превратит переменную в строку, а не пометит каждый элемент последовательности.

slice

Возвращает срез списка.

Используется синтаксис Python для срезов. Ознакомиться с ним можно на странице http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice.

Пример:

{{ some_list|slice:":2" }}

Если some_list равно ['a', 'b', 'c'], фильтр выведет ['a', 'b'].

slugify

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

Например:

{{ value|slugify }}

Если value равно "Joel is a slug", фильтр выведет "joel-is-a-slug".

stringformat

Форматирует переменную в соответствии с переданным аргументом, представляющим собой спецификатор формата. В спецификаторе используется синтаксис Python для строк формата, за исключением служебного символа «%».

Документацию по форматированию строк в Python см. на http://docs.python.org/library/stdtypes.html#string-formatting-operations

Например:

{{ value|stringformat:"s" }}

Если value равно "Joel is a slug", фильтр выведет "Joel is a slug".

striptags

Удаляет все [X]HTML-теги.

Например:

{{ value|striptags }}

Если value равно "<b>Joel</b> <button>is</button> a <span>slug</span>", фильтр вернет "Joel is a slug".

time

Выводит время в указанном формате.

Формат может быт задан преопределенной константой TIME_FORMAT (зависит от локальных настроек) либо пользовательским форматом (см. тег now).

По понятным причинам данный фильтр принимает только формат, определяющий вывод времени, а не даты. Для вывода даты используйте фильтр date.

Например:

{{ value|time:"H:i" }}

Если value эквивалентно datetime.datetime.now(), фильтр выведет строку "01:23".

Еще один пример:

Допустим, параметр USE_L10N равен True, а LANGUAGE_CODE, например, "de", тогда результатом фильтра:

{{ value|date:"TIME_FORMAT" }}

будет строка "01:23:00" (т.к. "TIME_FORMAT" в локали de использует строку формата "H:i:s").

Если формат не указан:

{{ value|date }}

то будет использована строка формата, определенная в параметре TIME_FORMAT, без учета локализации.

Добавлено в 1.2.

Текущая локаль теперь влияет на предопределенные форматы.

timesince

Форматирует дату в виде периода времени, прошедшего с того момента (например, «4 days, 6 hours»).

Принимает необязательный аргумент, содержащий дату окончания периода (без этого аргумента окончанием периода считается сейчас). Например, если blog_date содержит дату, указывающую на полночь 1 июня 2006 года, а comment_date указывает на 08:00 1 июня 2006 года, то {{ blog_date|timesince:comment_date }} вернет «8 hours».

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

Результат округляется до целого числа полных минут. Если начальная дата периода позже конечной, результат будет «0 minutes».

timeuntil

Аналогично timesince, но вычисляет период до указанного момента. Например, если сегодня 1 июня 2006 года, а conference_date указывает на дату 29 июня 2006 года, то {{ conference_date|timeuntil }} вернет «4 weeks».

Принимает необязательный аргумент, содержащий дату начала периода (вместо сейчас). Например, если from_date указывает на 22 июня 2006, то {{ conference_date|timeuntil:from_date }} вернет «1 week».

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

Результат округляется до целого числа полных минут. Если конечная дата периода раньше начальной, результат будет «0 minutes».

title

Делает заглавными первые буквы слов в строке.

Например:

{{ value|title }}

Если value равно "my first post", фильтр выведет "My First Post".

truncatewords

Обрезает строку после указанного числа слов. Аргумент: Максимальное число выводимых слов.

Например:

{{ value|truncatewords:2 }}

Если value равно "Joel is a slug", фильтр выведет "Joel is ...".

truncatewords_html

Аналогично truncatewords, но с учетом HTML-тегов. Закрывает все теги, не закрытые к моменту усечения строки.

Работает медленнее, чем truncatewords, поэтому рекомендуется использовать только для обработки HTML-текстов.

Например:

{{ value|truncatewords_html:2 }}

Если value равно "<p>Joel is a slug</p>", фильтр выведет "<p>Joel is ...</p>".

unordered_list

Превращает список списков (любой вложенности) в HTML-список (БЕЗ обрамляющего тега <ul>).

Измененов в 1.0.

Добавлена поддержка более простого формата списка.

Список должен быть правильно сформирован. Например, если var содержит список ['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']], то {{ var|unordered_list }} вернет:

<li>States
  <ul>
    <li>Kansas
      <ul>
        <li>Lawrence</li>
        <li>Topeka</li>
      </ul>
    </li>
    <li>Illinois</li>
  </ul>
</li>

Примечание: поддержка предыдущего формата (более строгого, но точного) сохранена: ['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]].

upper

Приводит строку к верхнему регистру.

Например:

{{ value|upper }}

Если value равно "Joel is a slug", фильтр выведет "JOEL IS A SLUG".

urlencode

Экранирует строку для использования в адресе URL.

Например:

{{ value|urlencode }}

Если value является строкой "http://www.example.org/foo?a=b&c=d", фильтр вернет "http%3A//www.example.org/foo%3Fa%3Db%26c%3Dd".

urlize

Заменяет ссылки в обычном тексте на активные HTML-ссылки.

Имейте в виду, что применение фильтра urlize к тексту, в котором уже содежится HTML-разметка, может привести к неправильным результатам. Применяйте этот фильтр только к простому тексту.

Например:

{{ value|urlize }}

Если value является строкой "Check out www.djangoproject.com", результатом будет строка "Check out <a href="http://www.djangoproject.com">www.djangoproject.com</a>".

urlizetrunc

Заменяет ссылки в обычном тексте на активные HTML-ссылки, усекая их текст до указанного числа символов.

Как и urlize_, применяется только к простому тексту. Аргумент: Максимальная длина текста ссылки.

Например:

{{ value|urlizetrunc:15 }}

Если value является строкой "Check out www.djangoproject.com", результатом будет строка 'Check out <a href="http://www.djangoproject.com">www.djangopr...</a>'.

wordcount

Возвращает число слов.

Например:

{{ value|wordcount }}

Если value равно "Joel is a slug", фильтр выведет 4.

wordwrap

Переносит слова с учетом указанной длины строки. Аргумент: число, задающее длину строки.

Например:

{{ value|wordwrap:5 }}

Если value равно Joel is a slug, фильтр вернет:

Joel
is a
slug

yesno

Превращает значения true, false и (необязательно) None в соответствующую строку, указанную в аргументе:

Таблица F.6. Пример

Значение | Аргумент | Результат
True | да, нет, может быть | да
False | да, нет, может быть | нет
None | да, нет, может быть | может быть
None | да, нет | нет (если значение для None не указано, берёт строку для False)

Дополнительные библиотеки тегов и фильтров

В состав Джанго входят несколько дополнительных библиотек тегов и фильтров. Для использования их нужно подключить в разделе настроек INSTALLED_APPS и загрузить в шаблоне посредством тега {% load %}.

django.contrib.humanize

Набор фильтров для представления данных в более привычном для человека формате.

django.contrib.markup

Набор фильтров, реализующих обработку некоторых языков разметки:

Textile

Markdown

ReST (ReStructured Text)

django.contrib.webdesign

Набор тегов, полезных для веб-дизайнеров, например, генератор текста «Lorem Ipsum».

Приложение G. Утилита django-admin

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.

Приложение H. Объекты HttpRequest и HttpResponse

Эта глава ждёт своего переводчика.

В ожидании пока это произойдёт, читаем оригинал.