Fork me on GitHub
11/12/2006

Введение в Nevow

Начал писать про Nevow в контексте выбора фреймворка, да вышло так, что слишком много нужно пояснять, так что вначале небольшое введение в Nevow.

Сводка

Краткая сводка о Nevow:

  • Самоопределение: a web application construction kit
  • Сайт: http://divmod.org/trac/wiki/DivmodNevow
  • Лицензия: BSD
  • Текущая версия: 0.9.0
  • Используемая мной версия: 0.9.16 (svn)
  • Установка: distutils
  • Зависимости: twisted
  • ORM/DB: отсутствует, рекомендуется axiom
  • Шаблоны: собственные, основанные на XML
  • Обработка форм: собственная (formless), рекомендуется formal
  • Роутинг: обход объектов (object traversal)
  • Пакетирование приложения: не предусмотрено
  • Методы развертывания: twisted, WSGI (не без ошибок см. #1743)

Общая структура приложения

Ключевой объект приложения - наследник nevow.rend.Page, атрибут docFactory которого определяет способ "загрузки шаблона". Я намеренно ограничиваюсь загрузчиками nevow.loaders.xmlfile и nevow.loaders.xmlstr, из названий понятно, что первый подгружает xml-шаблон из файла, второй - из строки.

Так что элементарное приложение, которое будет просто отображать шаблон:

from nevow import rend, loaders
template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">
Hello, world!
</html>
"""

class HelloWorld(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template)

Как же теперь его запустить? Если код находится в helloworld.py, то пишем такой helloworld.tac:

from nevow import appserver
from twisted.application import service, strports
import helloworld

site = appserver.NevowSite(helloworld.HelloWorld())
application = service.Application('NevowHelloWorld')
httpd = strports.service("tcp:8000", site)
httpd.setServiceParent(application)

И вот этот файл "скармливаем" Twisted: twistd -ny helloworld.tac и открываем в браузере localhost:8000.

Роутинг - обход объектов

Теперь встает вопрос о том, как в Nevow назначаются URL'ам те или иные объекты. Назначаются они по свойствам объекта:

  • наличию атрибута или метода child_something, где something - "дочерний URL"
  • словарю children
  • методам locateChild или childFactory

Наиболее простой способ - это, конечно, первый. Допустим, у нас есть потомки nevow.rend.Page с именами OnePage, TwoPage и ThreePage, и теперь мы хотим поставить им в соответствие URL'ы /one/, /two/ и /three/ соответственно. С child_* это делается так:

class Root(rend.Page):
    addSlash = True     # необходим для того, чтобы /one и /one/ имели одинаковый смысл
    docFactory = loaders.xmlstr(template_root)   # точно так же как и в HelloWorld
    child_one = OnePage()   # можно повторить код HelloWorld, но с другим шаблоном
    child_two = TwoPage()
    child_three = ThreePage()

Второй способ не принципиально отличается от первого, так что я приводить его не буду, а желающие могут посмотреть код отдельно.

locateChild и childFactory являются более низкоуровневыми вещами, но позволяют выстраивать и более "хитрые" схемы. Причем, childFactory есть упрощенный locateChild. Чтобы лучше понять, объясню как Nevow определяет кому обрабатывать текущий URL.

При обращении по определенному URL, Nevow передает его в виде кортежа сегментов обработчику locateChild. Например, URL /foo/bar трансформируется в сегменты ('foo', 'bar'), "корневой" URL / - в сегмент ('', ), /foo/bar/baz/ - в ('foo', 'bar', 'baz', ''). Обработчик обрабатывает URL как ему положено, и возвращает результат и необработанные сегменты. Чтобы упростить эту процедуру, используют childFactory, которой передается только текущий сегмент, и, соответственно, childFactory возвращает только результат, необработанные сегменты возвращать не нужно. Так вот, locateChild по умолчанию настроен на обработку child_*, children и childFactory, так что если вы переопределяете этот самый низкоуровневый обработчик, то не удивляйтесь, что более высокоуровневые перестали работать ;-)

Пример с childFactory выглядит так:

class Root(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template_root)

    def childFactory(self, context, name):
        choices = {
            'one': PageOne(),
            'two': PageTwo(),
            'three': PageThree(),
        }
    return choices.get(name)

Пример фактически повторяет действие словаря children, точнее его "машинерию". Ну и если childFactory возвращает None, то полагается, что данному URL'у не может быть сопоставлен ни один объект и это означает ошибку 404 Not found. Кстати, в этом месте появляется вездесущий (в Nevow он встречается сплошь и рядом) контекст.

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

Шаблоны

Шаблоны Nevow представляют собой XML-файл, в котором пространство имен http://nevow.com/ns/nevow/0.1 указывает обработчику на управляющие конструкции. Пространство имен должно указываться в начальном теге шаблона так:

<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">

...
</html>

В этом случае управляющие теги/атрибуты будут иметь вид nevow:tag. Если указать пространство имен таким образом (иногда так сокращают):

<html xmlns:n="http://nevow.com/ns/nevow/0.1">
...
</html>

то управляющие теги/атрибуты будут иметь вид n:tag. Однако на мой взгляд это тот случай, когда простота хуже воровства - такое сокращение резко снижает читабельность шаблона.

Язык шаблонов Nevow относится к TAL (template attribute language), т.е. управляющие конструкции представляют собой атрибуты обычных XHTML-тегов. В Nevow определены два атрибута, которые вызывают Python-код:

  • nevow:pattern - помечает данный тег и дает имя образцу
  • nevow:render - вызывает соответствующий метод (render_name) объекта и заменяет узел шаблона результатом
  • nevow:data- вызывает соответствующий метод (data_name) объекта и устанавливает спец-данные для узла шаблона

Существует ряд встроенных "отрисовщиков" (renderers):

  • data - отрисовывает данные как есть и вставляет в текущий узел
  • string - отрисовывает данные как строку и вставляет в текущий узел
  • sequence - делает итерации текущих данных, копируя образец (pattern) item для каждого элемента. Также использует образцы header, divider, footer и empty.
  • mapping - вызывает .items() у текущих данных и заполняет слоты-ключи значениями

Nevow не является "чистым TAL", так что в нем определены и несколько тегов:

  • nevow:slot - определяет слот, который необходимо "заполнить"
  • nevow:attr - указывает, что данным узлом устанавливается атрибут родительского узла
  • nevow:invisible - "невидимый" тег (в конечный результат он не попадает, однако все конструкции, определенные в нем выполняются наравне с обычными).

Что ж, лексические конструкции известны, давайте смотреть, что с ними можно сделать. Рассмотрим типичные use-cases:

Вставка данных

Необходимо вставить некие данные в шаблон. Для этого воспользуемся встроенными "отрисовщиками" data или string, либо тегом nevow:slot:

from nevow import rend, loaders

from datetime import datetime
from nevow._version import version as nevow_version
from twisted._version import version as twisted_version

template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">
<p>Hello, <em nevow:data="name" nevow:render="data">Dude</em></p>

<p>Current time is: <em nevow:data="time" nevow:render="string">2006-12-10 14:49:52</em></p>

<p nevow:render="fillSlots">
This page running by Twisted <nevow:slot name="twisted" /> and Nevow <nevow:slot name="nevow" />

</p>
</html>
"""

class Root(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template)

    data_name="Nevow newbie"

    def data_time(self, context, data):
        return datetime.now()

    def render_fillSlots(self, context, data):
        context.tag.fillSlots('twisted', twisted_version.short())
        context.tag.fillSlots('nevow', nevow_version.short())
        return context.tag

Как видно, данные для шаблона - это либо атрибут объекта, либо метод (с сигнатурой (self, context, data)). Что касается nevow:slot, то его есть смысле применять там, где происходит простая подстановка данных, однако в этом случае все слоты нужно "оборачивать" отдельным "отрисовщиком".

Условные переходы

Нужно в зависимости от некоего условия выводить те или иные данные. Единственного решения нет, все зависит от ситуации. Предлагаемые решения:

  • в случае простых значений - условие переносится в data_-метод

  • в случае комплексных (т.е. когда выводимый результат состоит из более чем одного тега) - использование образцов (pattern)

from nevow import rend, loaders, inevow
   
template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">
<p>Hello, <em nevow:data="name" nevow:render="data">Dude</em></p>

    <div id="definition" nevow:render="definition">
       <p nevow:pattern="twisted_definition"><strong>Twisted</strong>
       is an event-driven networking framework written in Python</p>

       
       <p nevow:pattern="nevow_definition"><strong>Nevow</strong>
       is a web application construction kit written in Python.</p>
   </div>
</html>
"""

class Root(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template)
    counter = 1

    def data_name(self, context, data):
        self.counter += 1
        if divmod(self.counter, 2)[1] == 0:
            return "Nevow newbie"
        else:
            return "Twisted hacker"

    def render_definition(self, context, data):
        query = inevow.IQ(context)
        twisted_pattern = query.onePattern('twisted_definition')
        nevow_pattern = query.onePattern('nevow_definition')

        if divmod(self.counter, 2)[1] == 0:
            return context.tag[nevow_pattern]
        else:
            return twisted_pattern

В данном коде применены обе практики - простое условие в data_name и использование шаблонов в render_definition. Если с первым более-менее понятно, то насчет второго стоит чуть пояснить.

Если вы вставляете атрибут nevow:pattern в тег, то без дополнительных "манипуляций" данные не будут отображаться в конечном результате. Т.е. если убрать тег <div nevow:render="definition">..</div>, то текст про Nevow и Twisted не будет отображен. Поэтому, мы и добавили отрисовщик definition для того, чтобы выбрать, какой образец будет показан. Теперь пару слов об отрисовщике. Результат отрисовщика переписывает тег полностью. Т.е. если отрисовщик бы вернул текст "haba haba", то вместо тега <div>..</div> стоял бы данный текст. Если же есть желание, чтобы данный текст появился внутри тега, то нужно возвращать context.tag["haba haba"]. Для избежания путаницы стоит придерживаться одного из этих вариантов. Например, я всегда отрисовываю данные только внутри тега, а если место, где нужно отрисовать что-либо не совпадает с каким-либо XHTML-тегом, то использую Nevow-тег nevow:invisible. В примере специально в одном случае выводится с context.tag, а в другом - без, чтобы вы смогли оценить влияние. Что же касается образцов, то тут методика такова: при помощи интерфейса nevow.inevow.IQ объект контекста приводится к объекту запроса (query). Из запроса, при помощи onePattern мы и получаем искомый образец. И затем, в зависимости от ситуации, выводим тот или иной образец.

Циклы

Зачастую требуется выводить некий список данных. На этот случай в Nevow есть стандартный отрисовщик sequence. Он обладает достаточно широкими возможностями и сейчас мы их продемонстрируем:

from nevow import rend, loaders

template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">
<ul nevow:data="fruits" nevow:render="sequence">

<p nevow:pattern="empty">There is no fruits</p>
<p nevow:pattern="header">There is some fruits:</p>
<li nevow:pattern="item" nevow:render="data" class="odd">Some fruit here</li>

<li nevow:pattern="item" nevow:render="data" class="even">Another fruit here</li>
<p nevow:pattern="footer">...and nothing more</p>
</ul>

</html>
"""

class Root(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template)

    counter = 1

    def data_fruits(self, context, data):
        self.counter += 1
        if divmod(self.counter, 2)[1] == 0:
            return ('apple', 'orange', 'pear', 'apricot')
        else:
            return ()

Иными словами, образцы header и footer подставляются в начало и конец в любом случае, образец empty отрисовывается если передана пустая последовательность, item - на каждом элементе последовательности, причем данные для узла уже заполнены (т.е. nevow:data не нужен). Поскольку в нашем случае указано два образца item, то они заполняются попеременно, так что можно, например, чередовать цвета строк для лучшей читабельности.

Фрагменты шаблонов

Зачастую есть смысл выносить общий дизайн в один шаблон, а на различных страницах лишь "рисовать" неповторяющиеся элементы. В Django templates это делает {% block somename %}, в Nevow, как и для много другого, нет специальной конструкции, и нет одного способа решения. Таких способа два:

  • использование nevow:slot
  • использование отрисовщика

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

from nevow import rend, loaders

main_template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">
<p nevow:render="fillAllSlots">

Some text before first (inserted by nevow:slot tag) fragment. 
<em><nevow:slot name="content_fragment" /></em>
Some text after first fragment.
</p>
<p>
Some text before second (inserted by nevow:render attribute) fragment.
<em nevow:render="fragment" />

Some text after second fragment.
</p>
</html>
"""

fragment_template="""
<p xmlns:nevow="http://nevow.com/ns/nevow/0.1">
Fragment may use own data and renderers, for example this is the counter: 
<strong nevow:data="counter" nevow:render="data">counter here</strong>

</p>
"""

class Fragment(rend.Fragment):
    docFactory = loaders.xmlstr(fragment_template)

    def __init__(self, counter_step=1):
      self.counter = 0
      self.counter_step = counter_step
      super(Fragment, self).__init__()

   def data_counter(self, context, data):
        self.counter += self.counter_step
        return "%.2f" % self.counter

class Root(rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(main_template)
    fragment_one = Fragment(0.346)
    fragment_two = Fragment(0.724)

    def render_fillAllSlots(self, context, data):
        context.tag.fillSlots('content_fragment', self.fragment_one)
        return context.tag

    def render_fragment(self, context, data):
        return context.tag[self.fragment_two]

Думаю, код говорит сам за себя.

Формы

У Nevow есть два инструмента обработки форм: стандартный formless и не идущий в поставке, но рекомендуемый formal (ранее называвшийся forms). Я не буду приводить пример formless, а сразу перейду к formal.

Как formless, так и formal подразумевают, что форма не только обрабатывается и валидируется, но и генерируется автоматически.

Что ж, давайте попробуем что-нибудь с formal:

from nevow import rend, loaders, inevow
import formal

template ="""
<html xmlns:nevow="http://nevow.com/ns/nevow/0.1">

<link rel="stylesheet" type="text/css" href="formalcss" />
<p nevow:render="form simple" />
</html>

"""

class Root(formal.ResourceMixin, rend.Page):
    addSlash = True
    docFactory = loaders.xmlstr(template)

    child_formalcss = formal.defaultCSS

    counter = 0

    def form_simple(self, context):
        self.counter += 1
        form = formal.Form()
        form.addField("number", formal.Integer(required=True))
        form.addField("name", formal.String(missing="absence"))
        form.addField("id", formal.Integer(immutable=True))
        form.addAction(self.action)
        form.data = {'id': self.counter}
        return form

    def action(self, context, form, data):
        return "You've entered %r" % data

Как видно, необходимо наследоваться от formal.ResourceMixin и добавить спец-метод form_name, где name - имя формы (в нашем случае - simple). Этот метод должен вернуть объект формы. В formal определены некоторые типы данных (Integer, String, Boolean и т.д.). Конструктору типа передаются параметры, смысл которых вполне очевиден: required - поле необходимо для заполнения; missing - значение, передаваемое обработчику формы в случае, если поле не заполнено; immutable - поле не активно, не доступно для редактирования. Начальные значения полей формы можно задавать при помощи атрибуты data формы, в виде словаря.

В целом, схема приемлема, однако я не нашел ни одного примера (а документация по formal отсутствует как класс) как сделать, чтобы данные формы выводились на этой же странице без редиректа и промежуточных хранилищ. Все примеры в обработчике либо просто писали в лог введенные данные, либо сохраняли введенные данные в какое-либо хранилище, а после делали редирект на страницу "все ок, данные записаны". Еще один момент, который остался для меня непонятным - как делать GET-формы? Понятное дело, я могу сделать их "руками", но как их валидировать? тоже "руками"? Эти вопросы остались для меня без ответов.

Заключение

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

Код, как всегда, на code.google.com

Комментарии

Все статьи