Zope - The Object Publishing Environment

Авторы: Олег Бройтман и Русскоязычная Группа Пользователей Python и Zope.
При использовании материалов ссылка на источник обязательна.

ПРЕАМБУЛА - трудности перевода в Zope

Во-первых, само название. Формально оно звучит как Z Object и так далее. Если написать The Object - оно должно быть Tope, но это означает "пьянствовать". Так что не у одних русских с этим словом проблемы.

Во-вторых, транскрипция или транслитерация - Зоп? Зопе? в Зопе? Последний вариант у некоторых рождает неподобающие мысли и ассоциации :) Я предпочитаю писать Zope и произносить Зоп. Он, в конце концов, сервер!

ВВЕДЕНИЕ в Zope

Zope - это объектно-ориентированная платформа, сервер приложений, предназначенный для создания динамических web-приложений и интерактивных сайтов.

У выражения "объектно-ориентированный" здесь несколько сторон. Во-первых, Zope написан на языке Python, объектно-ориентированном языке со множественным наследованием.

Во-вторых, Zope построен вокруг идеи "публикации объектов" - URL, к которому обращается браузер, является ссылкой на объект (экземпляр класса), вызываемый на выполнение.

В-третьих, сами объекты (сериализованные экземпляры классов) хранятся в объектно-ориентированной базе данных ZODB.

В дальнейшем я буду продолжать употреблять выражение "объектно-ориентированный" достаточно часто, не потому что это модное слово, а потому что неотъемлемое свойство Zope.

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

Другие модные слова, типа XML, я буду употреблять реже. Это не значит, что Zope не работает с XML - работает еще как, - просто к моему введению это не имеет отношения, а я стараюсь "не употреблять слова только за то, что они красивые и длинные" (C) Кэрролл, перевод Демуровой).

Еще несколько модных слов, имеющих отношение к делу: free software, open source, 64-бит (на соответствующих ОС), многоплатформенность и переносимость (Zope написан на портабельном языке Питон и работает во всех юниксах и в Windows; основной формат базы данных ZODB - файл Data.fs - полностью независим от платформы и ОС), масштабируемость и распределенность (с помощью компонента ZEO, о чем позже).

ПОЧЕМУ Zope

Протоколы WWW (HTTP, CGI и т.д.) часто неадекватны задачам и могут делать публикацию динамических данных неоправданно сложной. Их низкий уровень недостаточен для непосредственного создания многих классов web-приложений на их основе.

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

ПОЛЬЗОВАТЕЛИ Zope

C Zope работают следующие категории пользователей:

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

КОМПОНЕНТЫ Zope

Zope Core

В "сердце" Zope находится ORB (object request broker), а также механизмы, обеспечивающие поиск (ZCatalog), безопасность, коллективную работу и разделение информации. Zope имеет web-интерфейс для программирования и администрирования.

ZServer

Многопоточный ZServer предоставляет гибкий механизм связи, поддерживая протоколы HTTP, FTP, XML-RPC, FastCGI и PersistentCGI. Zope может быть запущен с ZServer, причем можно использовать ZServer совместно с уже существующим WWW сервером; или же Zope можно запустить из-под существующего WWW сервера в режиме PCGI (однопоточный сервер PersistentCGI).

Object Database (ZODB)

Объектно-ориентированная база Zope хранит объекты (именно объекты в смысле Python, то есть сериализованные экземпляры классов); сама ZODB написана объектно-ориентированно, то есть как набор деревьев классов. В ZODB можно произвольно менять класс StorageManager - хранилище. Стандартное хранилище FileStorage хранит данные в файле Data.fs, но можно использовать альтернативные классы - SQLStorage или BerkeleyStorage. ZODB поддерживает атомарные операции (транзакции), неограниченный undo (только с соответствующим хранилищем, например, FileStorage или InterbaseStorage поддерживают Версии и откат, а остальные хранилища - нет), приватные Версии, и масштабируется до гигабайтов хранимых данных. Отдельный механизм ZEO (Zope Enterprise Option) позволяет повысить надежность и масштабируемость путем кластеризации. Собственно, ядром ZEO является еще одно хранилище ServerStorage, которое обращается не к локальному Data.fs, а к удаленному серверу; вторым компонентом ZEO является как раз сервер.

Document Template Markup Language (DTML)

За этим названием скрывается богатый механизм интерпретации (рендеринга) шаблонов. Простые сайты можно создавать, вообще не обращаясь к Питону - на одном DTML (естественно, пользуясь, уже готовыми компонентами Zope).

Интеграция с реляционными СУБД

