2008-04-03

Директивы для создания видов и форм.txt

  2008-04-03 14:05

Zope может обрабатывать различные запросы, соответствующие различным протоколам. Общие параметры директив: Все директивы создания видов базируются на создании адаптера **zope:adapter**, а потому обладают рядом общих параметров: for -- интерфейс, для которого будет отображаться вид (иногда носит характер маркера), class -- mix-in класс, домешиваемый в вид, благодаря которому вид может делать какие-то нетривиальные преобразования для отображения объекта, template -- темплейт, при помощи которого выводится вид, type -- скин к которому относится вид (иногда используется layer), permission -- доступ, необходимый для отображения вида (не надо путать с доступом, необходимом для доступа к объекту: они могут различаться), name -- имя вида (фактически, строка, которая используется в HTTP-запросе), title -- название вида в меню, menu -- меню, в которое должен быть включен вид. Директивы пространства имен zope: Дл ...

Директивы для создания видов и форм:

В статье Введение в компоненты.txt было введено понятие адаптера и описаны различные способы его использования. Но одним из основных назначений адаптера является адаптирование объектов к представлению, передаваемому в ответ на запрос клиента. Такой адаптер принято называть адаптером вида или просто видом. Zope может обрабатывать различные запросы, соответствующие различным протоколам. В рамках статьи будут рассмотрены HTTP-запросы.

Технически, адаптер вида - это именованный мультиадаптер контент-компонента и запроса к произвольному интерфейсу. Для одного и того же набора адаптируемых и предоставляемых интерфейсов может существовать несколько именованных адаптеров, различающихся только именем. В статье API работы с реестрами.txt описано как можно на низком уровне зарегистрировать такой адаптер, получить или его или полный список всех адаптеров, релевантных интерфейсам, но на практике, для случая адаптера вида существует ограниченое количество вариантов использования, которые будут описаны ниже. Для каждого варианта использования существует ZCML-директива, сводящая его реализацию к обычной параметризации.

В случае адаптеров вида для http-запроса, имя используется как название страницы в пространстве имен ++view++, сокращенно обозначаемое как @@.

Общие параметры директив:

Все директивы создания видов базируются на создании адаптера zope:adapter, а потому обладают рядом общих параметров:

for
интерфейс, для которого будет отображаться вид (иногда носит характер маркера),
class
mix-in класс, домешиваемый в вид, благодаря которому вид может делать какие-то нетривиальные преобразования для отображения объекта,
template
темплейт, при помощи которого выводится вид,
type
скин к которому относится вид (иногда используется layer),
permission
доступ, необходимый для отображения вида (не надо путать с доступом, необходимом для доступа к объекту: они могут различаться),
name
имя вида (фактически, строка, которая используется в HTTP-запросе),
title
название вида в меню,
menu
меню, в которое должен быть включен вид.

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

Директивы пространства имен zope:

Для директив создания и управления видами существует пространство имен browser, и на практике используются они. Но что бы проиллюстрировать интегрированность видов в компонентную модель, в этом разделе дан пример того, как сделать адаптер вида "стандартным" способом, т.е. без использования специализированных инструментов.

Директива zope:adapter:

Для начала продемонстрируем создание простого вида "в лоб", т.е. как обычного адаптера. Здесь, так же как и во всех последующих случаях, начнем с кода директивы, и лишь потом, при необходимости, сошлемся на другие исходные коды. Для иллюстрации возьмем вид с гистограммой из примера quotaview, описанного в статье про виды, и на его основе сделаем вид как адаптер. Код ZCML:

                <adapter
                  factory=".histogram2.Histogram2"
                  for="quota.interfaces.IQuota zope.publisher.interfaces.browser.IDefaultBrowserLayer"
                  name="histogram2.html"
                  provides="zope.publisher.interfaces.browser.IBrowserPublisher"
                  permission="zope.ManageContent"
                  />

У директивы adapter есть еще много опций, которые при создании адаптера вида лучше вообще не знать, чтобы не использовать случайно, а описание полезных опций следует:

factory
фабрика адаптера, примерно соответствует параметру class;
for
напомним, что вид - это мультиадаптер, поэтому в данном случае указывается не только адаптируемый объект, но и запрос. В специализированных директивах он маскируется параметром type или layer;
provides
предоставляемый интерфейс. Несмотря на то, что публикация ищет адаптеры к интерфейсу zope.interface.Interface, при этом в директиве создания вида на адаптер накручивается логика защиты, в нашем случае такой возможности нет, поэтому приходится приводить не к тому интерфейсу, который требуется, а к тому, который реально используется публикацией.

