
Preparing
---------

For the upcoming tests, let's create some basic objects.

  >>> from zope.publisher.browser import BrowserView, TestRequest
  >>> from zope.publisher.interfaces.browser import IBrowserView
  >>> from zope.interface import implementer

  >>> class IBasicView(IBrowserView):
  ...     pass

  >>> @implementer(IBasicView)
  ... class BasicView(BrowserView):
  ...     pass

  >>> class Context(object):
  ...     pass

  >>> context = Context()
  >>> request = TestRequest()
  >>> view = BasicView(context, request)


Base Content Provider
---------------------

Now, let's create a base content provider and see if the interface
provided is correct.

  >>> from zope.contentprovider.interfaces import IContentProvider
  >>> from canonical.content.provider import ContentProvider
  >>> from zope.interface.verify import verifyObject

  >>> provider = ContentProvider(context, request, view)

  >>> verifyObject(IContentProvider, provider)
  True

  >>> provider.context is context
  True
  >>> provider.request is request
  True
  >>> provider.view is view
  True
  >>> provider.update()
  >>> provider.render()
  ''


Template Content Provider
-------------------------

A content provider which is capable of rendering a template is
also available.

  >>> from canonical.content.provider import TemplateContentProvider
  >>> from tempfile import NamedTemporaryFile

  >>> file = NamedTemporaryFile()
  >>> _ = file.write(b"""\
  ... Class names:
  ...   <span tal:replace="provider/__class__/__name__" />
  ...   <span tal:replace="context/__class__/__name__" />
  ...   <span tal:replace="request/__class__/__name__" />
  ...   <span tal:replace="view/__class__/__name__" />
  ... """)
  >>> file.flush()

  >>> class TestContentProvider(TemplateContentProvider):
  ...     template_filename = file.name

  >>> provider = TestContentProvider(context, request, view)
  >>> print((provider.render()))
  Class names:
    TestContentProvider
    Context
    TestRequest
    BasicView
  <BLANKLINE>


Registering via ZCML
--------------------

The content package also provides additional ZCML directives to
register content providers via ZCML.  Let's create a test module
to be able to access created classes and interfaces via ZCML.

  >>> import sys, types
  >>> import canonical.content
  >>> from canonical.testing.resources import zcml

  >>> content_test_module = types.ModuleType("content_test_module")
  >>> sys.modules["content_test_module"] = content_test_module
  >>> content_test_module.Context = Context

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider1"
  ...        for="content_test_module.Context"
  ...        template="%s" />
  ... </configure>
  ... """ % file.name, packages=[canonical.content])
  <...>

  >>> from zope.component import queryMultiAdapter

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider1")

  >>> print(provider.render())
  Class names:
    ZCMLContentProvider
    Context
    TestRequest
    BasicView
  <BLANKLINE>


Things get more interesting when we provide a class to be used as
the base of the content provider.  Let's create a basic class to
play with.

  >>> class MyProvider(object):
  ...
  ...     def update(self):
  ...         self.view = None
  ...
  ...     def render(self):
  ...         render = getattr(super(MyProvider, self), "render", None)
  ...         if render:
  ...             result = render()
  ...             if result:
  ...                 return "This is from MyProvider's super:\n" + result
  ...         return "I'm from MyProvider.render"
  ...
  ...     def some_method(self):
  ...         return "I'm from MyProvider.some_method"

  >>> content_test_module.MyProvider = MyProvider


Our first attempt will be using this class with our previously
created template.  When class and template are used together,
the resulting content provider will inherit from a class to handle
the template and also the class explicitly provided.  In this case,
if a render method exists in the provided class, it will be
respected.


  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider2"
  ...        for="content_test_module.Context"
  ...        class="content_test_module.MyProvider"
  ...        template="%s" />
  ... </configure>
  ... """ % file.name, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider2")

  >>> provider.update()
  >>> print(provider.render())
  This is from MyProvider's super:
  Class names:
    ZCMLContentProvider
    Context
    TestRequest
    NoneType
  <BLANKLINE>

  >>> isinstance(provider, MyProvider)
  True
  >>> provider.update.__func__ is MyProvider.update
  True
  >>> provider.render.__func__ is MyProvider.render
  True



Now, let's try the same without the template.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider3"
  ...        for="content_test_module.Context"
  ...        class="content_test_module.MyProvider" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider3")

  >>> print(provider.render())
  I'm from MyProvider.render

  >>> isinstance(provider, MyProvider)
  True
  >>> provider.update.__func__ is MyProvider.update
  True
  >>> provider.render.__func__ is MyProvider.render
  True


