- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF106:Django
Материал из Linuxformat.
- Django Разрабатываем динамические web-приложения современным способом
Содержание |
Личная блогосфера
Django |
---|
|
- ЧАСТЬ 2 Новостной портал из прошлого номера журнала легко превратить в мини-блог, предоставив посетителям возможность оставлять комментарии. Но что делать с троллями и спамерами? Никита Шультайс (http://shultais.ru) во всем разберется.
В прошлый раз мы научились создавать простые Django-приложения, добавили несколько записей в базу данных и вывели на сайт заголовок, дату и описание для последних десяти новостей. А как быть с теми, кто пожелает прочитать текст сообщения целиком?
Правильно, нужно снова создавать представления и шаблоны. Но для начала, усовершенствуем наши URL-карты.
Добавим гибкости
Откройте файл urls.py (как вы помните, именно он устанавливает соответствие между URL-адресами и представлениями) и замените строку
(r'^news/', 'news.views.last_news')
на
(r'^news/', include('news.urls'))
Теперь при переходе к news/ будут подгружаться URL-карты, относящиеся к приложению. Это позволит нам работать с новостной системой, не выходя за рамки директории news. Далее, нужно составить URL-карты приложения: создайте файл news/urls.py следующего содержания:
- from django.conf.urls.defaults import *
- urlpatterns = patterns('news.views',
- url(r'^$','last_news',name = 'news.last_news'),
- url(r'^(?P<news_id>\d+)/$','news_detail',name = 'news.news_detail'),
- )
Что происходит при обращении к http://mysite.com/news/? Сначала часть URL (news/) ищется в главном файле URL-карт, где обнаруживается соответствие. Затем система считывает URL-карты, отвечающие за конкретное приложение (благодаря include('news.urls')) и продолжает поиск, откинув уже найденный шаблон. Так как после news/ ничего нет, Django остановится в строке 3 файла news/urls.py и выполнит представление news.views.last_news, которое мы создали на прошлом уроке.
Рассмотрим news/urls.py подробнее. Во второй строке есть интересная запись – news.views, которая указывает Django, что нужно задействовать представления из одноименного модуля; это позволяет сэкономить место при наборе URL-карт и облегчает чтение кода. Строки 3 и 4 – это URL-карты особого формата. Они обернуты функцией url(), принимающей три (точнее сказать – до пяти) аргументов:
- первый (позиционный обязательный) – регулярное выражение;
- второй (позиционный обязательный) – имя представления, которое будет выполняться;
- третий (именованный необязательный) – имя URL-карты. Оно должно быть уникальным не только для приложения, но и для всего проекта, и в нашем случае состоит из двух частей: имени приложения и представления (это не единственный вариант). Часто возникают ситуации, когда одно представление обслуживает несколько URL-адресов, но имя каждой URL-карты всё равно должно быть уникальным. Скажем, если представление view может добавлять или изменять объекты, в зависимости от URL, то карты могут называться app.view.add и app.view.change.
Если вы взглянете на наши модели, то увидите, что поля первичного ключа – id – в них нет, однако в примере с тегом url мы используем news.id, как ни в чем не бывало. Все верно – Django сам создает первичные ключи для каждой модели.
Обратите внимание на конструкцию (?P<news_id>\d+) в строке 4. Она совпадает с числом (\d+), захватывает его и помещает в переменную news_id, которая затем передается в представление.
Хотя картам и не обязательно давать имена, это очень сильно упрощает жизнь разработчика и делает приложение переносимым. Поэтому откройте файл news/templates/news/last_news.html и измените строку:
<strong>{{ news.title }}</strong>
на
<strong> <a href=”{% url news.news_detail news_id=news.id %}”>{{ news.title }}</a> </strong>
В качестве значения атрибута href мы вставили тег url системы шаблонов Django. Ему передаются имена URL-карты и объявленной нами переменной – news_id. Значение news_id присваивается первичному ключу новости, которую хотим просмотреть.
Теперь, когда Django будет компилировать наш шаблон, {% url news.news_detail news_id=news.id %} заменится на http://mysite.com/news/1/ для новости с номером 1, и так далее. Если же вы решите модифицировать URL- карты, например, переименовав news/ в главном файле URL-карт в supernews/, то система автоматически скорректирует все ссылки, и сайт продолжит работать. Не забудьте только отредактировать файл media/templates/index.html, поправив:
<a href=”/news/”>News</a>
на
<a href=”{% url news.last_news %}”>News</a>
Если вы решите опробовать возможность автоматической генерации URL, не дожидаясь конца урока, то потребуется также добавить в файл news/views.py представление-заглушку для нового типа URL,
def news_detail(requets,news_id): pass
В противном случае автогенерация работать не будет.
Создаем форму
Продолжим развитие нашего приложения. Сейчас мы создадим представление и шаблон, с помощью которого можно просматривать детали новости, а также добавлять комментарии. Первым делом нужно реализовать форму для комментариев (рис.1), и, как вы могли догадаться, Django поможет нам в этом непростом деле. Дистрибутив Django содержит целых две системы обработки форм: forms (старую) и newforms (новую). Мы будем пользоваться последней, так как она более удобна и имеет больше возможностей. К тому же в версии 1.0 разработчики удалят старую библио теку, оставив только newforms (которая будет переименована в forms).
Формы очень удобно хранить в файле forms.py в директории приложения. Создайте его и наберите код:
- # -*- coding: utf-8 -*-
- from django import newforms as forms
- class CommentForm(forms.Form):
- username = forms.CharField(label=”Имя”,required=True,
- widget=forms.TextInput(attrs={
- 'size':'30',
- 'maxlength':'255'}))
- text = forms.CharField(label=”Текст”,required=True,
- widget=forms.Textarea())
Приведем два основных преимущества отделения URL-карт уровня приложений от главной карты сайта:
- Переносимость Теперь, если мы хотим добавить наше приложение в другой проект, нужно только скопировать директорию с ним в корень проекта, вписать его в INSTALLED_APPS и включить news/urls.py в главный файл urls.py. Если автор приложения расширит функциональность (например, добавит версию для печати), достаточно будет обновить файлы внутри директории news, не заботясь о добавлении или изменении URL-карт.
- Удобная работа с картами. В реальных проектах количество приложений может достигать 20 и больше, и каждое из них может содержать 10–20 внутренних карт. Если бы мы оформляли все URL-карты в одном файле, то его размер составил бы около 400 строк, а следовательно, возросла бы вероятность ошибки.
Каждая форма представлена классом Python, унаследованным от forms.Form. Она содержит набор полей, являющихся объектами класса forms.*Field. Так, в строках 4-7 определяется поле username типа CharField – символьное. Атрибут required указывает, что мы не можем оставить его пустым. Далее следует определение «виджета», то есть органа управления, который будет отображаться на web-странице. Виджет связан с определенным элементом HTML-формы, например, <select>, <input> или <textarea>. Каждый класс поля имеет свой стандартный виджет, но мы можем переопределить или расширить его, добавив дополнительные атрибуты. Виджетом по умолчанию для CharField является TextInput (<input type=»text»>), здесь мы задаем ему атрибуты size и maxlength. В строках 8-9 определяется поле text, которое также является символьным, но, поскольку одна строка плохо подходит для ввода комментария, мы назначаем text виджет Textarea(), соответствующий HTML-тегу <textarea>. Обратите внимание на атрибуты label: они содержат поясняющий текст, который выводится около элемента экранной формы. Так как мы используем кириллицу, необходимо указать кодировку – это делается в первой строке. Я предпочитаю UTF-8.
Теперь пришло время заменить и нашу заглушку (если, конечно, вы ее создали):
- def news_detail(request, news_id):
- news = News.objects.get(pk=news_id)
- if request.method == 'POST':
- form = CommentForm(request.POST)
- if form.is_valid():
- comment = Comment(
- news=news,
- username = form.cleaned_data['username'],
- text = form.cleaned_data['text'])
- comment.save()
- return HttpResponseRedirect(
- reverse('news.news_detail',kwargs={'news_id':news_id}))
- else:
- form = CommentForm()
- comments = Comment.objects.filter(news=news).order_by(“pub_ date”,”id”)
- comment_count = comments.count()
- template = get_template(“news/news_detail.html”)
- context = RequestContext(request, {
- “news”:news,
- “form”:form,
- “comment_count”:comment_count,
- “comments”:comments
- })
- return HttpResponse(template.render(context))
Сразу стоит обратить внимание на то, что в представление передается дополнительный аргумент news_id, имя которого совпадает с идентификатором переменной, захватываемой при обработке регулярного выражения в файле news/urls.py. Во второй строке мы извлекаем новость, а так как нам нужен только один объект, то используем для этого функцию get(), передавая ей news_id в качестве именованного аргумента pk (Primary Key, первичный ключ). Дальше следуют два блока: для POST- (строки 4-12), и GET-запросов (14). В строке 3 проверяется, как именно произошло обращения к данному URL. В случае GET мы просто создаем объект формы с пустыми полями.
Если же данные были получены посредством запроса POST, они передаются в форму. Далее идет проверка на корректность (строка 5): мы убеждаемся, что заполнены все поля. В случае успеха создается объект комментария. Первый параметр – это новость, к которой он относится, аргументам username и text присваиваются очищенные (то есть преобразованные к определенному типу: unicode для текста, datetime для даты и так далее) данные из полей формы. В 10-й строке объект сохраняется в БД, а в строках 11–12 мы осуществляем перенаправление. Функция reverse – это аналог тега url в шаблонах. Она принимает имя URL-карты, а в словаре kwargs должны содержаться требуемые аргументы, причем переменной news_id присваивается номер текущей новости... Проще говоря, мы обновляем страницу. Если же проверка в строке 5 не проходит, то мы автоматически отправляемся к строке 15 с формой, где уже заполнены некоторые поля, а для пустых созданы сообщения об ошибках.
Обратимся к строкам 16 и 17. В первой мы извлекаем все комментарии, относящиеся к новости, а во второй получаем их количество, причем функция count() генерирует SQL-конструкцию COUNT, которая работает очень быстро.
Строки 19-26 рассматривались на прошлом уроке, и сейчас мы не будем на них останавливаться. Лучше рассмотрим шаблон news/templates/news/news_detail.html – он может иметь такой вид:
- {% extends “index.html” %}
- {% block application %}
- <h1>{{ news.title }}</h1>
- <p> {{ news.text }} </p>
- {% if comment_count %}
- <h3>Комментариев: {{ comment_count }}</h3>
- {% for comment in comments %}
- <div>
- <strong>{{ comment.username }}</strong> пишет:<br/>
- {{ comment.text }}<br/>
- Дата {{ comment.pub_date|date:”d.m.Y” }}
- </div>
- <br/><br/>
- {% endfor %}
- {% else %}
- <h3>Комментариев нет</h3>
- {% endif %}
- <h3>Оставить комментарий:</h3>
- <form action=”.” method=”POST”>
- <table>{{ form }}</table>
- <input type=”submit”/>
- </form>
- {% endblock %}
Обратите внимание на строку 7, с которой начинается новый для нас тег – {% if %}. Он принимает логическую (а поскольку в Python все числовые значения и строки, которые не равны нулю и не пусты, эквивалентны True, то практически любую) переменную. В строке 17 расположена вторая часть тега – альтернативный блок {% else %} и, наконец, в строке 19 тег закрывается. Разумеется, альтернативный блок можно опустить.
Хотя Django берет на себя бремя генерации форм, некоторую часть кода приходится писать вручную. Так, в строке 22 мы определяем форму, а в строке 23 создаем таблицу, в которую она будет помещена. Дело в том, что по умолчанию при вызове объекта формы все виджеты оборачиваются в <tr> и <td>. Это поведение можно исправить, заменив form на form.as_ul или form.as_p, которые которые возвращают ее в виде ненумерованного списка (<ul>) и параграфов (<p>), соответственно. Кнопка для отправки формы создается в строке 24.
Теперь, когда мы создали шаблон и представление для просмотра текста новости и добавления комментариев, осталось только импортировать необходимые классы и функции в представление news/views.py:
from news.models import News from news.forms import CommentForm from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse
Не забудьте, что импортировать классы и функции нужно до их первого использования.
Плохой песик, плохой!
Когда ваши новости комментируют объективно – это прекрасно, но мир неидеален и пока не искоренены спам и флуд, вам придется модерировать сообщения, оставленные посетителями. Наша система не поддерживает пре-модерацию; конечно, в реальных проектах так поступать не стоит. Но и у нас не всё потеряно – мы можем:
- Модерировать комментарии из административного интерфейса
- Делать это прямо с сайта.
Первый вариант настолько прост, что мы даже не будем на нем останавливаться, но прежде чем перейти ко второму случаю, поговорим о системе прав Django. С главной страницы «админки» можно перейти в разделы управления пользователями и группами (рис.2).
Каждый пользователь и группа могут иметь определенные права доступа, которые описываются в моделях. По умолчанию, модель предоставляет три разрешения: добавить, изменить и удалить объект из базы данных. Если вы перейдете на страницу редактирования пользователя в «админке», то в разделе Permissions > User permissions увидите список прав доступа, причем в левой части (Available user permissions) указываются всевозможные права, известные системе, а в правой (Chosen user permissions) – разрешения, которыми обладает редактируемый пользователь (рис.3).
Каждая строка содержит три части: приложение, модель и право доступа в человеко-понятной форме. Например, news | comment | Can add comment означает, что пользователь может добавлять комментарии в модель comment приложения news. Проверку прав доступа нужно осуществлять на уровне представлений или/и шаблонов. На уровне моделей она не производится, поэтому комментарии к нашим новостям могут добавлять любые посетители, даже не имеющие прав на совершение таких действий.
Условно, всех пользователей системы можно разделить на две группы: анонимные и авторизованные. Особый случай – это суперпользователь; в Django, как и в Linux, он царь и бог и обладает всеми возможными правами. Назначить себя суперпользователем можно, установив галочку напротив поля Superuser status. Отметим ещё два поля в разделе Permissions:
- Staff status – если эта галочка установлена, пользователь может заходить в «админку» Django.
- Active – если флажок снят, посетителю будет отказано в авторизации, даже когда он верно указывает свои имя и пароль.
Создадим еще одно представление, с помощью которого будем удалять нежелательные комментарии:
- @permission_required('news.delete_comment')
- def delete_comment(request, news_id, comment_id):
- comment = get_object_or_404(Comment,pk=comment_id)
- comment.delete()
- return HttpResponseRedirect(reverse('news.news_ detail',kwargs={'news_id':news_id}))
Основным его отличием от виденных нами ранее является проверка прав доступа. Она реализована с помощью декоратора в строке 1 – кстати, не забудьте его импортировать.
from django.contrib.auth.decorators import permission_required
Права доступа в декораторах определяются именем приложения (news) и названием разрешения (delete_comment). Последнее получается автоматически конкатенацией действия (delete) и модели. Обратите внимание на строку 3: в ней мы извлекаем из базы данных единственный объект, но используем сокращение get_object_or_404(), которое возвращает страницу 404 в случае отсутствия объекта. Не забудьте импортировать функцию get_object_or_404() в ваше представление до её использования:
from django.shortcuts import get_object_or_404
Теперь нам нужно связать URL с представлением:
url(r'^(?P<news_id>\d+)/(?P<comment_id>\d+)/delete/$','delete_comment',name='news.delete_comment'),
Давайте расширим наш шаблон: во-первых, незачем показывать форму анонимным пользователям, во-вторых, следует добавить ссылку для удаления комментария, если посетитель обладает достаточными правами. Допишите следующий код после строки 10:
{% if perms.news.delete_comment %} <a href=”{% url news.delete_comment news_id=news.id,comment_id=comment.id %}”>[X]</a> {% endif %}
Это напоминает использование декораторов, за исключением обращения к объекту perms. Теперь напротив каждого комментария будет присутствовать ссылка для его удаления. Далее, заключите форму (строки 21-25) в блок {% if %}, чтобы она не отображалась неавторизованным пользователям.
{% if user.is_authenticated %} ... {% endif %}
Помимо perms, содержащего права текущего пользователя, в шаблон передается объект user, предоставляющий доступ к прочим реквизитам: имени пользователя, хэшу пароля, данным о текущем состоянии (анонимный или авторизованный). Например, с помощью:
{% if user.is_superuser %}
можно определить, является ли посетитель суперпользователем.
Вас взломали!
Казалось бы, все в порядке, но есть одна тонкость. Спрятав форму от глаз неавторизованного пользователя, мы не лишили его возможности «заполнять» ее, генерируя HTTP-заголовки вручную – представление ведь осталось прежним! Чтобы закрыть лазейку, нужно перво-наперво импортировать исключение Http404 (страница не найдена):
from django.http import Http404
После проверки метода запроса в файле news/views.py (строка 3) добавьте конструкцию
if not request.user.is_authenticated(): raise Http404
она выбрасывает исключение, если посетитель неавторизован.
Помимо Http404, можно создавать ответы с различными HTTP-кодами, например, HttpResponseForbidden (403 – доступ запрещен) и так далее. Заметьте, что Http404 – это именно исключение и его нужно возбуждать с помощью raise, тогда как остальные ответы возвращаются посредством return.
И, напоследок, выполним еще одну оптимизацию. Так как комментарии могут оставлять только авторизованные пользователи, не будем утруждать их вводом своего имени. Система обработки форм позволяет динамически заполнять некоторые поля исходными данными. При замене
form = CommentForm()
на
form = CommentForm(initial={'username':request.user.username})
поле username автоматически получит имя текущего авторизованного пользователя. LXF