Locally customizing template-based views
========================================

This document describes a typical story of locally customizing a
global view component.  The steps in this story are:

1. Making a folder a site

2. Walking up to an object and getting a list of its template-based
   views

3. Selecting a particular view, seeing its template source and
   deciding to customize it

4. Customizing the template and seeing the customized template take
   effect

5. Deleting the template and seeing the old view take over again


Setup
-----

  >>> app = layer['app']


1. Turning an ObjectManager into a site
----------------------------------------

Let's create a folder that we'll turn into a Zope3-style site:

  >>> from OFS.ObjectManager import ObjectManager
  >>> site = ObjectManager()

We need to add it to the root so that objects contained in it have a
proper acquisition chain all the way to the top:

  >>> id = app._setObject('site', site)
  >>> site = app.site

Now we make this a real site by using a view that a) sets
``IObjectManagerSite``, b) sets a traversal hook and c) gives the site
a component registration object (formerly known as site manager):

  >>> import zope.component
  >>> from zope.publisher.browser import TestRequest
  >>> request = app.REQUEST
  >>> view = zope.component.getMultiAdapter((site, request),
  ...                                       name=u"components.html")
  >>> view.makeSite()

Now the site provides ``IObjectManagerSite``:

  >>> from Products.Five.component.interfaces import IObjectManagerSite
  >>> IObjectManagerSite.providedBy(site)
  True

And it has a site manager (component registry):

  >>> site.getSiteManager() # doctest: +ELLIPSIS
  <PersistentComponents ...>


2. Template-based views available on an object
----------------------------------------------

Let's create a simple content object that we put into the folder
(a.k.a. the site):

  >>> from Products.Five.tests.testing.simplecontent import SimpleContent
  >>> item = SimpleContent('item', 'An item')
  >>> site._setOb('item', item)
  >>> item = site.item

Let's get a list of views (that also shows where each view is
registered at):

  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"zptviews.html")
  >>> from pprint import pprint
  >>> viewnames = [reg.name for reg in view.templateViewRegistrations()]
  >>> viewnames.sort()
  >>> u'customizezpt.html' in viewnames
  True
  >>> u'zptviews.html' in viewnames
  True

3. and 4. Customizing a template-based view
-------------------------------------------

In the list of template-based browser views we can select one and see
the source of its template:

  >>> from Acquisition import aq_base
  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"customizezpt.html")
  >>> template = view.templateFromViewName(u'customizezpt.html')
  >>> 'PageTemplateFile' in repr(aq_base(template))
  True
  >>> import os.path
  >>> os.path.basename(template.filename)
  'customize.pt'

  >>> print(view.templateCodeFromViewName(u'customizezpt.html')) # doctest: +ELLIPSIS
  <html metal:use-macro="context/@@standard_macros/view"
        i18n:domain="zope">
  ...
    <p i18n:translate="">This is the source of the
    <code tal:content="request/form/viewname">viewname</code>:</p>
  ...

We now hit the customize button and get a customized ZPT template:

  >>> zpt = view.doCustomizeTemplate(u'customizezpt.html')

That actually creates a TTWViewTemplate object in the nearest site
(perhaps later we'd like to have the option to pick which of the sites
above us should be targeted)

  >>> zpt = getattr(site, 'customize.pt')
  >>> print(zpt.read()) # doctest: +ELLIPSIS
  <html metal:use-macro="context/@@standard_macros/view"
        i18n:domain="zope">
  ...
    <p i18n:translate="">This is the source of the
    <code tal:content="request/form/viewname">viewname</code>:</p>
  ...

It also registers this component as a view now, so when we look up the
view again, we get the customized one.  Therefore let us actually
change the template to give us some info output:

  >>> zpt.pt_edit("""
  ... context:   <tal:var replace="structure context" />
  ... template:  <tal:var replace="structure nocall:template" />
  ... request:   <tal:var replace="structure python:repr(request)" />
  ... view:      <tal:var replace="structure nocall:view" />
  ... modules:   <tal:var replace="structure modules" />
  ... options:   <tal:var replace="structure options" />
  ... nothing:   <tal:var replace="structure nothing" />
  ... """, 'text/html')

