slav0nic's blog

Заметки о python, linux и других занимательных вещах

web.py part #2

Попробуем написать блог, полностью писать груз (пока что) , сделаем простое добавление записей с поддержкой markdown'a.
Будут использованы такие модули:
  • web.py 0.22
  • markdown
  • pytils
  • sqlalchemy для sqlite базы

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

В качестве БД будет использоваться SQlite; markdown - для форматирования текста в постах, вместо всяких bb-code и raw html'я; pytils - для slug'a (для ссылок на посты, чтоб можно было обращаться к записям не по их ID, а по понятному slug'y полученному из заголовка поста), wsgi сервер - дефолтный.

Структура файлов будет примерно такой:
blog/
    blod.db - база
    code.py - само WSGI-приложение
    config.py - конфиг блога для всяких переменных
    model.py - описание модели и ф-ций для работы с БД
    view.py - описание форм
    tmpl/ -темплейты
        add_entry.html
        index.html

Начнёмс с модели, она будет довольно простой (model.py):
  1. blog = Table('blog', metadata,
  2.     Column('id', Integer, primary_key=True), # в sqlite primary_key типа int == autoincrement
  3.     Column('author', String),
  4.     Column('subject', String),
  5.     Column('entry', String),
  6.     Column('slug', String, unique=True),
  7.     Column('tags', String),
  8.     Column('date', DateTime, default=func.current_timestamp()),
  9. )
Хотя в принципе для подобных задач web.db было бы достаточно.

Code.py изначально имеет примерно такой вид:

  1. import web
  2. import view
  3. import model
  4. import config
  5.  
  6.  
  7. web.webapi.internalerror = web.debugerror
  8. web.webapi.notfound = view.not_found
  9.  
  10. urls = (
  11.     '/', 'index',
  12.     '/add', 'add_entry',
  13.     '/view/(.*)', 'view_entry',
  14. #    '/login', 'login',
  15. #    '/logout', 'logout',
  16. #    '/edit/(.*)', 'edit',
  17. #    '/page/(\.*)', 'page',
  18. #    '/view/(.*)/comment', 'comment',
  19. )
  20.  
  21. render = web.template.render('tmpl/', cache=False)
  22.  
  23. class index:
  24.    
  25.     def GET(self):
  26.         web.header("Content-Type","text/html; charset=utf-8")
  27.         posts = model.getEntries()
  28.         entry_count = model.getEntriesCount()
  29.         web.output(render.index(posts, config, xrange(entry_count/config.ENTRIES_PER_PAGE)))
  30.  
  31. if __name__ == "__main__":
  32.     web.run(urls, globals(), web.reloader)


Опишу подробно строки.
web.webapi.internalerror = web.debugerror подружает отладчик. таким образом кроме вывода ошибки в консоль, в браузере будет появляться отладочная инфа при возникновении внутренних ошибок.
Строкой ниже заменяем ф-цию отвечающую за ответ на 404 ошибку (страница не найдена) на свою (простой print "<b>404 B] </b>")
В urls описываются соответсвия URL и классов, URL задаётся при помощи регулярных выражений.
Ниже задаётся каталог с шаблонами (шаблоны встроенные), кэширование отключено для возможности разработки без перезагрузки сервера, кстати за это отвечает 3й параметр web.run() - web.reloader (в версии 0.21 он не работает)
Класс index включает в себя всего один метод GET для обработки запросов соответсвующего типа, web.header() позволяет изменить http заголовок на нужным, в нашем случае задаём кодировку. Ниже идут ф-ции получения записей их количества.
web.output служит для выдачи результата клиенту, в принципе во всех примерах используется простой print, по больщому счёту разницы никакой, данная ф-ция лишь добавляет строку (в нашем случаем возвращает темплейт) к ответу сервера и кодирует юникод объекты в utf-8, но имхо так более кошерней смотрится В)).
render.index вызывает темплейт. Темплятор находит в каталоге 'tmpl/' шаблон index.html и передаёт ему заданные параметры.