Напомним, что адаптер должен быть вызываемым, а значит .histogram2.Histogram2 класс придется написать вот так:

                from zope.interface import implements
                from histogram import Histogram
                from zope.app.pagetemplate.viewpagetemplatefile \
                            import ViewPageTemplateFile
                from zope.publisher.interfaces.browser import IBrowserPublisher
                from zope.publisher.interfaces import NotFound

                class Histogram2(Histogram) :

                    implements(IBrowserPublisher)
                    template = ViewPageTemplateFile("histogram.pt")
                    __name__ = "histogram2.html"

                    def __init__(self,context,request) :
                        self.context = context
                        self.request = request
                        super(Histogram2,self).__init__(context,request)

                    def browserDefault(self,request) :
                        return self.__call__,()

                    def publishTraverse(self, request, name):
                        raise NotFound(self, name, request)

                    def __call__(self,*kv,**kw) :
                        return self. template(self,*kv,**kw)

Этот адаптер наследует mix-in обычного вида гистограммы, но предоставляет интерфейс IBrowserPublisher и использует ViewPageTemplateFile для вызова темлейта. Отчасти это достаточно общая техника построения адаптера страницы (см. например "Использование модификатора method:")

Напоминаем, что директива zope:adapter приведена здесь только как искусственный пример, как первооснова. В реальности виды создаются специализированными директивами.

Директива zope:view:

Это довольно странная директива. Напоминает какое-то ископаемое, возможно, унаследованное из времен python2.0. Если изучить исходник, это точное преобразование директивы adapter для случая вида (чтобы проще было декларировать), поэтому иллюстрировать будем на том же примере, итак, ZCML:

               <view
                  factory=".histogram3.Histogram3"
                  for="quota.interfaces.IQuota"
                  type="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
                  name="histogram3.html"
                  provides="zope.interface.Interface"
                  permission="zope.ManageContent"
                  allowed_interface="zope.publisher.interfaces.browser.IBrowserPublisher"
                  />

Класс Histogram3 в этом примере отличается от Historgram2 предыдущего примера только именем:

                from histogram2 import Histogram2

                class Histogram3(Histogram2) :
                    __name__ = "histogram3.html"

Расскажем про другие опции:

for
полностью аналогичен обычному адаптеру, но несмотря на то, что определяется по-прежнему мультиадаптер, содержит только один интерфейс. Второй перенесен в специально выделенную опцию type,
type
содержит интерфейс запроса, для вида имеет смысл "Скин", подробнее о том, как устроен скин можно прочитать в Скины.txt,
allowed_interface
позволяет разрешить использование поля этого интерфейса с указанным в permission допуском. Обратите внимание, что теперь это выделено в отдельное поле, тогда как раньше было смешано с provides, обсуждение - ниже.
allowed_attributes
в примере не использован, но, тем не менее, полезен. Позволяет разрешить использование перечисленных атрибутов, при наличии допуска, указанного в permission.

Остальные опции не отличаются от стандартного случая.

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

При публикации (непосредственно или через адаптацию), используется другой интерфейс - IBrowserPublisher, который в нашем случае пришлось указать в аргументе allowed_interface. Тогда в чем смысл аргумента provides? Оказывается, действительно, можно породить адаптер вида к любому интерфейсу, и либо использовать его поля в TAL-выражениях, либо построить иерархию видов, т.е. объявить вид от этого вида. Это немножко сложно для нашей статьи, тем более, что существуют более современные способы сделать что-то подобное, но, тем не менее, такой подход может оказаться удобным решением в каком-то случае, поэтому он упомянут здесь.

Директивы пространства имен browser:

Директива browser:page :

Директива browser:page - это самый простой способ создать адаптер вида. Продолжим вариации на тему вида с гистограммой, и приведем (сокращенно) тот пример, который приведен в статье про виды:

                <browser:page
                  class=".histogram.Histogram"
                  for="quota.interfaces.IQuota"
                  layer="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
                  name="histogram.html"
                  permission="zope.ManageContent"
                  template="histogram.pt"
                  menu="zmi_views" title="Histogram"
                  />

Самое заметное отличие от директивы zope:view - опция factory превратилась в class. И содержит эта опция теперь не фабрику адаптера, а только небольшой mix-in класс, который будет домешан к базовому классу при создании фабрики адаптера. Этот базовый класс нельзя увидеть даже в исходниках, так как он неявно собирается из мелких деталей, но в принципе он подобен тому, что было воссоздано для директив zope:adapter и zope:view.