Zope имеет уровень абстракции ZSQL, позволяющий легко интегрировать систему с SQL, будь то PostgreSQL, Oracle, MySQL или ODBC.

Продукты Zope

Продукты - компоненты, написанные программистом на Питоне - позволяют дополнять Zope новыми типами объектов. Например, компонент (назовем его условно Poll) для создании на сайте голосований. После того, как программист напишет соответствующие классы, webмастер расставит экземпляры этих классов на сайте и создаст каждому из экземпляру дизайн; редактор сайта наполнит их содержимым (вопрос и список ответов для каждого экземпляра); и посетители сайта могут начинать голосовать!

ZClasses

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

ЧТО ДАЕТ Zope

Программисту:

web-мастеру:

администратору:

ПРОГРАММИРОВАНИЕ для Zope

Программирование для этой сложной и гибкой платформы осуществляется разными механизмами и на разных языках.

1. Программирование на DTML. Это не столько программирование, сколько верстка, работа webмастера. Из DTML доступно большое число функций и объектов Питона и Зоп, за исключением тех, которые скрыты по соображениям безопасности. DTML предназначен преимущественно для презентации, а не для манипуляции данными.

2. PythonMethods. Код пишется на Питоне и вводится через web-интерфейс Zope. На этот код распространяются те же ограничения безопасности,что и на DTML. Обычно PythonMethod - одна или несколько простых функций.

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

4. Компоненты. Они пишутся на Питоне с помощью Product API. Компонент - это класс или набор деревьев классов. Никаких ограничений по безопасности (в уже указанном для ExternalMethods смысле; использование же методов компонента может быть защищено совместными усилиями программиста и администратора сайта). Код этот ставится в файловую систему администратором хоста, и Zope приходится перезапускать. После этого в списке Продуктов появляется новый Продукт (а то и не один, если программист или администратор хоста разом инициализирует несколько Продуктов или в одном Продукте регистрирует несколько Производителей (конструкторов)), экземпляры которого можно создавать в любом месте иерархии объектов.

5. ZClass. "Программирование мышкой". Создатель Z-класса расписывает, какие у объекта есть атрибуты, и создает на DTML способы редактирования и показа экземпляров класса. Все "программирование" идет через web-интерфейс Zope. Z-Класс добавляется в список Продуктов, и можно создавать его экземпляры. При изменении программистом Z-класса все экземпляры меняются автоматически (то есть экземпляры содержат не копию кода, а ссылку на класс). Z-Классы можно наследовать от богатого базового набора классов Zope, можно от других Z-классов, и программист может создать Компонент, включающий классы, от которых можно наследовать Z-классы.

ПУБЛИКАЦИЯ ОБЪЕКТОВ

