--- a/src/source/appextend.rst Wed May 16 14:03:44 2018 +0200
+++ b/src/source/appextend.rst Fri May 18 11:23:53 2018 +0200
@@ -1,167 +1,11 @@
.. _appextend:
-How to create a new Portlet ?
------------------------------
-
-**Portlets** are pluggable user interface software components that are managed and displayed in a web portal,
-for example an enterprise portal or a web CMS. A portlet can aggregate (integrate) and personalize content from
-different sources within a web page. A portlet responds to requests from a web client and generates dynamic content
-(*reference:* `Wiki portlet`_).
-
-.. _`wiki portlet`: https://en.wikipedia.org/wiki/Portlet
-
-
-**PyAMS Portal** provides the portal engine but only a very small set of predefined portlets that can be used to compose
-and organize the structure of a web page; additional portlets are provided by other packages, like
-:ref:`pyams_content`.
-
-
-1. Define portlet settings
-''''''''''''''''''''''''''
-
-Portlet settings interface are defined in ``interfaces.py``, you can use :py:class:`pyams_portal.interfaces.IPortletSettings`
-or extend the interface by adding additional properties for example:
-
-.. code-block:: python
-
- from zope.schema import Text
-
- NEW_PORTLET_NAME = 'new.portlet'
-
- class INewPortletSettings(IPortletSettings):
-
- comment = Text(title=_("Comment"),
- required=True)
-
-
-A :py:class:`pyams_portal.portlet.PortletSettings` persistent subclass then implements what IPortletSettings describes:
-
-.. code-block:: python
-
- @implementer(INewPortletSettings)
- class NewPortletSettings(PortletSettings):
- """Portlet settings"""
-
- comment = FieldProperty(INewPortletSettings['comment'])
-
-
-2. Create Portlet
-'''''''''''''''''
-
-The Portlet component is a utility, which implements the :py:class:`pyams_portal.interfaces.IPortlet` interface and is
-registered by the :py:func:`pyams_portal.portlet.portlet_config` decorator;
-
-.. code-block:: python
-
- @portlet_config(permission=VIEW_PERMISSION)
- class ImagePortlet(Portlet):
- """Image portlet"""
-
- name = NEW_PORTLET_NAME
- label = _("New portlet")
-
- toolbar_image = None
- toolbar_css_class = 'fa fa-fw fa-2x fa-picture-o'
-
- settings_class = NewPortletSettings
-
-
-Where:
- - **permission**: permission required to display this portlet content
- - **name**: internal name given to this portlet. This name must be unique between all portlets, so using your own
- namespace into this name is generally a good option!
- - **label**: user label given to this portlet
- - **toolbar_image**: URL of an image used to display portlet button into ZMI toolbar, if any
- - **toolbar_css_class**: CSS class used to display portlet button into ZMI toolbar, if any
- - **settings_class**: class used to store portlet settings.
-
-3. Create portlet settings edit form
-''''''''''''''''''''''''''''''''''''
-
-Portlet settings have to be updated through management interface via a :py:class:`pyams_portal.zmi.portlet.PortletSettingsEditor`
-subform:
-
-.. code-block:: python
-
- @pagelet_config(name='properties.html', context=INewPortletSettings, layer=IPyAMSLayer,
- permission=VIEW_SYSTEM_PERMISSION)
- class NewPortletSettingsEditor(PortletSettingsEditor):
- """New portlet settings editor"""
-
- settings = INewPortletSettings
-
-
- @adapter_config(name='properties.json', context=(INewPortletSettings, IPyAMSLayer), provides=IPagelet)
- class NewPortletSettingsAJAXEditor(AJAXEditForm, NewPortletSettingsEditor):
- """New portlet settings editor, JSON renderer"""
-
-
-4. Previewing portlet content
-'''''''''''''''''''''''''''''
-
-A *previewer* is used into ZMI to display portlet content into portal template definition page:
-
-.. code-block:: python
-
- @adapter_config(context=(Interface, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletPreviewer)
- @template_config(template='my-portlet-preview.pt', layer=IPyAMSLayer)
- class NewPortletPreviewer(PortletPreviewer):
- """New portlet previewer"""
-
-
-The previewer template is a Chameleon template:
-
-.. code-block:: genshi
+Extending PyAMS
+===============
- <tal:var define="settings view.settings">
- <tal:if condition="settings.visible">
- <div tal:content="settings.comment">Comment</div>
- </tal:if>
- <tal:if condition="not settings.visible">
- <div class="text-center padding-y-5">
- <span class="fa-stack fa-lg">
- <i class="fa fa-eye fa-stack-1x"></i>
- <i class="fa fa-ban fa-stack-2x text-danger"></i>
- </span>
- </div>
- </tal:if>
- </tal:var>
-
-Here we check if portlet is visible or not to display a small icon when hidden; otherwise we display entered comment.
-
-
-5. Rendering portlet content
-''''''''''''''''''''''''''''
-
-A *renderer* is used to display portlet content into rendered page content:
-
-.. code-block:: python
+.. toctree::
+ :maxdepth: 1
- @adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletRenderer)
- @template_config(template='my-portlet-render.pt', layer=IPyAMSLayer)
- class NewPortletRenderer(PortletRenderer):
- """New portlet renderer"""
-
- label = _("Default comment renderer")
-
-
-Each renderer template is also a Chameleon template:
-
-.. code-block:: genshi
-
- <div class="comment" tal:content="view.settings.comment">Comment</div>
-
-
-This is the configuration of a *default* renderer defined for this portlet; you can provide several renderers for a
-given portlet by given distinct names to the adapters:
-
-.. code-block:: python
-
- @adapter_config(name='another-renderer',
- context=(IPortalContext, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletRenderer)
- @template_config(template='my-portlet-render-2.pt', layer=IPyAMSLayer)
- class AnotherNewPortletRenderer(PortletRenderer):
- """Another new portlet renderer"""
-
- label = _("Another comment renderer")
+ howto-adapter
+ howto-portlet
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/source/howto-adapter.rst Fri May 18 11:23:53 2018 +0200
@@ -0,0 +1,180 @@
+.. _adapterhowto:
+
+
+How to define an annotations adapter?
+=====================================
+
+Adapters are important concepts of ZCA and PyAMS framework. If you don't know what are adapters, see :ref:`zca`.
+
+
+What are annotations?
++++++++++++++++++++++
+
+When an adapter have to add persistent attributes to a persistent class, it can add these attributes directly into
+persistent instances. But this way can lead to conflicts when several adapters want to use the same attribute name for
+different kinds of information.
+
+Annotations are an elegant way to handle this use case: they are based on a BTree which is stored into a
+specific instance attribute (*__annotations__*). Any adapter can then use this dictionary to store it's own
+informations, using it's own namespace as dictionary key.
+
+ZODB browser allows you to display existing annotations:
+
+.. image:: _static/annotations-1.png
+
+This example displays several annotations, each using it's own namespace:
+
+.. image:: _static/annotations-2.png
+
+
+Designing interfaces
+++++++++++++++++++++
+
+The first step with ZCA is to design your interfaces.
+
+The are going to base our example on PyAMS_content 'paragraphs' component: a content class is marked as a
+*paragraphs container target*, a class that can store paragraphs. But the real storage of paragraphs is done by
+another *container* class:
+
+.. code-block:: python
+ :linenos:
+
+ from zope.annotation.interfaces import IAttributeAnnotatable
+ from zope.containers.constraints import containers, contains
+
+
+ class IBaseParagraph(Interface):
+ """Base paragraph interface"""
+
+ containers('.IParagraphContainer')
+
+
+ class IParagraphContainer(IOrderedContainer):
+ """Paragraphs container"""
+
+ contains(IBaseParagraph)
+
+
+ class IParagraphContainerTarget(IAttributeAnnotatable):
+ """Paragraphs container marker interface"""
+
+
+ PARAGRAPH_CONTAINER_KEY = 'pyams_content.paragraph'
+
+
+- line 5 to 8: :class:`IBaseParagraph` is the base interface for all paragraphs; constraint implies that paragraphs
+ can only be stored in a container implementing :class:`IParagraphContainer` interface.
+- line 11 to 14: :class:`IParagraphContainer` is the base interface for paragraphs containers; constraint implies that
+ such a container can only contain objects implementing :class:`IBaseParagraph` interface.
+- line 17 to 18: :class:`IParagraphContainerTarget` is only a *marker* interface which doesn't provide any method or
+ attribute; it only inherits from :class:`IAttributeAnnotatable`, which implies that classes implementing this
+ interface allows other classes to add informations as annotations through a dedicated *__annotations__* attribute.
+- line 21: this is the key which will be used to store our annotation.
+
+
+Creating persistent classes
++++++++++++++++++++++++++++
+
+The first step is to declare that a given content class can store paragraphs:
+
+.. code-block:: python
+ :linenos:
+
+ from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget
+ from zope.interface import implementer
+
+ @implementer(IParagraphContainerTarget)
+ class WfNewsEvent(WfSharedContent):
+ """News event class"""
+
+Here we just say "Well, I'm a shared content, and I'm OK to store paragraphs!".
+
+So we can design the paragraphs container class. It's this class which will *really* store the paragraphs:
+
+.. code-block:: python
+ :linenos:
+
+ @implementer(IParagraphContainer)
+ class ParagraphContainer(BTreeOrderedContainer):
+ """Paragraphs container"""
+
+The paragraphs container class inherits from a :class:`BTreeOrderedContainer` and implements
+:class:`IParagraphContainer`.
+
+The last operation is to create the adapter, which is the *glue* between the *target* class and the paragraphs
+container:
+
+.. code-block:: python
+ :linenos:
+
+ from pyams_utils.adapter import adapter_config, get_annotation_adapter
+
+ @adapter_config(context=IParagraphContainerTarget, provides=IParagraphContainer)
+ def paragraph_container_factory(target):
+ """Paragraphs container factory"""
+ return get_annotation_adapter(target,
+ PARAGRAPH_CONTAINER_KEY,
+ ParagraphContainer,
+ name='++paras++')
+
+PyAMS provides a shortcut to create an annotation adapter in :func:`pyams_utils.adapter.get_annotation_adapter`.
+It's mandatory arguments are:
+
+- **context** (line 6): the context to which the adapter is applied
+- **key** (line 7): the string key used to access and store context's annotations
+- **factory** (line 8): if the requested annotation is missing, a new one is created using this factory (which can be a class or
+ a function)
+
+Optional arguments are:
+
+- **markers** (None by default): if set, should be a list of marker interfaces which will be assigned to object
+ created by the factory
+- **notify**: if *True* (default), an :class:`ObjectCreatedEvent` event is notified on object creation
+- **locate**: if *True* (default), context is set as *parent* of created object
+- **parent**: if *locate* is True and if *parent* is set, this is the object to which the new object should be *parented*
+ instead of initial context
+- **name** (None by default): some objects need to be traversed, especially when you have to be able to access them through an URL; this
+ is the name given to created object.
+
+
+Using your adapter
+++++++++++++++++++
+
+Starting from your *content* object, it's then very simple to access to the paragraphs container:
+
+.. code-block:: python
+ :linenos:
+
+ event = WfNewsEvent()
+ paragraphs_container = IParagraphContainer(event, None)
+
+And that's it! From now I can get access to all paragraphs associated with my initial content!!
+
+
+Managing traversal
+++++++++++++++++++
+
+As said before, sometimes you have to be able to *traverse* from an initial content to a given sub-content
+managed by an adapter.
+
+PyAMS defines a custom :class:`pyams_utils.traversing.NamespaceTraverser`: when a request traversing subpath is
+starting with '++' characters, it is looking for a named traverser providing :class:`ITraversable` interface
+to the last traversed object.
+
+.. code-block:: python
+ :linenos:
+
+ @adapter_config(name='paras', context=IParagraphContainerTarget, provides=ITraversable)
+ class ParagraphContainerNamespace(ContextAdapter):
+ """++paras++ namespace adapter"""
+
+ def traverse(self, name, furtherpath=None):
+ return IParagraphContainer(self.context)
+
+- line 1: the adapter is named "paras"; this is matching the *++paras++* name which was given to our annotation adapter
+- line 2: the adapter is just a simple context adapter, so inheriting from :class:`pyams_utils.adapter.ContextAdapter`
+- lines 5 to 6: the *traverse* method is used to access the adapted content; if a name like "++ns++value" is given
+ to an adapted object, the "value" part is given as *name" argument.
+
+From now, as soon as an URL like "/mycontent/++paras++/" will be used, you will get access to the paragraphs container.
+This is a standard BTree container, so will get access to it's sub-objects by key.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/source/howto-portlet.rst Fri May 18 11:23:53 2018 +0200
@@ -0,0 +1,168 @@
+.. _portlethowto:
+
+
+How to create a new Portlet?
+============================
+
+**Portlets** are pluggable user interface software components that are managed and displayed in a web portal,
+for example an enterprise portal or a web CMS. A portlet can aggregate (integrate) and personalize content from
+different sources within a web page. A portlet responds to requests from a web client and generates dynamic content
+(*reference:* `Wiki portlet`_).
+
+.. _`wiki portlet`: https://en.wikipedia.org/wiki/Portlet
+
+
+**PyAMS Portal** provides the portal engine but only a very small set of predefined portlets that can be used to compose
+and organize the structure of a web page; additional portlets are provided by other packages, like
+:ref:`pyams_content`.
+
+
+1. Define portlet settings
+''''''''''''''''''''''''''
+
+Portlet settings interface are defined in ``interfaces.py``, you can use :py:class:`pyams_portal.interfaces.IPortletSettings`
+or extend the interface by adding additional properties for example:
+
+.. code-block:: python
+
+ from zope.schema import Text
+
+ NEW_PORTLET_NAME = 'new.portlet'
+
+ class INewPortletSettings(IPortletSettings):
+
+ comment = Text(title=_("Comment"),
+ required=True)
+
+
+A :py:class:`pyams_portal.portlet.PortletSettings` persistent subclass then implements what IPortletSettings describes:
+
+.. code-block:: python
+
+ @implementer(INewPortletSettings)
+ class NewPortletSettings(PortletSettings):
+ """Portlet settings"""
+
+ comment = FieldProperty(INewPortletSettings['comment'])
+
+
+2. Create Portlet
+'''''''''''''''''
+
+The Portlet component is a utility, which implements the :py:class:`pyams_portal.interfaces.IPortlet` interface and is
+registered by the :py:func:`pyams_portal.portlet.portlet_config` decorator;
+
+.. code-block:: python
+
+ @portlet_config(permission=VIEW_PERMISSION)
+ class ImagePortlet(Portlet):
+ """Image portlet"""
+
+ name = NEW_PORTLET_NAME
+ label = _("New portlet")
+
+ toolbar_image = None
+ toolbar_css_class = 'fa fa-fw fa-2x fa-picture-o'
+
+ settings_class = NewPortletSettings
+
+
+Where:
+ - **permission**: permission required to display this portlet content
+ - **name**: internal name given to this portlet. This name must be unique between all portlets, so using your own
+ namespace into this name is generally a good option!
+ - **label**: user label given to this portlet
+ - **toolbar_image**: URL of an image used to display portlet button into ZMI toolbar, if any
+ - **toolbar_css_class**: CSS class used to display portlet button into ZMI toolbar, if any
+ - **settings_class**: class used to store portlet settings.
+
+
+3. Create portlet settings edit form
+''''''''''''''''''''''''''''''''''''
+
+Portlet settings have to be updated through management interface via a :py:class:`pyams_portal.zmi.portlet.PortletSettingsEditor`
+subform:
+
+.. code-block:: python
+
+ @pagelet_config(name='properties.html', context=INewPortletSettings, layer=IPyAMSLayer,
+ permission=VIEW_SYSTEM_PERMISSION)
+ class NewPortletSettingsEditor(PortletSettingsEditor):
+ """New portlet settings editor"""
+
+ settings = INewPortletSettings
+
+
+ @adapter_config(name='properties.json', context=(INewPortletSettings, IPyAMSLayer), provides=IPagelet)
+ class NewPortletSettingsAJAXEditor(AJAXEditForm, NewPortletSettingsEditor):
+ """New portlet settings editor, JSON renderer"""
+
+
+4. Previewing portlet content
+'''''''''''''''''''''''''''''
+
+A *previewer* is used into ZMI to display portlet content into portal template definition page:
+
+.. code-block:: python
+
+ @adapter_config(context=(Interface, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletPreviewer)
+ @template_config(template='my-portlet-preview.pt', layer=IPyAMSLayer)
+ class NewPortletPreviewer(PortletPreviewer):
+ """New portlet previewer"""
+
+
+The previewer template is a Chameleon template:
+
+.. code-block:: genshi
+
+ <tal:var define="settings view.settings">
+ <tal:if condition="settings.visible">
+ <div tal:content="settings.comment">Comment</div>
+ </tal:if>
+ <tal:if condition="not settings.visible">
+ <div class="text-center padding-y-5">
+ <span class="fa-stack fa-lg">
+ <i class="fa fa-eye fa-stack-1x"></i>
+ <i class="fa fa-ban fa-stack-2x text-danger"></i>
+ </span>
+ </div>
+ </tal:if>
+ </tal:var>
+
+Here we check if portlet is visible or not to display a small icon when hidden; otherwise we display entered comment.
+
+
+5. Rendering portlet content
+''''''''''''''''''''''''''''
+
+A *renderer* is used to display portlet content into rendered page content:
+
+.. code-block:: python
+
+ @adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletRenderer)
+ @template_config(template='my-portlet-render.pt', layer=IPyAMSLayer)
+ class NewPortletRenderer(PortletRenderer):
+ """New portlet renderer"""
+
+ label = _("Default comment renderer")
+
+
+Each renderer template is also a Chameleon template:
+
+.. code-block:: genshi
+
+ <div class="comment" tal:content="view.settings.comment">Comment</div>
+
+
+This is the configuration of a *default* renderer defined for this portlet; you can provide several renderers for a
+given portlet by given distinct names to the adapters:
+
+.. code-block:: python
+
+ @adapter_config(name='another-renderer',
+ context=(IPortalContext, IPyAMSLayer, Interface, INewPortletSettings), provides=IPortletRenderer)
+ @template_config(template='my-portlet-render-2.pt', layer=IPyAMSLayer)
+ class AnotherNewPortletRenderer(PortletRenderer):
+ """Another new portlet renderer"""
+
+ label = _("Another comment renderer")