- PVSM.RU - https://www.pvsm.ru -
Как поздравить девушек на работе с прекрасным праздником весны? В этом году хотелось сделать что-то необычное, чем-то удивить их в дополнение к традиционным подаркам и цветам. Так появилось веб-приложение «Бар желаний», созданное за один день с помощью Python [1] и Pyramid [2].

Может быть, после прочтения статьи кто-то решит повторно использовать «Бар желаний» для поздравлений. Возможно, кто-то откроет для себя Pyramid — веб-фреймворк, прекрасно подходящий для быстрого создания небольших веб-проектов. Наконец, можно просто забрать исходный код приложения с GitHub [3] для использования в своих целях.
В статье показан процесс разработки небольшого веб-приложения, начиная с постановки задачи и проектирования и заканчивая развертыванием приложения на сервере. По ходу статьи приведены комментарии к реализации, которые объясняют на примерах некоторые принципы работы веб-приложений в общем и Pyramid в частности. Таким образом, статью можно рассматривать также как руководство по Pyramid для начинающих на примере реальной задачи.
Первого марта, за неделю до Международного женского дня мужская часть нашего коллектива собралась в переговорной для решения непростой проблемы — как поздравить наших девушек с прекрасным праздником весны. В ходе обсуждения родилась простая и в то же время чрезвычайно сложная для реализации идея — в этот праздничный день исполнить все их желания. А если и не все (а мы здраво оценивали свои силы), то хотя бы некоторые, самые простые. Так появился концепт «бара желаний».
«Бар желаний» — виртуальный бар, в котором девушка может выбрать все, что придется ей по душе, указать особые пожелания и оформить заказ. Этот заказ должен обработать ответственный сотрудник и организовать доставку выбранных «желаний» девушке. Меню бара включает такие «желания», как «Кусочек тортика» или «Клубника с мороженым» — десерты, фрукты, напитки. Меню должно быть составлено с юмором. Заказы должны обрабатываться оперативно — нельзя заставлять девушку ждать.
Первым делом — спецификация и проектирование. Определим сценарии использования.
Для того, чтобы официант знал, кому нести выполненный заказ, необходимо различать пользователей веб-приложения. Достаточно знать имя девушки. Значит, попросим ее указать имя перед оформлением заказа. Авторизация нам не нужна: а) мы в локальной сети, злодеев нет; б) вряд ли девушки хотят заполнять формы авторизации в такой день. Зная имя, можно будет обратиться к ней в приложении по имени, это добавляет ощущение индивидуального подхода. Для пущей уверенности при оформлении заказа можно запомнить IP-адрес.
Оповещать официантов решено средствами Google Talk — свой jabber-сервер в локальной сети не прижился, google-аккаунты есть у многих, IM быстрее электронной почты. Наконец, у нас есть опыт работы с Google Talk на Python через xmpppy [4] — на сервере в офисе работает скрипт, который периодически выбирает ответственного за полив цветов и отправляет ему сообщение-напоминание.
Дальнейшее описание предполагает, что вы знакомы с Python и понимаете основные принципы построения веб-приложений.
Начнем с создания директории проекта. Назовем проект wishbar (далее по тексту все пути к файлам даны относительно директории проекта). Создадим модуль веб-приложения (файл server.py):
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
def create_app():
config = Configurator()
app = config.make_wsgi_app()
return app
if __name__ == '__main__':
app = create_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
Для запуска веб-приложений используется простой минималистичный HTTP-сервер с поддержкой WSGI [5] — модуль wsgiref.simple_server [6] из стандартной библиотеки Python. Его вполне достаточно для нашего веб-приложения, как для разработки, так и использования в локальной сети. По крайней мере, пока. В конце статьи будет показано, как Pyramid-приложение подготовить для развертывания на сервере — в боевых условиях мы будем использовать другой сервер.
Проверим доступность приложения через браузер по адресу http://localhost:8080/ [7]. Сервер доступен, но возвращает только 404 Not found.
В веб-приложении нам потребуются стили, скрипты и изображения. Создадим директории js, css, img и добавим view-функции (виды, представления; англ. views) для обработки статики.
def create_app():
config = Configurator()
path = os.path.abspath(__file__)
root = path[:path.rindex("/")]
config.add_static_view("css", "{0}/css".format(root))
config.add_static_view("js", "{0}/js".format(root))
config.add_static_view("img", "{0}/img".format(root))
app = config.make_wsgi_app()
return app
Теперь запросы к URL вида /css/*, /js/*, /img/* будут возвращать файлы из соответствующих директорий.
Каждому поступающему на сервер запросу Pyramid должен сопоставить view-функцию, которая будет его обрабатывать. Если функцию найти не удается, клиенту вернется ответ со статусом ошибки. В Pyramid есть несколько способов зарегистрировать view-функции. Для регистрации обработчиков статики мы применяем императивный подход — при вызове add_static_view [8] создается и регистрируется объект класса pyramid.static.static_view [9], который будет обрабатывать запросы к URL, начинающимся с префикса, переданного в первом параметре.
Займемся HTML. Начнем с «hello world». В конфигурацию веб-приложения добавим маршрут.
config = Configurator()
config.add_route("index_route","/")
Маршрут определяется шаблоном URL (второй параметр). Первый параметр — это имя маршрута. Если запрашиваемый URL подпадает под шаблон маршрута, вызывается view-функция обработки запроса, связанная с маршрутом.
Сопоставление по маршрутам — один из способов привязки view-функций к запросам. Также есть traversal — механизм поиска view-функций на основе дерева ресурсов. В рассматриваемом веб-приложении используется только первый подход. О traversal можно почитать подробнее в документации [10].
Создадим модуль view-функций views.py, добавим в него код обработчика маршрута index_route:
@view_config(route_name="index_route")
def index(request):
return render_to_response('pt/index.pt', { 'name' : 'world' }, request)
и примитивный шаблон pt/index.html в папке pt:
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>wishbar</title>
</head>
<body>
Hello, ${name}!
</body>
</html>
render_to_responseгенерирует HTML-страницу для запросаrequestпо указанному в первом параметре шаблонуpt/index.pt, подставляя данные из словаря во втором параметре.
Зарегистрируем в веб-приложении этот обработчик. Можно воспользоваться императивным подходом, но проще попросить веб-приложение в create_app «просканировать» views.py и зарегистрировать все описанные в нем обработчики.
config = Configurator()
config.add_route("index_route","/")
config.scan("views")
Нам потребуется три страницы — страница для ввода имени, страница меню и страница подтверждения заказа. Все три страницы должны иметь единый дизайн. Поэтому желательно использовать шаблоны. В частности, иметь один базовый шаблон HTML-страницы, в тело которой уже для каждого конкретного экрана будет вставлен соответствующий контент. В поставке Pyramid идет движок шаблонов Chameleon [11], остановим свой выбор на нем. Создадим базовый шаблон pt/base.pt и по шаблону для каждой их трех страниц — pt/login.pt, pt/index.pt (изменим «hello world»), pt/confirm.pt. Для использования шаблона base.pt, как базового, создадим рядом с server.py файл subscribers.py со следующим содержимым:
from pyramid.renderers import get_renderer
from pyramid.events import BeforeRender, subscriber
@subscriber(BeforeRender)
def add_base_template(event):
base = get_renderer('pt/base.pt').implementation()
event.update({'base': base})
Осталось зарегистрировать все обработчики событий из subscribers.py в веб-приложении в функции create_app.
config = Configurator()
config.add_route("index_route","/")
config.scan("subscribers")
@subscriber [12] определяет функцию
add_base_templateкак обработчик внутренних событий Pyramid [13] типаBeforeRender. Обработчик событий BeforeRender [14] вызывается сразу перед рендерингом шаблона. В обработчике мы можем изменить набор renderer globals [15] — именованных значений, доступных в шаблонах. В частности, мы добавляем в этот набор рендерер базового шаблонаbase.ptпод именемbase.
В pt/base.pt объявим нужные слоты расширения:
<div id="content">
<tal:block metal:define-slot="content">
</tal:block>
</div>
Теперь в шаблонах login.pt, index.pt и confirm.pt можно «наследоваться» от базового шаблона:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="base">
<tal:block metal:fill-slot="content">
Hello, ${name}!
</tal:block>
</html>
Каркас веб-приложения готов, можно заняться прикладной логикой. Во view-функции главной страницы надо проверить — знаем ли мы имя пользователя, или это новый клиент. Пусть, когда девушка указывает имя, веб-приложение передает ей cookie с введенным именем. Соответственно, в обработчике главной страницы можем проверить, есть ли cookie в запросе. Если есть — мы знаем имя и можем отобразить список желаний. Если нет — отображаем девушке страницу с формой ввода имени.
if 'username' in request.cookies:
pass
else:
return render_to_response('pt/login.pt', {}, request)
Немного поработаем над версткой и стилями. В помощь возьмем awesome-кнопки [16].

Поскольку awesome-кнопки это не кнопки, а ссылки, добавим немного jQuery [17] для отправки формы при нажатии на кнопку. Данные формы отправим на URL /login/ в форме POST-запроса. Добавим маршрут и обработчик. В обработчике сохраним имя девушки в форме cookie с ключом username и отправим 302 Found на корень веб-приложения.
config.add_route("index_route","/")
@view_config(route_name="login_route")
def login(request):
username = request.params['username']
response = Response()
response.set_cookie('username', value=username, max_age=86400)
return HTTPFound(location = "/", headers=response.headers)
Как и в любом веб-приложении, перенаправление необходимо для того, чтобы при обновлении страницы браузер не предлагал заново отправить POST-запрос с данными формы. Так происходит, если вернуть HTML-страницу в ответе на POST-запрос.
Теперь-то у нас есть уже cookie с именем девушки. При обработке запроса по маршруту index_route можно отобразить список «желаний». Закодируем список желаний в форме списка в Python и передадим его в шаблон вместе с именем девушки.
if 'username' in request.cookies:
username = request.cookies['username']
response = render_to_response("pt/index.pt", { "username" : username, "wishbar" : WISHBAR }, request)
return response
В шаблоне сгенерируем таблицу со строками по каждому из желаний в списке.
<div class="table">
<tal:block repeat="wish wishbar">
<label for="${wish.name}">
<div><input id="${wish.name}" name="wish-${wish.name}" type="checkbox"></input><div class="checkbox"></div></div>
<div>
<div class="title">${wish.title}</div>
<div class="description">${wish.description}</div>
</div>
</label>
</tal:block>
</div>
Смотрим что получилось.

Для стилизации флагов был применен метод [18] с подменой input на div и их связыванием через JavaScript. Правильно было бы добавить предзагрузку картинок при загрузке страницы в соответствии с рекомендациями автора статьи, но этого я не сделал. Поскольку приложение было развернуто в локальной сети, девушки даже не заметили задержки на загрузку изображений.
Внизу страницы добавим поле для ввода «особых пожеланий» и кнопки отправки формы на сервер на корневой URL в форме POST-запроса. В соответствующей view-функции добавим проверку типа запроса. В случае POST получим данные формы, создадим объект заказа и перенаправим пользователя на /confirm/id-заказа.
if request.method == "POST":
username = request.cookies['username']
wishlist = []
for key,value in request.POST.items():
if key.startswith("wish-"):
wishlist.append(NAME2WISH[key[5:]])
special = request.params["special"]
bequick = request.params["bequick"]
order = Order(username,request.remote_addr,wishlist,special,bequick)
ORDERS[order.id] = order
return HTTPFound(location = "/confirm/{}".format(order.id))
Добавим соответствующий маршрут и обработчик /confirm/*.
config.add_route("confirm_route","/confirm/{order}")
@view_config(route_name="confirm_route")
def confirm(request):
order_id = request.matchdict['order']
if order_id in ORDERS.iterkeys():
order = ORDERS.pop(order_id)
notify(order)
return render_to_response('pt/confirm.pt', { "order" : order }, request)
else:
return HTTPFound(location = "/")
На странице подтверждения показываем пользователю содержание заказа и предлагаем «пожелать» что-нибудь еще.

Функция notify предназначена для оповещения официантов о заказе. О ней чуть позже. А пока — закончим с веб-приложением. Осталось немного: надо дать возможность пользователю «выйти» и «войти» под другим именем. Для этого на странице выбора желаний есть ссылка на URL /logout/. Зарегистрируем соответствующую view-функцию logout. В ней достаточно очистить cookie с именем пользователя и перенаправить его на главную.
config.add_route("logout_route","/logout/")
@view_config(route_name="logout_route")
def logout(request):
response = Response()
response.set_cookie('username', value=None)
return HTTPFound(location = "/", headers=response.headers)
Теперь можно заняться обработкой заказов.
Предполагалось, что будет несколько «менеджеров», которых надо оповещать о поступающих заказах. Менеджеры должны договориться, кто примет заказ, и организовать его исполнение. Для оповещения было решено использовать Google Talk. С XMPP [19] из Python удобно работать через xmpppy [4]. Поместим реализацию функции notify в notify.py.
USERNAME = 'username' # @gmail.com
PASSWORD = 'password'
class User():
def __init__(self,uri,name):
self.uri = uri
self.name= name
USERS = {
"user@gmail.com" : User("user@gmail.com",u"Имя пользователя")
}
def notify(order):
cnx = xmpp.Client('gmail.com')
cnx.connect( server=('talk.google.com',5223) )
cnx.auth(USERNAME,PASSWORD,'botty')
message = order.username + " (" + order.remote_addr + "): "
if order.donothing:
message += u"ничего не делать"
else:
message += order.wishstr
if order.bequick:
message += u", быстро-быстро"
for user in USERS.itervalues():
cnx.send(xmpp.Message(user.uri, message, typ='chat'))
Вот и все. Как только поступает заказ, notify отправляет сообщение с информацией о заказе всем пользователям из списка USERS.
Изначально планировалось оповещать несколько «менеджеров». Более того, «менеджер» мог ответить на адрес «бара желаний» — тогда всем остальным пришло бы подтверждение о взятии им заказа в обработку. Для этого даже был написан соответствующий gtalk-бот, код которого можно найти далее в файле notify.py. Бот запускался при старте приложения в отдельном потоке, обрабатывал входящие сообщения и рассылал их по списку USERS.
Но при прогоне оказалось, что с определенного момента сообщения перестают доходить до менеджеров. В результате серии экспериментов было выяснено, что в Google Talk встроена защита от большого потока событий — при отправке более 10 событий приблизительно за 100 секунд Google Talk блокирует отправку событий из клиента на пару минут. О чем мною было найдено лишь краткое упоминание на StackOverflow [20] без конкретных цифр.
Поэтому от идеи использования бота было решено отказаться. Поскольку времени оставалось мало, мы создали комнату на partych.at [21], добавили в нее всех официантов и аккаунт бара желаний. В списке USERS остался только аккаунт комнаты. Теперь, когда кто-то оставлял заказ, сообщение отправлялось в комнату, где его видели все и могли тут же договориться об обработке.
После того, как веб-приложение было готово, встал вопрос о способе его развертывания на сервере в локальной сети под управлением Ubuntu. Я вбил в поиск запрос «Pyramid setup.py» и обнаружил документ [22], с которым мне следовало ознакомиться в самом начале. Документ описывает стандартный способ создания Pyramid-проектов.
Я намерено вынес эту информацию в конец статьи. Во-первых, для того, чтобы начать непосредственно с задачи и кода и не запутать читателя. Во-вторых, привести свой проект к стандартному для Pyramid легко и быстро. Что я и сделал.
Утилита pcreate автоматически генерирует типовую структуру проекта и создает файл setup.py для нового веб-приложения, в котором уже прописаны все зависимости. Необходимо перейти на уровень выше нашего проекта и запустить в консоли pcreate -s starter wishbar.
pcreateпредлагает и другие scaffolds (каркасы) для веб-приложений. Например, alchemy — создает веб-приложение с использованием sqlalchemy [23].
Ключевым отличием Pyramid-проекта является размещение файлов веб-приложения в отдельном пакете wishbar. Что правильно, модули должны лежать в пакетах. В моем же случае файлы лежали в корне проекта. Перенос не составил труда — убран лишний сгенерированный код, добавлены недостающие директивы import для зависимостей между модулями, добавлен вызов create_app из server.py в __init__.py.
После того, как заключительный результат был выложен на GitHub [3], развернуть проект на сервере в локальной сети не составило труда:
cd ~
mkdir wishbar
cd wishbar
git init
git remote add origin "https://github.com/rgmih/wishbar.git"
git pull origin master
sudo python ./setup.py develop
pserve production.ini
При таком способе развертывания веб-приложение запускается под WSGI-сервером Waitress [24] на порту 7777.
Девушки остались очень довольны. В некоторых случаях даже был достигнут вау-эффект.
«Бар исполнения желаний» был успешно запущен 7го марта и проверен в условиях строжайшей секретности. Утром в праздничный день менеджеры и официанты были на своих боевых постах и непринужденно трепались в чате. Первый заказ поступил около 9 утра со словами «я в шоке» в разделе «особых пожеланий». Приложение успешно проработало до конца праздничного дня. Единственное, чего мы не учли в нашей «идеальной» схеме — что в здании ляжет шлюз и все офисы останутся без интернета. Соответственно, оповещения о заказах перестали передаваться в чат, да и сам чат стал недоступен для официантов. Благо, интернет отключили за десять минут до праздничного банкета, и никто не пострадал.
Приложение разработано и проверено под Python 2.7. Но думаю, что под Python 3 все также заработает без значительных измнений. Много важных для настоящего веб-приложения задач не было решено в силу отсутствия необходимости и ограниченности во времени — нет логирования, локализации, обработки ошибок и др. При разработке использовались: Python 2.7.2 [1], Pyramid 1.4 [2], xmpppy 0.5.0rc1 [4], LESS 1.3.0 [25], jQuery 1.9.1 [17]. Я не являюсь профессиональным разработчиком на Python. Поэтому буду признателен любой конструктивной критике и советам, которые позволят мне улучшить статью и свои навыки в этой области.
Исходный код проекта доступен на GitHub [3].
Автор: rgmih
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/29062
Ссылки в тексте:
[1] Python: http://www.python.org/
[2] Pyramid: http://www.pylonsproject.org/projects/pyramid/about
[3] GitHub: https://github.com/rgmih/wishbar
[4] xmpppy: http://xmpppy.sourceforge.net/
[5] WSGI: http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
[6] wsgiref.simple_server: http://docs.python.org/2/library/wsgiref.html#module-wsgiref.simple_server
[7] http://localhost:8080/: http://localhost:8080/
[8] add_static_view: http://www.kemeneur.com/clients/pylons/docs/pyramid/api/configuration.html#pyramid.configuration.Configurator.add_static_view
[9] pyramid.static.static_view: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/static.html#pyramid.static.static_view
[10] документации: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/traversal.html
[11] Chameleon: http://chameleon.readthedocs.org/en/latest/
[12] @subscriber: http://docs.pylonsproject.org/projects/pyramid/en/1.0-branch/api/events.html#pyramid.events.subscriber
[13] событий Pyramid: http://docs.pylonsproject.org/projects/pyramid/en/1.0-branch/narr/events.html
[14] BeforeRender: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/events.html#pyramid.events.BeforeRender
[15] renderer globals: http://docs.pylonsproject.org/projects/pyramid/en/latest/glossary.html#term-renderer-globals
[16] awesome-кнопки: http://www.zurb.com/blog_uploads/0000/0617/buttons-03.html
[17] jQuery: http://jquery.com/
[18] метод: http://www.maratz.com/blog/archives/2006/06/11/fancy-checkboxes-and-radio-buttons/
[19] XMPP: http://en.wikipedia.org/wiki/XMPP
[20] краткое упоминание на StackOverflow: http://stackoverflow.com/questions/1843837/what-is-the-throttling-rate-that-gtalk-applies-to-xmpp-messages
[21] partych.at: http://partychapp.appspot.com/
[22] документ: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/project.html
[23] sqlalchemy: http://www.sqlalchemy.org/
[24] Waitress: http://docs.pylonsproject.org/projects/waitress/en/latest/
[25] LESS 1.3.0: http://lesscss.org/
[26] Источник: http://habrahabr.ru/post/172243/
Нажмите здесь для печати.