2008-01-11

Скины.txt

  2008-01-11 19:04

Вообще говоря, разработать дизайн, полностью замещающий собой административный дизайн Zope - достаточно нетривиальная задача, которая сводится к тому, что нужно переписать: 1. Немного теории : Когда-то все было по-иному сложнее и менее следовало Zope3 style. Изучая исходники Zope и видя некоторые несоответствия написанному, обращайте внимание на слово "deprecated" и старайтесь понять, что было вчера, а что будет завтра. Отображение пользовательских интерфейсов в Zope3 основано, в конечном итоге, на видах. Вводим возможность при декларировании вида указать интерфейс запроса, для которого он декларируется, что достигается наличием специального параметра type во всех директивах, определяющие виды или что-либо подобное (пространство zcml browser); page -- определяет страницу; Zope устроен так, что позволяет создавать "вид" - некий инструмент для представления и работы с объектом. Стандартные виды zope ус ...

Скины :

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

  1. Все виды, определенные для разных интерфейсов (объектов);
  2. Страницу шаблона основной страницы;
  3. Различные ресурсы;
  4. Множество самых разных мелких деталей, о существовании которых даже и не подозреваешь, пока не дойдешь до них;

К счастью, есть два обстоятельства, делающие жизнь проще:

  1. Однажды написанный скин существует как отдельный продукт и может многократно повторно применяться. Кроме того, несколько скинов могут легко сосуществовать;
  2. Скин для конечного пользователя неизмеримо проще административного скина.

Кроме того, скины можно наследовать один от другого :), но реальность такова, что такое наследование - скорее декларация, чем реально полезный инструмент: в качественно разработанном скине перекрыть придется _всё_, а потому выигрыш от наследования - иллюзорен.

Немного теории :

Когда-то все было по-иному сложнее и менее следовало Zope3 style. К сожалению, это когда-то еще не ушло особенно далеко в прошлое, а поэтому везде всплывают его остатки. Ниже описано то, как это работает сейчас. Изучая исходники Zope и видя некоторые несоответствия написанному, обращайте внимание на слово "deprecated" и старайтесь понять, что было вчера, а что будет завтра.

Есть небольшая документация на эту тему. Рекомендуется с ней ознакомиться. Про взгляд на скин со стороны пользователя можно прочитать тут: TypicalPlacesInZope3Skins.txt.

Отображение пользовательских интерфейсов в Zope3 основано, в конечном итоге, на видах.

Вид
это специальный мультиадаптер, адаптирующий контекстный объект и запрос к пустому интерфейсу, снабженный, кроме того, именем.

Как и любой мультиадаптер, вид уникален: для каждой тройки (интерфейс контекста, интерфейс запроса, имя) существует только один __точно__ соответствующий вид. Но в утверждении "точно соответствующий" есть некая тонкость, связанная с тем, что интерфейсы могут наследоваться: Адаптер к подинтерфейсу является также адаптером к супер-интерфейсу (подробнее см. Особенности работы реестров.txt, adapter.ru.txt), а значит, наше утверждение лучше записать так:

  • "для любого имени, существует группа мультиадаптеров, которые в различной степени соответствуют любой наперед заданной двойке (интерфейс контекста, интерфейс запроса)".

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

Теперь вернемся к понятию скина:

Скин
это совокупность видов для компонент сайта, разработанная для совместного применения.

Посмотрев на вышеприведенное определение, легко предложить такое решение:

Скин
это совокупность видов, соответствующих одному интерфейсу запроса.

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

Следовательно, практическое решение должно быть таким:

  1. Вводим возможность определить специальный интерфейс запроса, смысл которого - скин, что легко достигается наличием директивы interface и всего с ней связанного;
  2. Вводим возможность при декларировании вида указать интерфейс запроса, для которого он декларируется, что достигается наличием специального параметра type во всех директивах, определяющие виды или что-либо подобное (пространство zcml browser);
  3. Вводим возможность переключить интерфейс запроса в процессе траверса, что достигается введением специального пространства имен skin ("++skin++" <имя скина>).

Это решение создано и работает. Отметим только, что параметр, аналогичный упомянутому выше параметру type, ранее назывался layer. В настоящий момент один параметр переименовывается в другой (не везде переименовался еще), но в любом случае, смысл его один и тот же: указать интерфейс запроса при регистрации адаптера.

Необходимые директивы:

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

resource
определяет ресурсы (файлы, лежащие на диске, и доступные явным указанием URL);
page
определяет страницу;
menu
создает меню;
menuItem
добавляет страницу в меню;
viewletManager
создает менеджер для viewlet'ов;
viewlet
создает и регистрирует viewlet;