In order to be able to look up the customized view now, we need to
make the site the current site:

  >>> from zope.component.hooks import setSite
  >>> setSite(site)

The newly registered adapter has an explicit security check that
matches the original (in this case 'Manage Five local sites'),
so calling it will fail unless logged in:

  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"customizezpt.html")
  >>> print(view())
  Traceback (most recent call last):
  ...
  AccessControl.unauthorized.Unauthorized: The current user does not have the required "Manage Five local sites" permission

Now look it up as manager and compare its output:

  >>> from plone.testing.zope import login
  >>> app['acl_users'].userFolderAddUser('admin', 'secret', ['Manager'], [])
  <User 'admin'>
  >>> login(app['acl_users'], 'admin')
  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"customizezpt.html")
  >>> print(view()) # doctest: +ELLIPSIS
  context:   <SimpleContent at item>
  template:  <TTWViewTemplate at customize.pt>
  request:   <HTTPRequest, ...>
  view:      <five.customerize.zpt.TTWView...>
  modules:   <Products.PageTemplates.ZRPythonExpr._SecureModuleImporter object at ...>
  options:   {'args': ()}
  nothing:...

5. Deleting view templates
--------------------------

Once in a while we would like to get rid of the customized view.  The
easiest way to do that is to simply delete the template:

  >>> site._delObject('customize.pt')

Now the view look-up is back to the old way:

  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"customizezpt.html")
  >>> print(open(view.index.filename, 'rb').read().decode('utf8')) #doctest: +ELLIPSIS
  <html metal:use-macro="context/@@standard_macros/view"
        i18n:domain="zope">
  ...
    <p i18n:translate="">This is the source of the
    <code tal:content="request/form/viewname">viewname</code>:</p>
  ...

6. Views with custom classes
----------------------------

Sometimes view classes have custom base classes which we need to be
available to the customized local view.  We create one of these views
(using all the normal browser:page insanity) and then customize it:

  >>> from Products.Five.browser import BrowserView
  >>> class TestView(BrowserView):
  ...     """A view class"""
  ...     __name__ = 'mystaticview.html'
  ...     def foo_method(self):
  ...         return 'baz'

  >>> try:
  ...     from Products.Five.browser.metaconfigure import makeClassForTemplate
  ... except ImportError:
  ...     makeClassForTemplate = None
  ...     from Products.Five.browser.metaconfigure import SimpleViewClass

  >>> from AccessControl.security import getSecurityInfo, protectClass
  >>> from AccessControl.class_init import InitializeClass
  >>> viewclass = None
  >>> if makeClassForTemplate:
  ...     cdict = getSecurityInfo(TestView)
  ...     cdict['__name__'] = 'simpleview.html'
  ...     viewclass = makeClassForTemplate(
  ...         'testviewtemplate.pt',
  ...         globals=globals(),
  ...         bases=(TestView,),
  ...         cdict=cdict,
  ...         name='simpleview.html'
  ...     )
  ... else:
  ...     viewclass = SimpleViewClass(
  ...         'testviewtemplate.pt',
  ...         bases=(TestView,),
  ...         name='simpleview.html'
  ...     )
  >>> protectClass(viewclass, 'zope.Public')
  >>> InitializeClass(viewclass)
  >>> from zope.component import provideAdapter
  >>> from zope.interface import Interface
  >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
  >>> provideAdapter(viewclass, (Interface, IDefaultBrowserLayer),
  ...                    Interface, name='simpleview.html')

Now we retrieve the view and make sure it does what it should:

  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"simpleview.html")
  >>> 'A simple view template with a class' in view()
  True

And we customize it:

  >>> customizeview = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"customizezpt.html")
  >>> zpt = customizeview.doCustomizeTemplate(u'simpleview.html')
  >>> template = getattr(site, 'testviewtemplate.pt')
  >>> template.pt_edit('''\
  ... A customized view
  ... <span tal:replace="view/foo_method" />''', 'text/html')

And render it again:

  >>> view = zope.component.getMultiAdapter((item, request),
  ...                                       name=u"simpleview.html")
  >>> 'A customized view' in view()
  True
