Случай использования интерфейсов в скине

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

Давайте построим адаптер вида - форму удаления объекта, при этом контекстом данного вида будет сам удаляемый объект.

configure.zcml:

<z3c:pagelet
    for=".interfaces.IMyContentType"
    name="delete"
    view=".delete.DeleteForm"
    permission="zope.ManageContent"
    />

delete.py:

from z3c.formui import form
from z3c.form import button
from zope.traversing.browser.absoluteurl import absoluteURL

class DeleteForm(form.Form):

    label = u'Delete this:'

    @button.buttonAndHandler(u'Yes')
    def handle_yes(self, action):
        name = self.context.__name__
        container = self.context.__parent__
        del container[name]
        self.request.response.redirect(absoluteURL(container, self.request))

    @button.buttonAndHandler(u'No')
    def handle_no(self, action):
        self.request.response.redirect('.')

Теперь по красивому адресу http://example.com/mycontent_item/delete находится формочка, переспрашивающая про удаление mycontent_item и делающая редиректы куда следует.

Сложность в том, что на реальном bluebream проекте при нажатии кнопки "Yes" велика вероятность получить ошибку "Объект такой-то не существует", несмотря на то, что в самой форме мы позаботились о том, чтобы не дергать уже уничтоженный контекст.

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

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

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

Для этого нужно 2 новых интерфейса и одна дополнительная декларация в zcml. Отмечу для построения компонентных видов в нашем примере используются пейджелеты.

  1. Сделаем интерфейс для всех страниц удаления объектов и интерфейс для сайдбаров-пустышек. myproject/skin/interfaces.py:

    from z3c.pagelet.interfaces import IPagelet
    from zope.viewlet.interfaces import IViewletManager
    
    class IContextDeletePagelet(IPagelet):
        """Виды, которые удаляют свой же контекст,
        долны предоставлять это интерфейс.
        """
    
    class IEmptyViewletManager(IViewletManager):
        """Вьюлет-менеджеры - пустышки"""
    
  2. Предположим (вполне типично), что злополучный сайдбар, в котором "сидят" вечно мешающиеся и дергающие контекст вьюлеты, задан так:

myproject/skin/configure.zcml:

<viewletManager
    name="sidebar"
    class="zope.viewlet.manager.ConditionalViewletManager"
    provides=".interfaces.ISidebar"
    permission="zope.View"
    />

Для достижения нашей цели - зарегистрируем одноименный вьюлет-менеджер на интерфейс вьюлет-менеджера-пустышки, дейстительный на страницах, предоставляющих интерфейс удаления своего контекста:

<viewletManager
   name="sidebar"
   class="zope.viewlet.manager.ConditionalViewletManager"
   provides=".interfaces.ISidebarEmpty"
   view=".interfaces.IContextDeletePagelet"
   permission="zope.View"
   />

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

<z3c:pagelet
    for=".interfaces.IMyContentType"
    name="delete"
    view=".delete.DeleteForm"
    permission="zope.ManageContent"
    provides="myproject.skin.interfaces.IContextDeletePagelet"
    />

В целом, при проектировании следует ориентироваться на интерфейсы и не стесняться создавать новые, пусть и для каждой мелочи. Это позволяет делать сложные вещи исключительно декларативными конструкциями, коими являются интерфейсы и zcml-описания.