Менее заметно, но вполне логично появление опции template, в которой указывается темплейт, используемый аналогично коду класса Histogram2: темплейт вызвается чтобы отрисовать страницу вида, а в параметрах в него будут переданы контекст вызова (будет доступен в TAL как context) и сам адаптер (будет доступен как view). Последний факт является основной причиной введения опции class: если бы темплейт просто работал с контекстом, то не было бы нужды в mix-in'е. Но сложные преобразования проще пишутся на питоне, чем на ZPT, а адаптер вида - это и есть преобразование.

Вместо опции template могла бы быть использована опция attribute - в ней указывается вызываемый атрибут, который вернет текстовое содержимое страницы. В принципе, эта опция используется редко.

Другие отличия не столь явны: добавилась пара опций для обслуживания меню (menu и title), смысл которых не отличается от изложенного в самом начале статьи, и исчезла опция provides -- этот вид не может иметь вложенных видов.

Директива browser:pages :

Эта директива - обычный вынос инварианта, которая не несет ничего нового, кроме того, что позволяет легко и быстро задать несколько адаптеров вида для одного интерфейса, как всегда ZCML:

               <browser:pages
                  class=".histogram.Histogram"
                  for="quota.interfaces.IQuota"
                  layer="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
                  permission="zope.ManageContent"
                  />
                    <browser:page 
                        name="histogram.html" 
                        template="histogram.pt"  
                        menu="zmi_views" title="Histogram"
                        />
                </browser:pages>  

Пример получен прямо из примера директивы page, чтобы подчеркнуть смысл этой директивы: групповая декларация страниц сходных по применению (один интерфейс, один допуск, и даже один mix-in), хороший пример практического использования такой директивы приведен в статье "Использование модификатора method:": многостраничный веб-интерфейс для единого сценария работы пользователя.

Директива browser:view :

Эта директива вводит вид с несколькими вложенными видами и ее можно рассматривать как расширение директивы pages: если директива pages просто выносит инвариант из группы определений страниц, то директива view дает этой группе имя. В принципе, в примере из "Использование модификатора method:" следовало бы использовать именно view, так как это снимает проблему конфликта между страницами c одинаковыми именами в двух разных веб-интерфейсах.

В качестве примера воспользуемся данным ранее примером с pages:

               <browser:view
                  class=".histogram.Histogram"
                  for="quota.interfaces.IQuota"
                  layer="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
                  permission="zope.ManageContent"
                  name="histogram"
                  menu="zmi_views" title="Histogram View"
                  >
                    <browser:page name="stat.html" template="stat.pt"/>
                    <browser:page name="graph.html" template="graph.pt"/>
                    <browser:defaultPage name="graph.html"/>
                 </browser:view>

Очевидно, что с программной точки зрения это такой же вид, как и все остальные виды этого раздела. Можно сразу заметить отличия от директивы pages:: появление опций включения в меню в основной директиве и опции name, а также пропадание опций включения в меню во вложенных директивах page и появление вложенной директивы defaultPage. Все эти отличия вполне очевидны и не требуют особых комментариев:

  • В меню добавляется основной вид под именем @@histogram, при вызове которого вызывается вид по умолчанию;
  • Вложенные виды нельзя добавить в меню;
  • Все виды работают как одна группа под именем @@histogram, их URL:
                        http:/....../@@histogram
    
                        http:/....../@@histogram/graph.html
    
                        http:/....../@@histogram/stat.html
    

К сожалению, в скине ротердам такая конструкция нарушает работоспособность меню, поэтому ее применение ограничено, но возможно.

Директивы создания форм:

Основная идея директив создания форм - дать возможность не просто создать адаптер вида, но и автоматически заполнить содержимым сам вид, т.е. HTML-представление, которое генерирует адаптер. Все формы строятся на основе схемы интерфейса, и директивы различаются только особенностями примения.

Все директивы создания форм сложные, т.е. имеют вложенные директивы, список которых не отличается разнообразием: это директива widget, позволяющая для любого поля схемы явно указать виджет, которым его следует отображать (обычно виджеты подбираются автоматически, это тоже адаптер поля к запросу и т.п., об этом можно прочитать в как работают и пишутся поля и виджеты).

Для опытов в этом разделе будем использовать уже знакомый нам по статье Использование сложных полей ввода.txt продукт bookmarknote.

Директива addform :