One more time, now using an alternative method.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider4"
  ...        for="content_test_module.Context"
  ...        class="content_test_module.MyProvider"
  ...        attribute="some_method" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider4")

  >>> print(provider.render())
  I'm from MyProvider.some_method

  >>> isinstance(provider, MyProvider)
  True
  >>> provider.update.__func__ is MyProvider.update
  True
  >>> provider.render.__func__ is MyProvider.some_method
  True


We can also define a type of request that this provider should be
applied to.  Let's create a marker interface and try it out.

  >>> from zope.interface import Interface, directlyProvides

  >>> class IMarker(Interface):
  ...     pass

  >>> content_test_module.IMarker = IMarker

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider5"
  ...        for="content_test_module.Context"
  ...        layer="content_test_module.IMarker"
  ...        class="content_test_module.MyProvider" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider5")

  >>> print(provider)
  None

  >>> directlyProvides(request, IMarker)

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider5")

  >>> print(provider)
  <...ZCMLContentProvider object at ...>


The same system also works for views.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider6"
  ...        for="content_test_module.Context"
  ...        view="content_test_module.IMarker"
  ...        class="content_test_module.MyProvider" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider6")

  >>> print(provider)
  None

  >>> directlyProvides(view, IMarker)

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider6")

  >>> print(provider)
  <...ZCMLContentProvider object at ...>


In all of these cases, a permission checker was defined for the
resulting content provider. We can easily check that this is true.

  >>> from zope.security.checker import getCheckerForInstancesOf, CheckerPublic

  >>> checker = getCheckerForInstancesOf(provider.__class__)
  >>> sorted(checker.get_permissions.keys())
  ['__parent__', 'context', 'render', 'request', 'update', 'view']

  >>> permissions = set(checker.get_permissions.values())
  >>> len(permissions) == 1 and list(permissions)[0] is CheckerPublic
  True


The ZCML directive is also capable of registering forward adapters
or content providers. Let's try it out by forwarding a provider
which expects an IMarker interface in the view, to one that doesn't
expect anything.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider8"
  ...        for="content_test_module.Context"
  ...        layer="content_test_module.IMarker"
  ...        view="content_test_module.IMarker"
  ...        forward="provider1" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider8")

  >>> print(provider.render())
  Class names:
    ZCMLContentProvider
    Context
    TestRequest
    BasicView
  <BLANKLINE>


What about an empty content provider?

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider9"
  ...        for="content_test_module.Context"
  ...        empty="true" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider9")

  >>> print(provider.render())
  <BLANKLINE>


Instead of using a template file, it's possible to inline the content of
provider directly in the ZCML declaration.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/browser">
  ...    <provider
  ...        name="provider9"
  ...        for="content_test_module.Context"
  ...        layer="content_test_module.IMarker"
  ...        view="content_test_module.IMarker"
  ...        content="Context class: ${context/__class__/__name__}" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> provider = queryMultiAdapter((context, request, view),
  ...                              IContentProvider, "provider9")

  >>> print(provider.render())
  Context class: Context


Form Content Provider
---------------------

FormContentProvider is a content provider which builds on the
features offered by zope.formlib to allow easy creation of
forms.

