Browser Page Brainstorming
==========================
Problem
-------
The ``browser:page`` directive does a lot. It can either take a class
or a template or both to make a browser page. In either case it will
create a new class that has the necessary interfaces and methods
inplace. When using just a class, it can take an attribute other than
__call__ to publish.
Why is this a problem? Because certain behaviour is mixed into the
class created on-the-fly. This behaviour is not apparent in our view
class, yet we assume it exists. It's magic.
Goals
-----
* Be more explicit (or, perhaps "declarative" is a better word?) when
creating browser pages (especially in Python).
* Browser "pages" are just views (=adapters) for the Component
Architecture. The fact that something is a view or a page is an
implementation detail of the view or page itself and should not be
of much interest to ZCML.
* The ZCML directive handlers of ``browser:page`` and ``browser:view``
currently perform a lot of automation (stuff you could also call
magic, such as the class creation). Automation should better be
done in Python, using convenient baseclasses (but not requiring
them).
Use cases
---------
1. You have a template, you simply want to register it for an interface
or class.
2. When you realize you need more logic for the template, you want to
add an auxiliary view class to go with the template.
3. Sometimes you want to implement a browser page completely in Python
w/o a template.
It should be possible to change your mind later about a view and
easily go from one usecase to the other. For example, you might find
out only later on that a page template needs additional logic.
Ideas
-----
Let's start with the last usecase:
Usecase 3
~~~~~~~~~
Handling usecase #3 in a decent manner is easy: You simply inherit
from ``zope.formlib.Page``::
from zope.component adapts
from zope.formlib import Page
from zope.publisher.interfaces.browser import IBrowserRequest
from dumpling.interfaces import IDumpling
class DumplingPage(Page):
adapts(IDumpling, IBrowserRequest)
def __call__(self):
return u'Dumplings are delicious'
Registering it would probably work like this::
Where did the ``for`` and ``type`` go? This information is intrinsic
to the page implementation and deducted from the ``adapts()``
information. Note that we don't need to say ``browser:view`` or
``browser:page`` but just simply ``view``. The fact that this has to
do with browsers is a choice of the implementation, not of the
configuration. Note that at this point you could even use the
``adapter`` directive, except that it doesn't have the feature for
declaring ``allowed_attributes (or ``allowed_interface``).
There's another question: What if you have a view class that contains
multiple pages and they all use common logic? Or when you have several
pages that are logically grouped? Here's how that could work out::
from zope.component adapts
from zope.wherever import Pages
from zope.publisher.interfaces.browser import IBrowserRequest
from dumpling.interfaces import IDumpling
class DumplingPages(Pages):
adapts(IDumpling, IBrowserRequest)
def index(self):
return u'Dumplings are delicious'
def form(self):
return u'...
...'
def update(self, data):
self.context.data = data
self.request.response.redirect(u'@@index.html')
You would register them using a ``browser:views`` call:
The ``browser:views`` directive used here is very similar to the
existing ``browser:pages`` directive except that it doesn't do as much
magic. Does ``browser:views`` create classes on-the-fly? Yes, it has
to because it creates more than one adapter. However, it does not
magically add logic to the classes, just a hint on which page name is
to be published in a particular adapter. All of the logic is provided
through the ``Pages`` baseclass.
Use case 2
~~~~~~~~~~
We have a template that requires some view logic. This logic is
typically placed in a helper method in a view class. So, we can take
the ``DumplingPage`` from above, let __call__ not be a method but a
page template and add the helper method, like so::
from zope.component adapts
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.formlib import Page
from zope.app.pagetemplate import ViewPageTemplate
from dumpling.interfaces import IDumpling
class DumplingPage(Page):
adapts(IDumpling, IBrowserRequest)
def aHelperMethod(self):
return u'I\'m just here to help'
__call__ = ViewPageTemplate('dumpling.pt')
Registration of the page stays the same (simple ``view`` directive).
What if someone wants to customize this view by changing the template
but not the helper method? You either have to subclass this view class
and add a custom __call__ to your subclass, or you use *named
templates* from zope.formlib:
from zope.component adapts
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.formlib import Page
from zope.formlib.namedtemplate import NamedTemplate
from zope.formlib.namedtemplate import NamedTemplateImplementation
from zope.app.pagetemplate import ViewPageTemplate
from dumpling.interfaces import IDumpling
class DumplingPage(Page):
adapts(IDumpling, IBrowserRequest)
def aHelperMethod(self):
return u'I\'m just here to help'
__call__ = NamedTemplate('dumplingview')
default_dumplingview = NamedTemplateImplementation(
ViewPageTemplate('dumpling.pt'), DumplingPage)
The registration of the view stays the same, we just need to register
an additional named adapter (the ``default_dumplingview`` one). This
is possible today.
Usecase 1
~~~~~~~~~
So far, we've had browser pages that also involved Python code.
Creating a Python class was therefore the right thing to do. For
usecase 1 we just want to register a template. Of course, we could
do::
from zope.component adapts
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.app.pagetemplate import ViewPageTemplate
from dumpling.interfaces import IDumpling
class DumplingPage(Page):
adapts(IDumpling, IBrowserRequest)
__call__ = ViewPageTemplate('dumpling.pt')
and register it as all the single-page views before. While it seems
like a lot of code just to get a page template running as a view, it
has the advantage that adding more view logic is a piece of cake now.
Just add those helper methods and you're done. I think we want more
convenience, though. How about::
from zope.somewhere import BrowserPageTemplate
from dumpling.interfaces import IDumpling
dumplingPage = BrowserPageTemplate('dumpling.pt', IDumpling)
Then we again use a simple ``view`` directive to register it, much
like the ones before. This is also very similar to the way we create
named templates: create the object itself in Python and just register
it in ZCML.
Do you want more automation? Well, we could of course get rid of
everything in Python and do it in ZCML as we do now, e.g. with a
``browser:page`` directive that only takes a ``template`` argument and
no ``class``. This would mean ZCML would be generating a class again,
much like does now.
Thoughts
--------
* Using the ``Page`` baseclass and the ``view`` directive, we easily
get rid of automatic class creation. The view class subclassing
``Page`` is directly registered as the adapter factory. When the
view is looked up later, it will be in fact an instance of that
class and not some magically created subclass.
* When creating multiple pages from one class, the ``Pages`` baseclass
in conjunction with the ``@page`` decorator feels like a compromise
because the necessary ``browser:pages`` directive would have to
create simple subclasses. These subclasses wouldn't contain logic
(this would be in the ``Pages`` baseclass) but they are still
created on-the-fly.
* When using a simple ``view`` directive to register browser pages,
the directive handler would deduct the request type from the
``adapts()`` statement. That means when the pages state
``adapts(IDumpling, IBrowserRequest)``, they will be registered for
``IBrowserRequest`` unless a specific layer is specified using the
view's ``type`` argument.
The problem with this is that it was once argued that browser views
should by default not be registered directly for ``IBrowserRequest``
but for an arbritrary layer we happen to call
``IDefaultBrowserLayer`` (the former ``default`` layer). People
have expressed usecases for this behaviour, we should keep it.
A good solution would be to create a specialized form of the
``view`` directive (potentially ``browser:view`` if sane BBB can be
provided) and let that directive's default ``type`` argument be
``IDefaultBrowserLayer``. The rest of the directive would behave
exactly like the ``view`` directive.
Questions
---------
* Does anyone currently use ``browser:view``? If so, why (and why
``browser:page`` not enough?)
* Does anyone need ZCML to do automation? IOW, would it be much of a
burden having to create ``BrowserPageTemplate`` objects in Python
first before hooking them up as views?
* I feel that there are many usecases for multiple pages from one view
class which is why I've tried to come up with a solution that has
almost no magic (mind the "almost"). Does anyone have a better
solution without magic?
References
----------
[1] "ZCML needs to do less": blog entry, origin of some of the ideas
mentioned above.
http://www.z3lab.org/sections/blogs/philipp-weitershausen/2005_12_14_zcml-needs-to-do-less
[2] "Reducing the amount of ZCML directives": Proposal on the removal
of ZCML directives that are either obsolete or introduce too much
indirection. This proposal was largely influenced by [1], just
like the ideas above.
http://dev.zope.org/Zope3/ReducingTheAmountOfZCMLDirectives
[3] "Simplify skinning": Proposal regarding the consolidation of the
layer and skin concept. The above ideas assume that this proposal
has already been implemented (which it is not at the time of this
writing).
http://dev.zope.org/Zope3/SimplifySkinning
[4] "Brainstorming about browser pages": Mailinglist thread announcing
this document.
http://mail.zope.org/pipermail/zope3-dev/2006-February/018255.html