Zope публикует Питоновские объекты (экземпляры классов). Для этого в Zope есть компонент ZPublisher - брокер объектных запросов. Получив запрос (от ZServer'а, который в свою очередь получает запрос из внешнего мира по одному из поддерживаемых протоколов), он:

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

<FORM name="formA" action="myObject" method="POST">
   <input type="text" name="age:int" size="2">
   <input type="checkbox" name="category:int:list">K1
   <input type="checkbox" name="category:int:list">K2
   <input type="submit" name="manage_setAge:method" value="Установить">
   <input type="submit" name="manage_delete:method" value="Удалить">
</FORM>

После заполнения формы в браузере и нажатия одной из кнопок ZPublisher преобразует введенные данные. Переменная age преобразуется в целое, checkbox'ы - в список целых, и вызовется один из методов объекта myObject в зависимости от нажатой кнопки.

Проверка, естественно, осуществляется на стороне сервера, в Zope. Если переменная age не преобразовывается в целое, возникнет ошибка. Ее может обработать публикуемый объект, а нет - Zope выдаст пользователю HTML с текстом об ошибке. Для проверки на стороне клиента можно использовать JavaScript. Искривленные имена слегка мешают доступу из JS, но это не смертельно: element = document.forms["formA"].elements["age:int"]

Как именно вызовется метод, зависит от его (метода) сигнатуры (в Питоне вся информация о коде доступна во время выполнения). Например, если myObject - экземпляр вот такого класса:

   class AgeManager:
      def view(self, age=None):
         if age is None:
            age = self.age
         return "Возраст: <b>%d</b>" % age
      def manage_setAge(self, age):
         self.age = age
      def manage_delete(self, category):
         for c in category: self.delete(c) # self.delete не показан

то метод manage_setAge вызовется с целым age, или manage_delete - со списком нажатых checkbox'ов. Остальные переменные формы можно извлечь из общего пространства имен, доступного через self.

Публикация через метод GET и того проще: на запрос http://www.my.server/root/subobject/sub2/myObject/view?age:int=12

ZPublisher обходит иерархию объектов (траверс с учетом механизма acquisition, о чем позже) и публикует myObject - у объекта вызывается метод view с целочисленным параметром.

ACQUISITION - заимствование вместо наследования

Acquisition - это механизм запроса значений переменных из текущего контекста. Переведем это слово как "заимствование" атрибутов; относительно адекватный перевод.

Заимствование значения атрибута происходит из контекста объекта. Контекстов бывает два - статический, возникающий в момент создания объекта, и динамический, возникающий во время вызова метода объекта на выполнение. Откуда именно происходит заимствование - этим управляет программист при создании или использовании компонента.

Контекст - это стек, в котором происходит поиск атрибута. Например, если есть контекст [object, sub2, myObject] (на вершине находится myObject), и myObject запросил значение атрибута color, то поиск будет происходить в глубину стека. Сначала атрибут с таким именем будет искаться в myObject, если его там нет - поиск перейдет к sub2, потом к object.

Статический контекст - это путь от корня ZODB (ZODB, не сайта!) к объекту в иерархии объектов. Динамический контекст - это путь (стек), возникающий во время обхода иерархии объектов компонентом ZPublisher при обращении к объекту через URL.

Например, если есть путь /root/object/subobject/myObject, то это и есть статический контекст (точнее, контекстом является стек объектов [root, object, subobject, myObject]).

Динамический контекст зависит от URL. Если произошло обращение к адресу http://www.server/root/object/subobject/myObject, то в этом случае динамический контекст совпадает со статическим. Но при обращении к http://www.server/root/english/object/subobject/myObject (где english - папка в объекте root) контекст будет другой - в стек добавится объект english. Чтобы понять, на какое именно место englsih добавится, надо подробно рассмотреть процесс траверса. ZPublisher сам тоже использует механизм acquisition, так что в целом разбор адреса http://www.server/root/english/object/subobject/myObject происходит следующим образом.

Получив (от ZServer'а) путь /root/english/object/subobject/myObject, ZPublisher начинает обходить отдельные части пути, строя по ходу стек. Сначала стек пуст, затем к нему добавляется root (поиск начинается от корня ZODB, и проверяется, что объект с таким именем есть в корне), затем ZPublisher обнаруживает english и запрашивает его (с учетом заимствования); объект обнаруживается в /root и попадает в стек, затем идет object, который заимствуется не из english, а из /root, затем нормальным путем идут subobject и myObject. В данном случае стек просто совпал с URL. Но если бы в english был свой object, он бы заимствовался бы оттуда, а не из /root. И если бы в этом object не было subobject, то subobject опять заимствовался бы из /root (ели он там есть). В результате мы имели бы контекст (стек) [/root, /root/english, /root/english/object, /root/subobject, myObject]. И если бы myObject запросил атрибут language, отсутствующий в /root/subobject, он получил бы его из /root/english/object, а не из /root/object!

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

Рассмотрим подробный пример. Два основных объекта Zope - это классы DTML Document и DTML Method, включенные в дистрибутив Zope. Они предназначены для разного типа использования. DTML Document хранит содержание, текст; его путь - заимствование из статического контекста. DTML Method предназначен для активных действий, он заимствует значения из динамического контекста. Еще есть класс Folder - папка. В ней хранятся другие объекты.

Пусть, скажем, Документ my.html начинается со стандартного заголовка, и заканчивается стандартным подвалом. На языке DTML это выражается как <dtml-var standard_html_header> и <dtml-var standard_html_footer>. Разместим эти объекты на небольшом абстрактном (то есть существующим только в наших головах) сайте. Пусть есть корень (корень в ZODB есть всегда), в нем несколько папок, скажем, Razdel1 и Razdel2, 2 Метода - header и footer, и в Razdel2 - наш my.html.

/
   standard_html_header
   standard_html_footer
   Razdel1
   Razdel2
      my.html

Итак, браузер обращается к http://www.server/Razdel2/my.html. ZPublsiher строит контекст [/, /Razdel2, /Razdel2/my.html] и вызывает рендеринг my.html. Тот начинает рендерится, и в самом начале встречает <dtml-var standard_html_header>. Запрашивается значение заголовка. В my.html такого объекта нет, в Razdel2 нет, поиск переходит в корень - там такой есть. Он выполняется (рендерится), потом выполнение возвращается в my.html, потом footer - все.

Возьмем и добавим в Razdel2 другой header:

/
   standard_html_header
   standard_html_footer
   Razdel1
   Razdel2
      standard_html_header
      my.html

И опять обратимся к http://www.server/Razdel2/my.html. Теперь my.html позаимствует другой header, и выглядеть будет по-другому!

Добавим в корень новый раздел, с другими header/footer:

/
   standard_html_header
   standard_html_footer
   Razdel1
   Razdel2
      standard_html_header
      my.html
   NewLook
      standard_html_header
      standard_html_header

И обратимся к http://www.server/Razdel2/NewLook/my.html. Будет ли my.html использовать header из NewLook? Нет! my.html - DTML Document, и всегда использует статический контекст. Его acquisition path всегда [/, /Razdel2, /Razdel2/my.html].

Добавим в Razdel1 объект DTML Method index.html

/
   standard_html_header
   standard_html_footer
   Razdel1
      index.html
   Razdel2
      standard_html_header
      my.html
   NewLook
      standard_html_header
      standard_html_header

И обратимся к http://www.server/Razdel1/index.html. Поскольку это Метод, то будет использован динамический контекст, но в данном случае он совпадает со статическим. А вот при обращении к http://www.server/Razdel1/NewLook/index.html динамический контекст будет другой, и index.html позаимствует атрибуты из NewLook - и будет выглядеть по другому!

Изменим сайт последний раз. Все удалим,

/
   standard_html_header
   standard_html_footer
   Razdel1
      index.html
   Razdel2
      my.html

и отредактируем header/footer, так чтобы они включали на сайте, скажем, левую колонку. Назовем ее left-column, и создадим ее в корне и в разделах:

/
   standard_html_header
   standard_html_footer
   left-column
   Razdel1
      index.html
      left-column
   Razdel2
      my.html
      left-column

Теперь при вызове http://www.server/Razdel1/index.html будет показываться одна колонка, http://www.server/Razdel2/my.html - другая. А header при этом один на всех! Как header знает, какую колонку использовать? Очень просто - он участвует в поиске по acquisition path, по контексту (статическому или динамическому в зависимости от того, откуда его вызвали), не более того.

Эти разные left-column даже не обязаны даже быть экземплярами одного класса. В корне это может быть DTML Method, а в Razdel2 - ZNavigator. Header'у все равно, кого рендерить, он вызывает left-column, ничего не зная об его типе и устройстве (опять объектно-ориентированное программирование).

Еще один пример, ближе к реальной жизни с Zope, но менее подробный. Предварительное замечание: когда URL ссылается не на метод объекта, а на сам объект, у него вызывается метод index_html.

Создадим маленький сайт. В корень поместим DTML Method index_html простого содержания:

   <dtml-var standard_html_header>
   <dtml-var content>
   <dtml-var standard_html_footer>

и DTML Document content, хранящий собственно содержание раздела, вообще без заголовка/подвала.

/
   standard_html_header
   standard_html_footer
   index_html
   content
   Razdel1
      content
   Razdel2
      content

Обратимся к корню сайта: http://www.server/. Корневая папка вызовет свой index_html, который интерпретируется, подгрузит соответствующие заголовок и подвал. Ничего особенного.

Теперь обратимся к одному из разделов: http://www.server/Razdel1/ Этот папка, поэтому она вызовет свой index_html... Но в Razdel1 нет своего index_html. Он заимствуется из корня! Ну, и поскольку он DTML Method, то он сам заимствует атрибуты из динамического контекста. Header/footer возьмутся из корня, а content из Razdel1.

Третий, и последний пример совсем кратко. В папке db лежат dtml-методы (пусть dtml-методы будут называться db/view, db/insert, db/update), и sql-методы, которые параметризованы (имя таблицы, имена столбцов).

Далее, внутри этой папки делается папка, например users. В ее атрибуты прописываются конкретные параметры для методов (имя таблицы, имена столбцов).

По обращению db/users/view - получаем готовую страничку с содержимым таблицы. Метод view (равно как и insert и update) унаследован из db, но заимствует атрибуты из users.

Метод db/users/insert (унаследованный из db) прочтет из свойств папки db/users название таблицы, названия полей, и сконструирует форму, для добавления записи. То же будет происходить с другими папками, и их свойствами. В ходе развития проекта, точно так-же как и для случая ОО программирования, добавление новых методов в "базовый объект" db (например нужно будет сделать поиск - db/search) автоматически расширит функциональность "потомков" db/users, db/something...

SECURITY - механизмы безопасности в Zope

Zope предоставляет программистам и администраторам простые, и в то же время мощные и гибкие механизмы управления безопасностью. Безопасность в Zope стоит на трех китах, трех базовых понятиях - пользователь, роль, и вид доступа.

Вид, или тип доступа определяет программист при создании компонента. Каждому классу в компоненте определяется полномочие "Add" ("Добавить экземпляр класса в дерево объектов"), каждому методу класса можно определить свои собственные полномочия, которые определят, кому и какой вид доступа предоставлен к этому методу класса. Например, методу index_html (который вызывается при обращении к объекту, а не к конкретному методу) обычно дается вид доступа View. Но это дело программиста, как назвать свои полномочия, и какие методы какими полномочиями защитить. Обычно методы объекта объединяются в группы, предоставляющие один сервис. Например, класс НовостеваяЛента может иметь сервисы (группы методов) "показ новостей", "добавление новостей", "редактирование новостей", "удаление новостей", "добавление/редактирование/удаление рубрик". И каждый из сервисов можно защитить (дав ему отдельный вид доступа) - с точностью до одного метода. Для более тонкого управления, уже внутри метода, программист может запросить SecurityManager - "имеет ли текущий пользователь права на создание DTML Методов в Папке Razdel?"

Роли создает администратор сайта через менеджерский web-интерфейс Zope. Понятие роли распространяется не на весь сайт, не на ZODB, а на часть дерева. Администратор создает роль в какой-то папке, и дальше благодаря механизму acquisition эта роль распространяется вниз по поддереву.

Zope, поставленная из дистрибутива, имеет 3 роли, определенные в корне ZODB - Anonymous, Owner и Manager. Manager - это такой всесильный администратор, аналог рута. Owner - владелец тех ресурсов, которые он создал. Анонимный пользователь - просто посетитель сайта; ему изначально доступны типы доступа: Access content, View, Use SQL Methods (это для того, чтобы позволить вызывать SQL Методы из DTML Методов) и Search ZCatalog.

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

Эти 2 механизма - категории доступа и роли - совершенно ортогональны, и в web-интерфейсе образуют табличку из сотен checkbox'ов - какой роли какие категории доступа.

Администратор сайта может, например, завести роли Editor и ChiefEditor, и дать роли Editor права на редактирование DTML Document'ов, а роли ChiefEditor - права на редактирование DTML Method'ов, картинок, и на создание папок. Дав роли SubAdmin права на администрирование безопасности в поддереве сайта, администратор эффективно делегирует полномочия.

Пользователи (то есть записи о пользователях) - это объекты (как и все остальное), экземпляры класса User. Изначально Zope ставится с компонентом UserFolder, который хранит эти объекты в ZODB, и может получать и проверять username/пароль только по схеме Basic Authentication. Но компонентная технология и здесь дает свои преимущества. Уже доступны компоненты:

Все перечисленные компоненты умеют как Basic Auth, так и куки. На сайте можно расставить сколько угодно экземпляров разных компонент, и таким образом авторизовывать одну часть сайта из домена NT, а другую - из SQL. К сожалению, поставить в одну папку 2 разных UserFolder нельзя.

Тип доступа для пользователя проверяется не непосредственно, а через роли. Запись о пользователе (объект класса User), помимо логина и пароля, хранит список его ролей, и список доменов и/или IP, с которых пользователю разрешено работать.

Опять-таки, благодаря механизму заимствования, запись пользователя доступна и проверяется везде в поддереве, на вершине которого эта запись внесена.

Еще один механизм нужен, если какое-то привилегированное действие надо позволить совершить пользователю, не обладающему нужными привилегиями. Скажем, дать на просмотр (но только на просмотр) Документ, доступный только Менеджеру. Тогда это конкретное действие описывается (скажем, на DTML пишется Метод для просмотра), и этому Методу дается Proxy Role под роль Manager. Полный аналог юниксового setuid, и как со всяким setuid, надо быть очень внимательным, чтобы не насоздавать дыр в защите.

Наконец, последний механизм, Local Role, позволяет дать определенному пользователю дополнительные права (роли) на конкретный объект. Так-то роли даются пользователю там, где определена запись пользователя; Local Role позволяет определить дополнительные роли в контексте объекта, а не пользовательской записи. Локальные роли не заимствуются механизмом acquisition.

НЕДОСТАТКИ Zope

Недостатки Zope в основном являются продолжением достоинств этой платформы.

Некоторые особенности имеют отдельные компоненты Zope.

РУССКОЯЗЫЧНАЯ ГРУППА ПОЛЬЗОВАТЕЛЕЙ Python и Zope

У нас еще нет своего сайта, он в процессе создания, но группа отличается активностью и интересом к продвижению технологии Zope. Вы можете подписаться на наш список рассылки, послав письмо с телом (с телом, не с темой) subscribe python по адресу majordomo@list.glasnet.ru. Администратор списка - Евгений Двуреченский; хостинг списка - Россия-Он-Лайн.