Рассмотрим непосредственно сам темплейт:
  1. $def with (posts, config, entry_count)
  2. <title> $config.BLOG_TITLE </title>
  3.  
  4. $for i in posts:
  5.         <p>author $i.author.encode('utf-8'), <a href="/view/$i.slug.encode('utf-8')"><b>$i.subject.encode('utf-8')</b></a> date: $i.date</p>
  6.         tags:
  7.         $if i.tags:
  8.             $for tag in i.tags.split(","):
  9.                 <a href="/tag/$tag.encode('utf-8')">$tag.encode('utf-8')</a>
  10.         <p>$:i.entry.encode('utf-8')
  11.         <hr size="2" NOSHADE>
  12. $for i in entry_count:
  13.         <a href="/page/$i">$i<a> |
  14.  
  15. </center>
  16. </html>
Язык довольно прост, все строки питона начинаются с $, для приёма параметров служит ф-ция with (posts, config, entry_count), стоит обратить внимание, что после with должен стоять пробел, иначе парсер ругнётся, также стоит следить за пробелами после : в циклах =\ (на будущее скажу, что парсер реально гавно=), хотя в целом к темплейтам привыкнуть можно). В принципе не сильно радует отсутствие импортов модулей, всё ф-ции необходимо передавать через глобальный свойство global класса Template. Например
чтоб получить доступ к xrange() ф-цие, необходимо добавить в code.py строку web.template.Template.globals['xrange'] = xrange, после чего в шаблонах можно обратиться к ней как $:xrange(), только вот как заюзать эту ф-цию например в цикле for я не вкурил.... ":" говорит о том что метод глобальный, хотя например $:i.entry говорит, что вывод будет осуществяться "так как есть", без прогонки через web.websafe() (эта ф-ция запрещает вывод html, заменяет <> и тп символы).
Также хочу отметить небольшую попу в версии 0.22, unicode объекты не раскодируются в utf-8, поэтому в примере я делал это вручную, в текущей svn версии 0.3 это делается автоматом.
В принципе это наверно всё по темлейтам, думаю добавить нечего) Подробней можно прочесть на офсайте.

Рассмотрим создание и обработку форм на примере добавления записей:

Как я уже говорил в первой заметке web.form довольно удобен и поддерживает валидаторы, с которыми разобрался даже я В)
Добавим 2й класс add_entry, добавим описание формы во view.py (кстати важно чтоб форма была глобальна, и к ней можно было обратиться из метода GET (непосредственного рисования) и POST для обработки и валидации) и шаблон:
  1. class add_entry:
  2.    
  3.     def GET(self):
  4.         web.header("Content-Type","text/html; charset=utf-8")
  5.         web.output(render.add_entry(view.add_entry_form))
  6.  
  7.     def POST(self):
  8.         web.header("Content-Type","text/html; charset=utf-8")
  9.         i = web.input()
  10.         v = view.add_entry_form(i)
  11.         if v.validates():
  12.             web.output("posting...")
  13.             model.postEntry((i.login, i.subject, i.tags, web.safemarkdown(i.entry.decode('utf-8'))))
  14.             web.output("ok")
  15.             web.seeother("/")
  16.         else:
  17.             web.output(render.add_entry(v))
Метод GET только рисует форму, POST - получает введённые данные через web.input(), получаем объект типа storage(), мы можем получить значение поля login как через i.login, так и через i['login'].value, после чего передаём все введённые данные нашей форме для проверки на валидность, метод validates() говорит нам валидна ли данные, если да - добавляем в базу, перекодируя текст в хтмл, пропуская через markdown (safemarkdown защищает от XSS-уязвимостей и убирает <> и тп, можете глянуть в utils.py). Методweb.seeother() перекидывает на главную страницу, в принципе есть метод web.redirect(), разница лишь в http-ответах (подробности в RFC по HTTP или рассылке по webpy B] )

