# HG changeset patch # User Thierry Florac # Date 1526635433 -7200 # Node ID 7e5e72ddeeb23e4e41a7b29a835fdccc0bf5f18f # Parent e7d62e94392f36b221cac5ae7fa657afbf997879 Added tutorials diff -r e7d62e94392f -r 7e5e72ddeeb2 src/source/appextend.rst --- 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 +=============== - - -
Comment
-
- -
- - - - -
-
-
- -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 - -
Comment
- - -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 diff -r e7d62e94392f -r 7e5e72ddeeb2 src/source/howto-adapter.rst --- /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. diff -r e7d62e94392f -r 7e5e72ddeeb2 src/source/howto-portlet.rst --- /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 + + + +
Comment
+
+ +
+ + + + +
+
+
+ +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 + +
Comment
+ + +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")