Очевидные шаги при разработке скина:

  1. Определить планировку страницы;
  2. Выделить в ней слоты меню, viewlet'ов и контента;
  3. Создать страницу с темплейтом основного вида, отведя в ней слот content;
  4. Создать различные дополнительные виды (если они нужны) директивами page.
  5. Объявить меню директивой menu;
  6. Наполнить меню элементами директивой menuItem;
  7. Объявить viewletManagers;
  8. Набить viewletManagers элементами директивой viewlet;

Пример :

Разработаем скин под названием note (продукт noteskin). Для иллюстративности унаследуем его от rotterdam :

            from zope.app.rotterdam import Rotterdam

            class NoteSkin(Rotterdam):
                """Скин для Note"""

И зарегистрируем в Zope:

            <interface
                interface=".interfaces.NoteSkin"
                type="zope.publisher.interfaces.browser.IBrowserSkinType"
                name="NoteSkin"
                />

Поясним немного задачу разработки скина. Предположим, что у нас есть некая иерархия объектов. Как известно, каждый объект предоставляет несколько интерфейсов. Zope устроен так, что позволяет создавать "вид" - некий инструмент для представления и работы с объектом. Доступность вида для каждого объекта определяется двумя свойствами:

  1. Объект предоставляет интерфейсы, требуемые видом;
  2. Данный пользователь обладает правами, достаточными для того, чтобы воспользоваться этим видом.

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

Стандартные виды zope устроены так, что для своего отображения вызывают макрос page и заполняют в нем слот body. Разумеется, можно не использовать стандартные виды вообще и написать все свое :), но в нашем примере мы это делать не будем.

