src/source/howto-adapter.rst
branchdoc-dc
changeset 68 de025ff8ba30
parent 58 7e5e72ddeeb2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/source/howto-adapter.rst	Fri May 18 11:33:19 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.