Let's borrow the test interface from formlib to use in our own
tests.

  >>> from zope.schema import TextLine, Int, Float, Datetime
  >>> from zope.interface import invariant, Invalid
  >>> from zope.formlib.interfaces import IDisplayWidget
  >>> from datetime import datetime

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/zope">
  ...   <include package="zope.formlib" />
  ... </configure>
  ... """)
  <...>

  >>> class IOrder(Interface):
  ...     identifier = Int(title="Identifier", readonly=True)
  ...     name = TextLine(title="Name")
  ...     min_size = Float(title="Minimum size")
  ...     max_size = Float(title="Maximum size")
  ...     now = Datetime(title="Now", readonly=True)
  ...
  ...     @invariant
  ...     def max_greater_than_min(order):
  ...         if order.max_size < order.min_size:
  ...             raise Invalid("Maximum is less than minimum")

  >>> directlyProvides(context, IOrder)

  >>> context.identifier = 0
  >>> context.name = "name"
  >>> context.min_size = 0.0
  >>> context.max_size = 0.0
  >>> context.now = datetime(2006, 9, 22)


Now, we can build a content provider form.  By default, FormContentProvider
will render to the result of an action, or to the super().render method.
We'll overload it and provide a custom one to be able to play with the
form in a nice way.

  >>> from canonical.content.form import (
  ...     FormContentProvider, Fields, Actions, Action, set_form_token)

  >>> class FormProvider(FormContentProvider):
  ...
  ...     prefix = "test"
  ...     fields = Fields(IOrder)
  ...     actions = Actions(Action("Edit", success="handle_edit_action"))
  ...     allowed_http_methods = ("GET",)
  ...
  ...     def render(self):
  ...         result = []
  ...         if self.has_errors():
  ...             if self.global_errors:
  ...                 result.append("Global errors:")
  ...                 result.extend("  - %s" % (e,)
  ...                               for e in self.global_errors)
  ...             if self.widget_errors:
  ...                 result.append("Widget errors:")
  ...                 result.extend("  - %s: %s" % (w, e)
  ...                               for w, e in list(self.widget_errors.items()))
  ...         for widget in self.widgets:
  ...             if self.widget_hidden[widget.name]:
  ...                 result.append(widget.hidden())
  ...             else:
  ...                 result.append(widget())
  ...         return "\n".join(result)
  ...
  ...     def handle_edit_action(self, action, data):
  ...         if self.apply_changes(self.context, data):
  ...             print("Context changed")
  ...         else:
  ...             print("No changes")
  ...
  ...     def validate(self, action, data):
  ...         if data.get("max_size") == 20:
  ...             self.add_error("I don't like that data.")
  ...             self.add_error("I hate twenty.", "max_size")
  ...
  ...     def get_initial_data(self):
  ...         return dict(min_size=1)


A little helper to render the form:

  >>> def render_form(cls):
  ...     form = cls(context, request, view)
  ...     form.update()
  ...     print(form.render())


Let's try it.

  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value=""  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value=""  />


Notice that all fields are empty, besides min_size who was specified
via the get_initial_data method.  If we wanted to create an edit form,
we'd probably want to show the current object state.  To do this we'd
enable the 'render_context' of the Fields class when creating the form.

  >>> FormProvider.fields = Fields(IOrder, render_context=True)
  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  0
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="name"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="0.0"  />
  <span class="dateTime">2006 9 22  00:00:00 </span>


Not only the current input values were rendered, but also the readonly
values.  We can ask to omit readonly values if we want.

  >>> FormProvider.fields = Fields(IOrder, render_context=True,
  ...                              omit_readonly=True)
  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="name"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="0.0"  />


If we add a field value to the request, it'll be used in place of
the default value in the context.

  >>> request.form["test.name"] = "Bob"
  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="Bob"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="0.0"  />


Unless we say that we don't want the request values to be rendered.

  >>> FormProvider.render_request = False
  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="name"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="0.0"  />


We can also easily invert the situation, and render *only* values
provided either via the request, or via get_initial_data.

  >>> FormProvider.fields = Fields(IOrder)
  >>> FormProvider.render_request = True

  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="Bob"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="1"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value=""  />


Let's simulate pressing the "Edit" action.

  >>> request.form["test.min_size"] = ""
  >>> request.form["test.max_size"] = ""
  >>> request.form["test.actions.edit"] = ""

  >>> render_form(FormProvider)
  Global errors:
    - Missing authentication token
  ...

Oops... the form authentication token is missing (it's required for
CSRF protection). Add it to the request.

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/zope"
  ...            xmlns:browser="http://namespaces.zope.org/browser">
  ...   <include package="zope.session" />
  ... </configure>
  ... """, packages=[canonical.content])
  <...>

  >>> from zope.session.http import CookieClientIdManager
  >>> from zope.session.interfaces import IClientIdManager, ISessionDataContainer
  >>> from zope.session.session import RAMSessionDataContainer
  >>> from zope.component import provideUtility
  >>> provideUtility(CookieClientIdManager(), IClientIdManager)
  >>> provideUtility(RAMSessionDataContainer(), ISessionDataContainer)

  >>> set_form_token(request)

  >>> render_form(FormProvider)
  Widget errors:
    - test.min_size: <span class="error">Required input is missing.</span>
    - test.max_size: <span class="error">Required input is missing.</span>
  ...


Oops.. we've provided invalid data.  Let's try it again with
better data.

  >>> request.form["test.min_size"] = "small"
  >>> request.form["test.max_size"] = "large"

  >>> render_form(FormProvider)
  Widget errors:
    - test.min_size: <span class="error">Invalid floating point data</span>
    - test.max_size: <span class="error">Invalid floating point data</span>
  ...


Ouch.  Let's try it once more.

  >>> request.form["test.min_size"] = "20"
  >>> request.form["test.max_size"] = "10"

  >>> render_form(FormProvider)
  Global errors:
    - Maximum is less than minimum
  ...


Oh my.. ok, we really want this.

  >>> request.form["test.min_size"] = "10"
  >>> request.form["test.max_size"] = "20"

  >>> render_form(FormProvider)
  Global errors:
    - I don't like that data.
  Widget errors:
    - test.max_size: <span class="error">I hate twenty.</span>
  ...