Далее пойдем по пунктам с небольшими пояснениями.

  1. Планировка страницы

    Не будем изобретать что-то из ряда вон выходящее, просто создадим страницу, у которой:

    В левом верхнем углу
    логотип;
    Сверху в середине
    две строчки: название текущего объекта и путь к нему от корня (т.н. breadcrumbs);
    В левой колонке
    меню видов (вариантов просмотра) для текущего объекта;
    В середине
    отображается вид объекта;
    В правой колонке
    отображается вспомогательная информация;
    Внизу
    меню "действий";

  2. Слоты

    Слот содержимого самый простой - как уже говорилось, общий шаблон страницы это макрос page, в котором определен слот body. В него и будет вставлено содержимое. Что до остальных слотов - они вызываются непосредственно из шаблона При этом:

    • Меню действий берем готовое (zmi_actions);
    • Меню видов создаем свое (note_views);
    • Вспомогательная информация отображается при помощи viewletManager rightcolumn;
    • Логотип вписываем явно;
    • Путь к объекту получаем из специально зарегистрированного вида @@absolute_url;

  3. Темплейт

    Пишем темплейт страницы и размещаем его в template_table.pt. Регистрируем шаблон:

                      <page
                          for="*"
                          name="skin_macros"
                          permission="zope.View"
                          layer="..interfaces.NoteSkin"
                          template="template_table.pt"
                          />
    

    Там же регистрируем два ресурса: логотип и css:

                      <resource
                          name="note.css" 
                          file="note.css" 
                          layer="..interfaces.NoteSkin"/>
    
                      <resource
                          name="notelogo.gif" 
                          file="notelogo.gif" 
                          layer="..interfaces.NoteSkin"/>
    

  4. Мы хотим использовать собственное меню видов, в котором разместим только специально созданные нами виды. В нашем примере мы не разрабатываем ни одного вида, а просто создаем их заново в новом скине директивами page и им подобными (определения остальных видов можно посмотреть в исходниках продукта noteskin.):
                      <editform
                          schema="note.interfaces.INote"
                          for="note.interfaces.INote"
                          label="Edit"
                          name="edit.html"
                          permission="zope.ManageContent"
                          menu="note_views" title="Edit" 
                          layer="..interfaces.NoteSkin"
                          />
    
  5. Объявляем специальное меню:
                        <menu
                            id="note_views"
                            title="Views"
                            description="Menu for displaying alternate representations of an object"
                            />
    
  6. Это меню будет вызываться в основном шаблоне как цикл по пунктам меню. Реальный код сложнее, его можно посмотреть в исходниках, но примерно это выглядит так:
                        <a href=""
                            tal:repeat="view context/@@view_get_menu/note_views
                            tal:attributes="href view/action;
                              class view/selected;"
                            tal:content="view/title" i18n:translate="">
                            label
                        </a>
    
  7. Вносим в меню все декларированные виды (впрочем, часть видов добавлено в меню уже директивами page.) :
                      <menuItem
                        for="zope.app.folder.interfaces.IFolder"
                        menu="note_views" title="Content"
                        action="contents.html"
                        layer="..interfaces.NoteSkin"
                        />
    
                      <menuItem
                        for="zope.app.component.interfaces.ISite"
                        menu="note_views" title="Quota"
                        action="++etc++site/quota/@@histogram.html"
                        layer="..interfaces.NoteSkin"
                        />
    
  8. Объявляем viewletManager. Менеджер viewlet'ов - это специальный инструмент, действующий наподобие меню: в нем регистрируются специальные объекты, для которых указывается в контексте какого объекта они могут отображаться и какие пользователи должны иметь права, чтобы их увидеть.

    В шаблон основной страницы вписывается вызов менеджера и для каждого конкретного объекта менеджер подбирает свое содержимое указанным выше способом. Регистрация менеджера выглядит вот так:

                      <viewletManager
                        name="rightcolumn"
                        permission="zope.ManageContent"
                        for="*"
                        provides=".interfaces.IRightColumn"
                        template="rightcolumn.pt"
                        layer="..interfaces.NoteSkin"
                        />
    

    Обратите внимание на параметр provides - именно по этому интерфейсу из общей массы viewlet'ов будут отбираться именно те, которые должны отображаться этом менеджере (как вы, наверно, догадываетесь - viewlet это всего-лишь адаптер).

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

                        <table border="1" width="100%">
                          <tr tal:repeat="viewlet options/viewlets">
                            <td tal:content="structure viewlet">&nobr;</td>
                          </tr>
                        <table>
    

    Как нетрудно видеть, это явный цикл по списку viewlets передаваемых в темплейт в виде опции.

  9. Чтобы поупражняться в написании viewlet'ов, запрограммируем какие-то информационные области, например такие:
    notebox
    отображает название директории заметок, выводится только в заметке;
    lastbox
    отображает название и содержимое последней измененной заметки, выводится только в директории;
    quotabox
    отображает текущие значения квот.

    Эти информационые области появляются в зависимости от интерфейсов, предоставляемых текущим объектом и сообщают дополнительную информацию о них.

    Для одной из них (quotabox) приведем исходный текст:

                        import zope.component
                        from zope.app import zapi
    
                        from zope.interface import implements
                        from zope.viewlet import interfaces
                        from quota.interfaces import IQuota
    
                        class QuotaBox(object) :
                            implements(interfaces.IViewlet)
    
                            def update(self) :
                                quota = zapi.getUtility(IQuota, context=self.context)
                                self.quota = quota.quota
                                self.size = quota.size
    

    Этот класс находит утилиту IQuota и извлекает из нее параметры статистики.

    Приведем текст шаблона, который показывает их:

                        <h1>Статистика квот</h1>
                        <dl>
                          <dt>Size:</dt> <dd tal:content="view/size">1</dd>
                          <dt>Quota:</dt> <dd tal:content="view/quota">1</dd> 
                          </dl>
    

    А теперь зарегистрируем их:

                      <viewlet
                        name="quotabox"
                        permission="zope.ManageContent"
                        manager=".interfaces.IRightColumn"
                        class=".quotabox.QuotaBox"
                        template="quotabox.pt"
                        for="note.interfaces.INote "
                        layer="..interfaces.NoteSkin"
                        />
    
                      <viewlet
                        name="notebox"
                        permission="zope.ManageContent"
                        manager=".interfaces.IRightColumn"
                        template="notebox.pt"
                        for="note.interfaces.INote "
                        layer="..interfaces.NoteSkin"
                        />
    
                      <viewlet
                        name="quotabox"
                        permission="zope.ManageContent"
                        manager=".interfaces.IRightColumn"
                        class=".quotabox.QuotaBox"
                        template="quotabox.pt"
                        for="notebook.interfaces.INotebook"
                        layer="..interfaces.NoteSkin"
                        />
    
                      <viewlet
                        name="lastbox"
                        permission="zope.ManageContent"
                        manager=".interfaces.IRightColumn"
                        class=".lastbox.LastBox"
                        template="lastbox.pt"
                        for="notebook.interfaces.INotebook"
                        layer="..interfaces.NoteSkin"
                        />
    

    Обратите внимание на аргумент for, определяющий для каких объектов будет отображаться этот viewlet.

    И наконец приведем вызов viewletManager в основном темплейте:

                        <td valign="top" width="20%" tal:content="structure provider:rightcolumn">
                            ViewLets
                        </td>        
    

    Существует описание на viewlet: /++apidoc++/Book/viewlet/show.html, оно дано с точки зрения использования внутри питоновского кода, и может показаться менее полезным.

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

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