view.py:

  1. from web import form
  2.  
  3. #форма добавления записи
  4. add_entry_form = form.Form(
  5.             form.Textbox("login", form.notnull),
  6.             form.Textbox("subject", form.notnull),
  7.             form.Textbox("tags"),
  8.             form.Textarea("entry", form.notnull, rows="30", cols="80" ),
  9.         )

Думаю тут всё понятно, form.notnull делает поле обязательным. Также можно задать более серьёзные валидаторы, кому инетерсно глянет на http://web.py/form
add_entry.html:
  1. $def with (add_entry_form)
  2. <style type="text/css">
  3. th { text-align: left; background-color: #EEEEEE; }
  4. p.error { background-color: #FFEEEE; }
  5. </style>
  6.  
  7. <form name="main" method="post">
  8. $if not add_entry_form.valid:
  9.     <p class="error">Incorrect data</p>
  10.     $:add_entry_form.render()
  11.     <input type="submit" />
  12. $else:
  13.     $:add_entry_form.render()
  14.     <input type="submit" />
  15.  
  16. </form>
  17. </html>
Здесь я реализовал все свои космические знания по web, забабахав CSS =))
В данном темплейте проверяется форма, если она не валидна - выводится мессага "Incorrect data", при этом автоматом справа от невалидного поля появляется надпись
Required (надо будет глянуть как его заменить =) )
Вот так это выглядит:


Вот почти и всё что хотелось показать, осталось сделать выборку постов по slug:
Так как описание ulr'a для данной операции имеет вид /view/(.*) то всё что будет передаваться после /view/ необходимо получить и обработать, для этого метод GET имеет такой вид:
  1. class view_entry:
  2.    
  3.     def GET(self, slug):
  4.        ...
Все необходимые данные будут заноситься в slug переменную, по которой и будет проходить выборка.

Запущенное на дефолтном серваке всё это дело ест 8.5мб озу

Вот и всё, писал как можно проще, чтоб было понятно начинающим и интересно бывалым =) Надеюсь не зря убил время В) Думаю в это статье мне удалось описать большую часть ф-ций webpy и способов работы с элементарными вещами и показать что web.py максимально прост, но за этой простотой скрывается достаточный функционал, который при желании легко достигается сторонними модулями. Если сравнивать с другими "большими" фреймворками, то webpy мне напоминает pylons, по крайней мере не вижу сложности взять все модули используемые в пилонах и прикрутить к webpy ) Также webpy подойдёт людям, которые не могу найти нормальный фреймворк, но имеют пристрастия к определённым пакетам для работы с формами, темплейтами, БД и вэбом в целом, опять же из-за простоты привязки. webpy не мешает писать и не навязывает свои правила.
Чёт меня в философию потянуло ёптъ... (O_o)

В дальнейшем постараюсь написать про использование cookie,
поддержку сессий родную (в web.py 0.3svn)/через flup, поддержку openid посредством модуля weboid , добавление rss и тп.
Надеюсь желание писать не пропадёт ).

PS: под вэб никогда толком не кодил, решил пострадать фигнёй )
Позже напишу пару строк про настройку lighttp + fcgi для этой гадости (хотя там всё стандартно)

Полные исходники можно взять с http://slav0nic.org.ua/static/files/sl_blog.zip
Zada post on 2008-04-15 15:41:26
Спасибо, интересно.
dima post on 2008-11-06 17:05:02
Не подскажете, как использовать шаблон внутри шаблона - например общий шаблон index, а внутри страницы
Andrew post on 2009-07-25 10:15:49
Здравствуйте, я установил web.py-0.32 Мне пришлось заменить metadata = BoundMetaData(db) На metadata = MetaData(db) Это правильно или нет? При запуске code.py получаю: C:\djproject\sl_blog>code.py Traceback (most recent call last): File "C:\djproject\sl_blog\code.py", line 58, in <module> web.run(urls, globals(), web.reloader) AttributeError: 'module' object has no attribute 'run' В чем проблема? Спасибо
slav0nic post on 2009-07-25 11:08:31
это старый пост) в 0.32 давно всё поменяли, смотри туториалы, api для запуска (run) в том числе

web.py