OH NO!  Ok, that's it.  Last time.

  >>> request.form["test.min_size"] = "10"
  >>> request.form["test.max_size"] = "30"

  >>> render_form(FormProvider) # doctest: +NORMALIZE_WHITESPACE
  Context changed
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="Bob"  />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="10.0"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="30.0"  />


Finally!  It says the context has changed.  Let's see if it has
really changed.

  >>> context.name
  'Bob'
  >>> "%.1f" % context.min_size
  '10.0'
  >>> "%.1f" % context.max_size
  '30.0'


It actually works. ;-)

Hiding specific fields out of the schema is also possible.

  >>> class HiddenFieldsFormProvider(FormProvider):
  ...
  ...     fields = FormProvider.fields.hide("name", "min_size")
  ...     fields = fields.select("name", "min_size", "max_size")
  ...     fields = fields.omit()

  >>> del request.form["test.actions.edit"]

  >>> render_form(HiddenFieldsFormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="hiddenType" id="test.name" name="test.name"
         type="hidden" value="Bob"  />
  <input class="hiddenType" id="test.min_size" name="test.min_size"
         type="hidden" value="10.0"  />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="30.0"  />

Notice that we've used select() and omit() to make sure that the
hidden setting is persisted.

Or you might prefer to disable the HTML elments associated to certain
fields of the rendered form.

  >>> class DisabledFieldsFormProvider(FormProvider):
  ...
  ...     fields = FormProvider.fields.disable("name", "min_size")

  >>> render_form(DisabledFieldsFormProvider) # doctest: +NORMALIZE_WHITESPACE
  <input class="textType" id="test.name" name="test.name" size="20"
         type="text" value="Bob" disabled="disabled" />
  <input class="textType" id="test.min_size" name="test.min_size" size="10"
         type="text" value="10.0" disabled="disabled" />
  <input class="textType" id="test.max_size" name="test.max_size" size="10"
         type="text" value="30.0"  />

Rather than using a custom render method, or relying on the ZCML
directives to hook a form template for us, we can use a pre-defined
class which mixes with TemplateContentProvider to use either a
custom template, or a default one.

  >>> from canonical.content.form import TemplateFormContentProvider

  >>> class TemplateFormProvider(TemplateFormContentProvider):
  ...
  ...     fields = Fields(IOrder, omit_readonly=True).hide("max_size")
  ...     actions = Actions(Action("Edit", success="handle_edit_action"))
  ...
  ...     prefix = "test"
  ...     summary = "Form summary"
  ...     description = "Form description."
  ...
  ...     def handle_edit_action(self, action, data):
  ...         pass

  >>> zcml("""
  ... <configure xmlns="http://namespaces.zope.org/zope"
  ...            xmlns:browser="http://namespaces.zope.org/browser">
  ...   <browser:provider
  ...     name="form-description"
  ...     empty="true"
  ...     />
  ... </configure>""", packages=[canonical.content])
  <...>

It should work out-of-the-box.

  >>> render_form(TemplateFormProvider) # doctest: +NORMALIZE_WHITESPACE
  <form class="content-form" action="http://127.0.0.1"
        method="post" enctype="multipart/form-data"
        id="test-form">
  <div class="form-body">
    <h2 class="form-summary">Form summary</h2>
      <div class="form-content">
        <div class="form-description">Form description.</div>
        <!-- .form-global-errors -->
        <div class="form-rows">
           <div class="form-row">
             <label for="test.name">
               <span class="form-label">Name</span>
               <span class="form-required">(Required)</span>
             </label>
             <div class="form-field">
               <div class="form-widget"><input class="textType" id="test.name" name="test.name" size="20" type="text" value="Bob"  /></div>
              </div>
            </div>
            <div class="form-row">
              <label for="test.min_size">
                <span class="form-label">Minimum size</span>
                <span class="form-required">(Required)</span>
              </label>
              <div class="form-field">
                <div class="form-widget"><input class="textType" id="test.min_size" name="test.min_size" size="10" type="text" value="10.0"  /></div>
              </div>
            </div>
          <input class="hiddenType" id="test.max_size" name="test.max_size" type="hidden" value="30.0"  />
        </div> <!-- .form-rows -->
        <div class="form-actions">
          <span class="form-action">
            <input type="submit" id="test.actions.edit" name="test.actions.edit" value="Edit" class="button primary" /><input type="hidden" name="form-security-token" value="..."/>
          </span>
        </div> <!-- .form-actions -->
      </div> <!-- .form-content -->
    </div>
  </form>


Cleanup
-------

  >>> del sys.modules["content_test_module"]
  >>> file.close()

# vim:ts=4:sw=4:et:ft=doctest