Форма добавления работает в довольно необычных условиях, когда объекта еще нет, а схема уже есть. Поэтому директива содержит ряд необычных опций, которые лучше всего рассмотреть на примере. Итак, ZCML:

                <addform
                    label="Add BookMark"
                    name="AddBookMark.html"
                    content_factory="..bookmarknote.BookmarkNote"
                    permission="zope.ManageContent"
                    schema="..interfaces.IBookmarkNote"
                    >
                  <widget field="mainurltext" class=".widgets.UrlTextWidget" />
                  <widget field="urltext"  class=".widgets.UrlTextTupleWidget"/>
                 </addform>

Сразу видны явные отличия от директив создания адаптеров вида, это вышеописанные вложенные директивы widget и опция schema, в которой указана схема, по которой будет создана форма. Кроме того (и это специфика директивы addform) есть несколько опций, описывающих процесс создания контент-компонента:

content_factory
вызываемый компонент, результатом вызова которого является создаваемый компонент; обычно используется класс,
content_factory_id
имя утилиты с интерфейсом IFactory, используемой для создания компонента,
fields
имена полей схемы, которые должны войти в форму, порядок имен соответствует порядку их появления в форме,
arguments
имена полей, значения которых будут переданы фабрике как позиционные аргументы,
keyword_arguments
имена полей, значения которых будут переданы фабрике как аргументы, передаваемые по ключу,
set_after_add
имена полей, значения которых будут установлены в компоненте после его добавления в контейнер,
set_before_add
имена полей, значения которых будут установлены в компоненте до его добавления в контейнер,

