Журнал LinuxFormat - перейти на главную

LXF108:Django

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
м (Акроним переименовал страницу LXF108:Django в LХF100-101:Aнaнac: ...)
м (Акроним переименовал страницу LХF100-101:Aнaнac в LXF108:Django поверх перенаправления)
 

Текущая версия на 19:53, 3 июня 2015

Содержание

[править] Финальные штрихи

ЧАСТЬ 4 Уроки Django подходят к концу, наступает пора экзаменов – если не для вас, то для приложений уж точно. Никита Шультайс (http://shultais.ru) разберется с тестированием и пробежится по другим возможностям этого каркаса.

Новостная лента готова – но оправдает ли она ожидания пользователей? Ответ на этот вопрос может дать тестирование, лучше всего – в реальных условиях; но какие-то основные вещи можно проверить еще на этапе разработки. Если вы успели заглянуть в учебник Rails, то уже знаете о методологии TDD. Мы, однако, пойдем другим путем, и будем тестировать приложение не до написания, а после.

Дело это – серьезное и сложное, так как нам нужно учитывать взаимодействие с БД, компиляцию шаблонов, обработку GET- и POST-запросов и прочие компоненты системы: сбой в любом из них может вызвать нарушить работу всего сайта. К данной задаче можно подойти с двух сторон:

  • Тестирование в браузере. Речь идет о программах Twill (http://twill.idyll.org) и Selenium (http://selenium.openqa.org): они «запоминают» последовательность ваших действий для каждой страницы, а затем воспроизводят ее по запросу. Например, можно ввести в поля формы заведомо неверные данные, получить ожидаемую ошибку и повторять этот тест при каждом серьезном изменении в коде вашего приложения.
  • Тестирование на сервере. И тут Django не оставляет нас на произвол судьбы, предлагая сразу два варианта: doctest (тестирование через документацию) и unittest (модульное тестирование), плюс специальный клиент для отправки GET- и POST-запросов.

Если вы давно программируете на Python, то вам, наверное, будет ближе doctest, а мигрантам из мира Java скорее придется по вкусу unittest. Никаких ограничений на их использование не накладывается: вы можете выбрать одну систему или применять обе сразу. Мы же остановимся на doctest.

[править] Документируй это!

Строка документации в Python – это обычный текст, размещаемый после определения функции или класса непосредственно в исходном коде. Она же предоставляет содержимое для атрибута __doc__. Как правило, ее помещают в тройные кавычки ("""), что позволяет вводить сложные конструкции с переносами строк, отступами, теми же кавычками и... тестами. Этим мы и воспользуемся.

Тесты могут находится в файлах моделей (models.py) – для проверки последних – и в специальных файлах tests.py, расположенных в директории приложения. К примеру, создайте файл news/tests.py такого содержания:

  1. # -*- coding: utf-8 -*-
  2.  
  3. """
  4. >>> from news.models import News
  5. >>> from datetime import datetime
  6. >>> news = News(title="Заголовок",description="Описание",pub_date=datetime.now(), text="Текст")
  7. >>> news.save()
  8. >>> news = News.objects.get(pk=1)
  9. >>> print news.title.encode('utf-8')
  10.  Заголовок
  11. """

В строке 1 задается кодировка, а начиная с третьей идет сам тест. Заметьте, что каждая строка начинается с трех знаков «больше» (>>>), как в интерактивном режиме работы Python. В строке 10 этих знаков нет, так как она содержит ожидаемый вывод команды print со строки 9.

(thumbnail)
Рис. 1. Тест пройден удачно!

Перед тем, как запустить тест, в settings.py нужно добавить строку

TEST_DATABASE_CHARSET="UTF8"

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

python manage.py test news

после выполнения которой вы увидите примерно то, что показано на рис. 1.

Сообщения похожи на появляющиеся во время создания таблиц при первой установке, но теперь все происходит в тестовой БД. В конце отображается число выполненных тестов, их результаты и уведомление об уничтожении тестовой базы. Мы проверяли наше приложение (news), но, как вы помните, Django содержит несколько собственных приложений и представлений (например, «админку») – и они тоже снабжаются своими тестами. Чтобы выполнить их все, нужно ввести команду:

python manage.py test

добавив предварительно в главный файл URL-карт следующие строки:

urlpatterns += patterns('django.contrib.auth.views',
url(r'^auth/password_reset/$','password_reset'),
)

Здесь мы подключаем одно из встроенных представлений, предназначенное для восстановления пароля. Это необходимо, так как при тестировании всего проекта происходит обращение по указанному URL, и если представление не определено, тест будет провален. Кстати, вы тоже можете попробовать password_reset в работе (рис. 2).

(thumbnail)
Рис. 2. Забыли пароль? Это проблемы Django!

[править] Имитатор Сети

Количество тестов уже достигло шести, но помимо создания и извлечения объектов из базы, нам нужно проверить реакцию на GET- и POST-запросы. Как вы знаете, для этих целей существует специальный клиент: он эмулирует запрос и возвращает переменные, которые передаются в шаблон для данного URL. Добавьте в файл tests.py после строки 10 следующий код:

  1. >>> from django.contrib.auth.models import User
  2. >>> user = User.objects.create_user('django_guru', 'user@example.com', 'password')
  3. >>> user.save()
  4. >>> from django.test.client import Client
  5. >>> c = Client()
  6. >>> c.login(username='django_guru',password=“password”)
  7. True
  8. >>> response = c.get('/news/1/')
  9. >>> response.status_code
  10. 200
  11. >>> print response.context[0]['user'].username
  12. django_guru
  13. >>> response = c.post('/news/1/',{'username':“testuser”,'text':»»})
  14. >>> response.status_code
  15. 200
  16. >>> response = c.post('/news/1/',{'username':“testuser”,'text':»Comment text»})
  17. >>> response.status_code
  18. 302
  19. >>> from news.models import Comment
  20. >>> comment = Comment.objects.get(news=news)
  21. >>> print comment.text
  22. Comment text

Разберемся, что здесь происходит. В строках 11–13 мы создаем нового пользователя (django_guru), а в 14–15 – тестовый клиент. В строке 16 django_guru авторизуется, и отныне все действия в системе будут совершаться от его имени. В строке 18 мы переходим на страницу нашей первой новости, передав средствами клиента GET-запрос. Для проверки, что нам это удалось, мы изучаем код ответа сервера (строка 19) – он должен равняться 200, или тест будет провален. Затем (строки 21–22), чтением дополнительных данных ответа, мы убеждаемся, что запрос сделал зарегистрированный пользователь django_guru. Теперь самое время оставить комментарий – не зря же мы авторизовались? В строке 23 генерируется POST-запрос (второй аргумент метода post() – словарь отсылаемых на сервер данных). Обратите внимание, что значение ключа text оставлено пустым, а значит, комментарий не добавится, однако сервер по-прежнему должен возвращать код 200 (строка 25). А вот в строке 26 мы передаем все необходимые данные, и так как после создания комментария нас перенаправляют на страницу новости, код ответа должен быть равен 302 (Требуемый URL перемещен). В строках 29–32 проверяется, что комментарий был действительно добавлен: мы сравниваем его текст с исходным значением. Уфф... тест пройден.

[править] Действительно простая синдикация

Какой новостной сайт без ленты? RSS и/или Atom есть везде – будут и у нас, а Django нам в этом поможет. Откройте главный файл URL-карт и добавьте в его конец следующие строки:

  1. from feeds import LatestNews
  2.  
  3. feeds = {
  4. 'latest': LatestNews,
  5. }
  6. urlpatterns += patterns('',
  7. (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
  8. {'feed_dict': feeds}),
  9. )

Далее нужно приготовить ленту LatestNews, которую мы импортируем в строке 1. Создайте в корне проекта каталог feeds с файлом __init__.py следующего содержания:

  1. # -*- coding: utf-8 -*-
  2.  
  3. from django.contrib.syndication.feeds import Feed
  4. from news.models import News
  5. from django.contrib.syndication.feeds import FeedDoesNotExist
  6.  
  7. class LatestNews(Feed):
  8. title = "Последние новости нашего сайта"
  9. description = "Последние события на сайте mysite.com"
  10. link = "http://127.0.0.1:8000/news/"
  11.  
  12. def items(self):
  13. return News.objects.order_by("-pub_date")[:5]
  14.  
  15. def item_link(self, obj):
  16. from django.core.urlresolvers import reverse
  17. return 'http://127.0.0.1:8000%s' % reverse('news.news_detail',kwargs={"news_id":obj.pk})
(thumbnail)
Рис. 3. Firefox предлагает подписаться на обновления нашего новостного сайта – теперь держитесь!

Поля title, description и link класса LatestNews являются обязательными и отвечают за одноименные элементы RSS. Метод items() передает в ленту требуемые объекты, а item_link() отвечает за ссылку на сайт. Теперь создайте каталог feeds в media/templates и добавьте в него два файла, latest_description.html и latest_title.html: они будут отвечать за вид новостной ленты. В lates_description.html напишите:

{{ obj.description }}

а в latest_title.html:

[{{ obj.pub_date|date:"d.m.Y" }}] {{ obj.title }}

Объект obj представляет собой запись из выборки, которую мы возвращаем в строке 13 файла feeds/__init__.py. Пройдя по адресу http://127.0.0.1:8000/feeds/latest/, мы увидим предложение Firefox сохранить ленту новостей. Пользователи KDE, вероятно, предпочтут Akregator – с ним тоже нет никаких проблем (рис. 3, 4)

[править] Общие представления

Чтобы облегчить жизнь web-разработчика, Django включил большое число представлений для решения стандартных задач. Так, добавив в главный файл URL-карт следующий код:

from django.views.generic.list_detail import object_list
from news.models import News
urlpatterns += patterns('',
('^lastnews/$', object_list, {
'queryset': News.objects.all().order_by('-pub_date')[:10],
'template_name':'news/last_news.html',
'template_object_name':'last_news'})
)

а также заменив в файле news/templates/news/last_news.html

{% for news in last_news %}

на

{% for news in last_news_list %}

мы сможем просматривать последние новости по адресу http://127.0.0.1:8000/lastnews/, не вызывая представление news.last_news. Чтобы сделать доступными оба варианта, нужно найти в представлении news.last_news строку

"last_news":news,

и заменить ее на

"last_news_list":news,

Как вы уже догадались, общее представление object_list предназначено для работы со списком объектов. Еще есть представления для вывода объектов в зависимости от даты (django.views.generic.date_based.*), что позволяет очень просто создавать архивы записей:

  • archive_index – вывод последних объектов, добавленных в базу данных;
  • archive_{year,month,week,day,today} – вывод всех объектов за определенный год, месяц, неделю, день или за сегодня;
  • object_detail – вывод одного объекта за определенный день.

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

[править] Добавляем переменные на лету

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

  • auth – информация о пользователе: объект user, его права доступа и сообщения, которые были ему отправлены;
  • i18n – сведения о текущем языке сайта и клиента;
  • request – данные о запросе.

Кроме них, существует еще процессор debug, передающий в шаблон данные о выполненных SQL-запросах, плюс мы можем написать свой собственный! Для этого создадим в корне нашего проекта каталог processors и добавим в него два файла: __init__.py и context_processors.py. Последний должен содержать такой код:

import settings
def site_settings(request):
return {'SETTINGS': settings}

Чтобы подключить процессор, просто перечислите его в кортеже TEMPLATE_CONTEXT_PROCESSORS. Проверим работоспособность: добавим в шаблон news.html следующее:

{{ SETTINGS.TIME_ZONE }}

Конечно, TIME_ZONE можно заменить на любую другую переменную, определенную в settings.py.

[править] Сам себе фильтр

С фильтрами мы познакомились еще в LXF105, однако часто возникают ситуации, когда поставляемых с Django вариантов недостаточно. Чтобы написать свой собственный фильтр, создайте в корне проекта каталог templatetags/ и добавьте в него файлы __init__.py и filters.py. В filters.py напишите:

  1. from django import template
  2.  
  3. register = template.Library()
  4.  
  5. @register.filter
  6. def exp(value, arg):
  7. if value.isdigit() and arg.isdigit():
  8. return int(value)**int(arg)
  9. else:
  10. return '<span style=“color:red”>Error</span>'
  11. exp.is_safe = True

Мы создали фильтр exp, который получает значение и показатель степени и возводит одно в другое; если аргументы не являются числами, генерируется ошибка. В строке 5 мы регистрируем фильтр в системе с помощью декоратора. Строка 11 указывает, что exp может возвращать HTML-код. Поскольку (в целях безопасности) он автоматически экранируется (< и > заменяются на &lt; и &gt; и т.д.), то, желая видеть чистый HTML, мы должны запретить такое поведение вручную. Следующим шагом является подгрузка библиотеки фильтров в шаблон, для чего нужно добавить в него следующую строку:

{% load filters %}

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

ln -s /var/www/myproject/templatetags/ /var/www/myproject/news/

Теперь проверим работу фильтра, добавив в какой-нибудь шаблон строку.

{{ "4"|exp:"4" }}

Во время компиляции она будет заменена на 256. Если же мы напишем

{{ "a"|exp:"4" }}

то увидим слово «Error», выделенное красным цветом. Кстати, если бы мы не указали в строке 11 фильтра exp.is_safe= True, можно было бы просто применить фильтр safe прямо в шаблоне:

{{ "a"|exp:"4"|safe }}

После регистрации фильтра в системе, информация о нем становится доступной по адресу http://127.0.0.1:8000/admin/doc/filters/ (рис. 4)

(thumbnail)
Рис. 4. Система любезно подскажет, как использовать созданный вами фильтр.

[править] Компоненты

Если нам надо выполнить какие-либо действия до или после того, как будет вызвано представление, либо в случае ошибки, можно создать свой компонент (middleware) или воспользоваться поставляемым с Django. Мы уже делали это, когда изучали кэширование (LXF107). Напомню, что в файле settings.py есть кортеж MIDDLEWARE_CLASSES, который перечисляет компоненты, задействованные в проекте. У нас это:

  • django.middleware.common.CommonMiddleware Решает общие задачи: нормализует URL (добавляет префикс www и завершающий /), запрещает доступ к сайту определенным роботам, взаимодействует с Etag.
  • django.contrib.sessions.middleware.SessionMiddleware Это сессии.
  • django.contrib.auth.middleware.AuthenticationMiddleware А это – авторизация.
  • django.middleware.doc.XViewMiddleware Используется для автоматического документирования Django.
  • django.middleware.locale.LocaleMiddleware Интернационализация.

Помимо перечисленных выше, в Django доступны следующие компоненты (django.middleware.*):

  • gzip.GZipMiddleware Сжатие отправляемой страницы для экономии трафика.
  • http.ConditionalGetMiddleware Поддержка условных GET-запросов для работы с Last-Modified и Etag.
  • http.SetRemoteAddrFromForwardedFor Обратное проксирование.
  • cache.CacheMiddleware Тот самый кэш, с которым мы сталкивались на прошлом уроке.
  • transaction.TransactionMiddleware Компонент для включения в SQL-запросы транзакционных конструкций: COMMIT, ROLLBACK.

Заметьте, что не все базы данных поддерживают транзакции.

И, наконец, django.contrib.csrf.middleware.CsrfMiddleware, защищающее от CSRF-атак.

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

  • process_request() – запускается после того, как поступил запрос,но перед тем как Django начнет искать запрашиваемый адрес в URL-картах;
  • process_view() – отрабатывает, когда конкретное представление уже определено, но еще не запущено;
  • process_response() – выполняется после представления.

Используется для сжатия сгенерированного HTML. process_exception() – вызывается, если что-то пошло не так или было возбуждено необработанное исключение.

Вот, в сущности, и все. Впрочем, нет – взгляните на врезку И прочая, прочая, прочая, почитайте документацию или свободную книгу о Django – Django Book (http://www.djangobook.com); если же вам больше по душе русский, советую заглянуть на http://cargo.caml.ru/djangobook. Наконец, примените полученные знания на практике – и дайте нам знать, если у вас получится что-то действительно стоящее!


Персональные инструменты
купить
подписаться
Яндекс.Метрика