Скелет контент класса в Zope.txt
2008-01-11 19:02Скелет контент класса в Zope: Для того, чтобы класс мог использоваться в Zope, должно быть сделано множество различных деклараций. Эти декларации настолько гибкие, что даже класс, который никогда не предполагалось размещать в Zope, можно легко затолкнуть туда, создав необходимые декларации. Все классы можно создать в рамках одного продукта, но чтобы лучше проявить сущность Zope3, создадим отдельный продукт для каждого класса, и посмотрим, как можно использовать их вместе и по-отдельности. В cамом деле, Zope3 - это среда, позволяющая писать множество маленьких, независимых продуктов-кубиков, а затем легко объединять их принеобходимости в одно целое. Скелет класса (теория) : Описание класса для Zope включает: 1. не был нулевой длины, например:: ## -*- coding: utf-8 -*- ####### # Make it a Python package Иначе некоторые утилиты по управлению исходным кодом могут быть сбиты с толку; Символическую ссылку на этот файл размещаем в катало ...
Скелет контент класса в Zope:
Для того, чтобы класс мог использоваться в Zope, должно быть сделано множество различных деклараций. Эти декларации настолько гибкие, что даже класс, который никогда не предполагалось размещать в Zope, можно легко затолкнуть туда, создав необходимые декларации.
Чтобы наиболее полно покрыть известные проблемы, мы будем рассматривать три независимых класса - класс-контейнер, вложенный в него терминальный класс и интерфейс-ограничение на вхождение терминального класса. Пусть это будут классы Notebook (блокнот), Note (заметка), а также Notestrict. Все классы можно создать в рамках одного продукта, но чтобы лучше проявить сущность Zope3, создадим отдельный продукт для каждого класса, и посмотрим, как можно использовать их вместе и по-отдельности. В cамом деле, Zope3 - это среда, позволяющая писать множество маленьких, независимых продуктов-кубиков, а затем легко объединять их принеобходимости в одно целое.
Каждый продукт будем создавать в отдельной директории.
Скелет класса (теория) :
Описание класса для Zope включает:
- Описание строки регистрации, обычно хранится в директории с
исходным кодом класса, в поддиректории etc. Для класса
notebook называется notebook-configure.zcml и содержит
строчку :
<include package="notebook" /> - Служебный файл __init__.py - без него директория не будет
воспринята как содержащая продукт. Вообще говоря, его нужно
класть везде, где лежат какие-либо коды.
Желательно, чтобы файл __init__.py не был нулевой длины, например:
## -*- coding: utf-8 -*- ####### # Make it a Python packageИначе некоторые утилиты по управлению исходным кодом могут быть сбиты с толку;
- Директория с продуктом должна быть расположена в PYTHONPATH;
- Должен быть файл interfaces.py, содержащий декларации интерфейсов (если продукт вводит какие-либо новые интерфейсы);
- Должен быть питоновский файл с декларациями классов продукта (напомним, что вообще говоря он генерируется автоматически по интерфейсу). Рекомендуется придерживаться правила: один файл на класс, хотя это и не всегда удается;
- Должна быть директория browser, содержащая темплейты и конфигурацию специальных видов, если таковые имеют место;
- В директории с продуктом должен лежать конфигурационный файл
configure.zcml, регистрирующий определяемые продуктом классы
и интерфейсы. Не забудьте указать в нем включение директори
browser, а также других директорий продукта, кроме etc, если они есть:
<include package=".browser" /> - В директории browser должен лежать конфигурационный файл configure.zcml, содержащий декларацию видов (или, в более общем случае, административного веб-интерфейса класса);
Создание класса Note :
Класс Note - это класс, содержащий атрибуты заметки:
- Создаем директорию класса, в которой создаем директории browser, etc и файл __init__.py.
- В директории etc создаем файл note-configure.zcml со
следующим содержимым :
<include package="note" /> - Символическую ссылку на этот файл размещаем в каталоге etc/package-includes сервера приложений Zope;
- Пишем файл interfaces.py.
Подробно об интерфейсах можно прочитать в статье Использование схем интерфейсов.txt:
from zope.interface import Interface from zope.schema import Text, TextLine, Datetime from datetime import datetime class INote(Interface): """A note object.""" title = TextLine( title=u"Title/Subject", description=u"Title and/or subject of the message.", default=u"", required=True) body = Text( title=u"Note Body", description=u"This is the actual note. Type whatever you wish.", default=u"", required=False) datetime = Datetime( title = u"Activation Time and Date", required=False, default=datetime.today())Чтобы узнать список возможных полей интерфейсов, можно воспользоваться поиском интерфейсов в экране помощи Zope (http://localhost:8080/++apidoc++/Interface/@@menu.html), введя поиск интерфейса IField и посмотреть список фабрик, создающих объекты реализующие данный интерфейс, или же можно заглянуть в исходные коды zope.schema;
- Корректируем PYTHONPATH так, чтобы он включал в себя
директорию, в которой расположена директория с продуктом
(например, /var/lib/zope/test/lib/python, если вы пользоветель Linux).
Для этого нужно издать такую команду:
export PYTHONPATH=$PYTHONPATH:/var/lib/zope/test/lib/pythonНа самом деле, если коды располагаются в специально отведенном месте (в папке lib/python инстанции zope), то в явном указании пути нет необходимости, поскольку Zope сам скорректирует путь при старте.
В настоящее время хорошей практикой считается установка продуктов Zope в то место, которое в данном дистрибутиве было предусмотрено при сборке и настройке python для установки доплонительных модулей.
- Отдаем команду, которая автоматически сгенерирует скелет
класса (пользователям Windows команду следует видоизменить):
/var/lib/zope/test/bin/pyskel note.interfaces.INote >note.py - Возможность генерировать скелеты классов замечательная, но
к сожалению, тот код, который генерируется, не совсем соответствует
современнности, поэтому на практике данная возможность используется редко.
Но уж раз воспользовались, то скорректируйте полученный результат следующим образом:
- заменим ссылку на интерфейс на относительную:
from interfaces import INote вместо from note.interfaces import INote - импортируем persistent.Persistent и zope.app.container.contained.Contained, после чего используем их как супер-класс.
Полученный код должен выглядеть примено так:
from zope.interface import implements from interfaces import INote from persistent import Persistent from zope.app.container.contained import Contained class Note(Contained,Persistent): __doc__ = INote.__doc__ implements(INote) # See note.interfaces.INote body = None # See note.interfaces.INote datetime = None # See note.interfaces.INote title = None - заменим ссылку на интерфейс на относительную:
- Создаем в директории продукта файл configure.zcml.
В нем указываем директивы interface (чтобы зарегистрировать интерфейс) и class (чтобы объявить существование класса):
<configure xmlns="http://namespaces.zope.org/zope"> <interface interface=".interfaces.INote" type="zope.app.content.interfaces.IContentType" /> <class class=".note.Note"> <require permission="zope.View" interface=".interfaces.INote" /> <require permission="zope.ManageContent" set_schema=".interfaces.INote" /> </class> <include package=".browser" /> </configure>Обратите внимание на нотацию записи путей в configure:
- если путь начинается не с точки, то это путь от директории, в которой лежит продукт (если быть точным - от одной из директорий в PYTHONPATH),
- если путь начинается с одной точки, то это путь от текущей директории,
- если путь начинается с двух точек, то это путь от директории уровнем выше,
- путь представляет собой элементы, разделенные точками, где элемент - имя директории, имя файла (без расширения) или имя переменной (класса) внутри файла.
В конце файла указана директива включения конфигурации из директории browser:
<include package=".browser" /> - Следующий шаг - создание административного веб-интерфейса.
Создаем в директории browser файл configure.zcml и размещаем в нем директивы:
- addform
- чтобы создать форму добавления;
- addmenuitem
- чтобы добавить пункт в меню add, соответствующей форме добавления;
- editform
- чтобы создать вид редактирования заметки.
Вот пример такого файла:
<configure xmlns="http://namespaces.zope.org/browser"> <addform label="Add Note" name="AddNote.html" schema="..interfaces.INote" content_factory="..note.Note" permission="zope.ManageContent" set_before_add="datetime" /> <addMenuItem class="..note.Note" title="Note" description="Note Text" permission="zope.ManageContent" view="AddNote.html" /> <editform schema="..interfaces.INote" for="..interfaces.INote" label="Edit" name="edit.html" permission="zope.ManageContent" menu="zmi_views" title="Edit" /> </configure>В соответствии со спецификацией, addform как и editform позволяют указать меню для включения пункта формы, но даже если вы найдете какое меню указать, чтобы включить в именно в меню "добавление" (хотя это всего лишь zope.app.publisher.interfaces.browser.AddMenu), элемент меню добавления, включенный таким образом не будет правильно работать (по крайней мере в zope 3.3.1), Это вызвано, предположительно, особой организацией меню добавления. Впрочем, желающием могут поэкспериментировать самостоятельно.
- Перезапускаем zope;
- Как и было обещано, появились все необходимые формы для создания и редактирования класса.
Создание класса Notebook :
Класс Notebook создается при помощи тех же самых шагов. Заметим лишь, что автоматически сгенерированный класс мы сделали подклассом BTreeContainer и выкинули из него все методы, каждый из которых есть в BTreeContainer'e. Фактически, это повторное использование хорошо известного системе класса - BTreeContainer - в некотором новом качестве.
Итак, по очереди, interfaces.py:
from zope.interface import Interface
from zope.schema import Text, TextLine, Datetime, Field
from datetime import datetime
class INotebook(Interface):
"""A note object."""
title = TextLine(
title=u"Title/Subject",
description=u"Title and/or subject of the message.",
default=u"",
required=True)
body = Text(
title=u"Note Body",
description=u"This is the actual note. Type whatever you wish.",
default=u"",
required=False)
Файл notebook.py:
from zope.interface import implements
from interfaces import INotebook
from zope.app.container.btree import BTreeContainer
from zope.app.container.interfaces import IContainer
class Notebook(BTreeContainer):
__doc__ = INotebook.__doc__
implements(INotebook,IContainer)
# See notebook.interfaces.INotebook
body = None
# See notebook.interfaces.INotebook
title = None
Файл configure.zcml:
<configure
xmlns="http://namespaces.zope.org/zope">
<interface
interface=".interfaces.INotebook"
type="zope.app.content.interfaces.IContentType"
/>
<class class=".notebook.Notebook">
<factory
id="notebook.Notebook"
description="Notebook"
/>
<require
permission="zope.ManageContent"
interface=".interfaces.INotebook"
/>
<require
permission="zope.ManageContent"
set_schema=".interfaces.INotebook"
/>
<require
permission="zope.View"
interface="zope.app.container.interfaces.IReadContainer"
/>
<require
permission="zope.ManageContent"
interface="zope.app.container.interfaces.IWriteContainer"
/>
</class>
<include package=".browser" />
</configure>
Обратите внимание на довольне нетривиальные декларации безопасности - IReadContainer, IWriteContainer - декларации на доступ к ним даются раздельно и с разными правами. Дело в том, что декларация "require ... interface" разрешает получать атрибуты интерфейса, декларация "require ... set_schema" разрешает их устанавливать. Но когда речь идет об интерфейсе, в схеме которого описан метод, то разрешение получения атрибута (метода в данном случае) автоматически означает разрешение вызвать этот метод. Поэтому методы позволяющие просматривать и редактировать контейнер пришлось разделить по разным интерефейсам и раздельно давать права на приведение к этим интерфейсам. Ислользование более простых решений (скажем "require ... set_schema ... IContainer") является распространенной ошибкой.
Файл browser/configure.zcml :
<configure
xmlns="http://namespaces.zope.org/browser">
<addform
label="Add Notebook"
name="AddNotebook.html"
schema="..interfaces.INotebook"
content_factory="..notebook.Notebook"
permission="zope.ManageContent"
set_before_add="title"
/>
<addMenuItem
class="..notebook.Notebook"
title="Notebook"
description="Notebook with notes"
permission="zope.ManageContent"
view="AddNotebook.html"
/>
<editform
schema="..interfaces.INotebook"
for="..interfaces.INotebook"
label="Edit"
name="edit.html"
permission="zope.ManageContent"
menu="zmi_views" title="Edit"
/>
<containerViews
for="..interfaces.INotebook"
index="zope.View"
contents="zope.View"
add="zope.ManageContent"
/>
</configure>
К случаю note/browser/configure добавилась директива containerViews, которая описывает вид и права на доступ к контейнеру. Помня про декларации доступа для интерфейсов класса, пожалуйста, постарайтесь понять: декларации доступа видов гарантируют только то, что злоумышленник не воспользуется этим видом для своих злых дел. Тем не менее, он может получить доступ к уязвимому интерфейсу класса каким-либо еще путем. Поэтому корректные декларации должны быть и там (что бы защитится от злоумылшенника) и здесь (что бы не пудрить мозг честным пользователям).
Строгое связывание Note и Notebook :
Классы Note и Notebook определены так, что класс Note может быть включен как в класс Notebook, так и в любой другой контейнер, да и в класс Notebook можно вставлять что угодно. Для борьбы с этим существуем специальная концепция "ограничений" в интерфейсах и можно было бы с легкостью модифицировать наши интерфейсы, чтобы достичь желанного эффекта. Но другой путь будет более общим и полезным.
Пусть Note & Notebook - это какие-то чужие классы, которые нет возможности исправлять, но хотелось бы использовать чуть иначе.
Создадим продукт notestrict, ограничившись в нем лишь файлами __init__.py, interfaces.py, configure.zcml и etc. В этом продукте определим интерфейсы:
- ICommonContainer
- некий общий контейнер, в который разрешается входить компонентам с интерфейсом INotebookContent;
- INotebookContent
- интерфейс, обозначающий содержимое ICommonContainer;
- INotebookContainer
- интерфейс, разрешающий вхождение только компонент с интерфейсом INotebookContent;
Если договорится, что для любого случая, когда компонент предоставляет интерфейс ICommonContainer, для него определен интерфейс с дополнительным ограничением, такой как INotebookContainer, то интерфейс INotebookContent обретает более новый смысл: это интерфейс, обладатель которого может быть содержимым INoteBook.
Это немного сложно понять и немного долго программировать. Зато потом появляется удивительная легкость настройки вхождения компонент.
Итак, начнем с определения всех этих волшебных интерфейсов:
from zope.interface import Interface
from zope.schema import Field
from zope.app.container.constraints import ContainerTypesConstraint
from zope.app.container.constraints import ItemTypePrecondition
from zope.app.container.interfaces import IContained, IContainer
class ICommonContainer(Interface) :
""" Interface that specify then it can be container of some
project component """
class INotebookContent(Interface) :
""" Interface that specify permission of object that can be content
of notebook """
__parent__ = Field(
constraint = ContainerTypesConstraint(ICommonContainer))
class INotebookContainer(IContainer,ICommonContainer) :
""" Article Container """
def __setitem__(name, object) :
""" Add IArticle Content """
__setitem__.precondition = ItemTypePrecondition(INotebookContent)
В configure.zcml указываем, что классы Note & Notebook предоставляют соответствующие интерфейсы:
<configure
xmlns:browser="http://namespaces.zope.org/browser"
xmlns="http://namespaces.zope.org/zope">
<interface interface=".interfaces.ICommonContainer" name="ICommonContainer"/>
<interface interface=".interfaces.INotebookContent" name="INotebookContent"/>
<interface interface=".interfaces.INotebookContainer" name="INotebookContainer"/>
<class class="note.note.Note">
<implements interface=".interfaces.INotebookContent"/>
</class>
<class class="notebook.notebook.Notebook">
<implements interface=".interfaces.INotebookContainer"/>
</class>
</configure>
Перезапустив Zope, убеждаемся, что это работает: можно легко добавлять объекты Note и Notebook, при этом объекты Note можно добавлять только внутри объектов Notebook.
Теперь, немного простоты. Если помните, в статье Использование сложных полей ввода.txt был разработан продукт bookmarknote, Если он у вас еще установлен, то болтается с возможностью добавить его куда попало. Легким движением руки, теперь его можно сделать содержимым исключительно Notebook, достаточно включить в вышеупомянутый configure.zcml следующие строки:
<class class="bookmarknote.bookmarknote.BookmarkNote">
<implements interface=".interfaces.INotebookContent"/>
</class>
Все, вот она вожделенная простота.
Заключение :
Хотя пример создания контент-класса можно на этом считать законченным, и любой может последовать этому примеру для получения вполне работающего кода, хочется заострить внимание на некоторых моментах:
- Приведенный здесь способ создания ограничения на вхождение как отдельного продукта notestrict является достаточно общей методикой, о которой специально будет рассказано в статье; Способ задания ограничений на вхождение.txt;
- Представьте себе, что у вас есть готовый продукт Note/Notebook, написанный кем-то для работы в какой-то совсем другой среде. Никто не мешает, декларировать в отдельном продукте интерфейсы для этих классов, зарегистрировать классы и интерфейсы и начать использовать внутри Zope. Единственно затруднение - наследование от persistent.Persistent - легко обходится введением дополнительного класса с множественным наследованием.
Таким образом, даже такой простой и маленький пример хорошо демонстрирует все преимущества и гибкость компонентной модели.