Сделаем небольшое отступление, чтобы пояснить смысл этих опций с точки зрения алгоритма создания контента. Алгоритм состоит из следующих шагов:

  1. Контент создается фабрикой, которой передаются позиционные и ключевые аргументы;
  2. Контент приводится к интерфейсу схемы формы, и в нем устанавливаются значения полей, перечисленные в set_before_add,
  3. Отсылается сообщение ObjectCreatedEvent,
  4. Контент добавляется в контейнер (подробности этой процедуры можно прочитать в статье "Как использовать namechooser), в процесе добавления отсылается сообщение ObjectAddedEvent,
  5. Контент приводится к интерфейсу схемы формы, и в нем устанавливаются значения полей, перечисленные в set_after_add,
  6. Отсылается сообщение ObjectModifiedEvent,
  7. Выполняется редирект, который управляется методом nextUrl() базового класса формы.

Как и любая директива создания вида, addform имеет поля class и template. Поле class содержит mix-in, который домешивается к классу zope.app.form,browser.add.AddView, реализующему вышеописанный алгоритм. Его изменение возможно, но редко используется (основная польза - это возможность перекрыть nextURL()). В качестве темплейта по умолчанию используется zope/app/form/browser/add/add.pt, перекрыв его, можно добавить в форму дополнительные кнопки или поля, подробный рассказ о том, как это сделать можно прочитать в статье HOWTO add button into standard add and edit form.txt.

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

Есть у addform и опция for, но поскольку регулирование отображения содержимого меню добавления происходит за счет сложных ограничений в интерфейсах контейнеров и их содержимого, эта форма, в сущности, тоже бесполезна.

Директива editform:

Основное назначение директивы editform - создание форм редактирования компонент, основанное на их интерфейсах. Приведем пример:

               <editform
                    schema="..interfaces.IBookmarkNote"
                    label="Edit"
                    name="edit.html"
                    permission="zope.ManageContent"
                    menu="zmi_views" title="Edit"
                    >
                  <widget field="mainurltext" class=".widgets.UrlTextWidget" />
                  <widget field="urltext"  class=".widgets.UrlTextTupleWidget"/>
                </editform>

Текст директивы практически полностью идентичен тексту addform, и смысл всех приведенных опций тоже. Но есть два небольших отличия. Опции включения в меню действительно работоспобны и используются повсеместно. Другое отличие касается опции for: ее назначение - указать, для какого интерфейса эта форма будет отобрана в качестве адаптера вида. Если опция for не указана, то в качестве ее значения используется содержимое обязательной опции schema.

Более интересная ситуация возникает когда опция for указана и ее значения не совпадает со значением опции schema: в этом случае адаптер вида будет отображатся для объекта, схема интерфейса которого вообще говоря не совпадает со схемой объекта. В этом случае отображение формы и сохранения данных будут происходит через адаптер объекта к интерфейсу схемы. Обратите внимание, что в этом случае событие ObjectModifiedEvent может не генерироваться, и другие службы сайта (в частности, поисковый каталог), не узнают об изменении объекта. Предположительно, в таких случаях генерацию события должен брать на себя адаптер объекта или же в классе редактирования должен перекрываться метод changed.

Директива form :

Если директива editform в состоянии редактировать адаптируемый объект, то директива form позволяет редактировать произвольные данные, хотя и требует задать класс для получения этих данных. Хотя ее возможности значительно шире, часто, директива form используется для редактирования схем, полученных в результате адаптации. Приведем пример использования директивы form вместо директивы editform:

               <form
                    class = form.FormView
                    schema="..interfaces.IBookmarkNote"
                    for="note.note.interfaces.INote"
                    label="Edit"
                    name="edit.html"
                    permission="zope.ManageContent"
                    menu="zmi_views" title="Edit"
                    >
                  <widget field="mainurltext" class=".widgets.UrlTextWidget" />
                  <widget field="urltext"  class=".widgets.UrlTextTupleWidget"/>
                </form>

Отличий появилось два: указан класс для доступа к данным и атрибут for, в котором указан интерфейс, для которого отображается форма. Обратите внимание: адаптера INote к IBookmarkNote вообще говоря, не существует (мы не писали).

Роль такого адаптера берет на себя класс доступа к данным, который должен обязательно предоставлять методы getData() and setData(), посмотрим на его пример:

                from zope.zapi import getUtility
                from zope.schema import getFieldNames
                from bookmarknote.interfaces import IBookmarkNote

                class Edit :
                    def getData(self,*kv,**kw) :
                        util=getUtility(IBookmarkNote,context=self.context, name=self.context.__name__)
                        return [ (x,getattr(util,x)) for x in  getFieldNames(IBookmarkNote)]

                    def setData(self,d,**kw) :
                        util=getUtility(IBookmarkNote,context=self.context, name=self.context.__name__)
                        for x in getFieldNames(IBookmarkNote) :
                            setattr(util,x,d[x])
                        return True

                **ПРИМЕР НЕ ТЕСТИРОВАЛСЯ**

Код, приведенный в примере демонстрирует как могли бы быть реализованы аннотации, если бы аннотаций не было. Раз аннотации есть - то, пожалуйста, не надо так писать! Тем не менее, пример демонстрирует то, как форма, отображаемая для одного объекта позволяет редактировать совсем другой объект.

Директива schemadisplay :

Директива schemadisplay - точная копия директивы editview с единственным исключением: для отображения используются виджеты, не допускающие ввод данных:

                <browser:schemadisplay
                    schema="..interfaces.INote"
                    for="..interfaces.INote"
                    label="View"
                    name="view.html"
                    permission="zope.Public"
                    menu="zmi_views" title="View"
                    />

Иными словами, этой директивой создаются страницы, представляющие данные, но не позволяющие их редактировать. Такие виды удобно указывать в качестве видов по умолчанию.

Директива browser:containerViews :

Директива containerViews стоит особняком от остальных директив создания видов, так как она управляет созданием видов для редактирования конкретного типа объекта: контейнера:

            <containerViews
                for="..interfaces.INotebook"
                index="zope.View"
                contents="zope.View"
                add="zope.ManageContent"
                />

Эта директива зарегистрирует для интерфейса, указанного в опции for, заранее подготовленные адаптеры видов для контейнера (заменить их нельзя, по крайней мере средствами этой директивы) и позволит задать для них права доступа. Вот о правах расскажем чуть подробнее:

index
права на вид по умолчанию, отображает содержимое контейнера без возможности редактирования, соответствует интерфейсу IReadContainer,
contents
права на вид для редактирования, позволяет удалять и переименовывать объекты, соответствует интерфейсу IWriteContainer,
add
права на вид добавления, содержит список форм добавления, никакому интерфейсу прямо не соответствует.

Подразумевается, что интерфейс, указанный в опции for, играет роль маркера компонента, предоставляющего, как минимум, интерфейсы IReadContainer, IWriteContainer.

Директива browser:defaultView :

Директива defaultView не создает адаптеров вида, а только задает, какой из адаптеров будет использован по умолчанию. Желательно, чтобы по умолчанию использовался адаптер вида с наименьшими требованиями к доступу, лучше всего - публичный. Если публичного адаптера вида нет, то может быть это повод его написать, так как иначе не всем пользователям будет удобно работать:

            <browser:defaultView
              for="..interfaces.INote"
              name="view.html"
              />

Заключение:

Вышеизложенный справочник по директивам создания адаптеров видов описывает достаточно материала, чтобы создать интерфейс администрирования контента Zope3, особенно при использовании готового скина. Хотя эти директивы полезны и для создания интерфейса конечного пользователя, для него все-таки необходимо знать много дополнительной информации.

Официальный сайт Zope3 Московская группа изучения реактивного движения The Dream Bot Site noooxml