src/source/developer_guide/howto-adapter.rst
branchdoc-dc
changeset 109 31b3d00edb8a
parent 58 7e5e72ddeeb2
equal deleted inserted replaced
108:1de74ac0628f 109:31b3d00edb8a
       
     1 .. _adapterhowto:
       
     2 
       
     3 
       
     4 How to define an annotations adapter?
       
     5 =====================================
       
     6 
       
     7 Adapters are important concepts of ZCA and PyAMS framework. If you don't know what are adapters, see :ref:`zca`.
       
     8 
       
     9 
       
    10 What are annotations?
       
    11 +++++++++++++++++++++
       
    12 
       
    13 When an adapter have to add persistent attributes to a persistent class, it can add these attributes directly into
       
    14 persistent instances. But this way can lead to conflicts when several adapters want to use the same attribute name for
       
    15 different kinds of information.
       
    16 
       
    17 Annotations are an elegant way to handle this use case: they are based on a BTree which is stored into a
       
    18 specific instance attribute (*__annotations__*). Any adapter can then use this dictionary to store it's own
       
    19 informations, using it's own namespace as dictionary key.
       
    20 
       
    21 ZODB browser allows you to display existing annotations:
       
    22 
       
    23 .. image:: _static/annotations-1.png
       
    24 
       
    25 This example displays several annotations, each using it's own namespace:
       
    26 
       
    27 .. image:: _static/annotations-2.png
       
    28 
       
    29 
       
    30 Designing interfaces
       
    31 ++++++++++++++++++++
       
    32 
       
    33 The first step with ZCA is to design your interfaces.
       
    34 
       
    35 The are going to base our example on PyAMS_content 'paragraphs' component: a content class is marked as a
       
    36 *paragraphs container target*, a class that can store paragraphs. But the real storage of paragraphs is done by
       
    37 another *container* class:
       
    38 
       
    39 .. code-block:: python
       
    40     :linenos:
       
    41 
       
    42     from zope.annotation.interfaces import IAttributeAnnotatable
       
    43     from zope.containers.constraints import containers, contains
       
    44 
       
    45 
       
    46     class IBaseParagraph(Interface):
       
    47         """Base paragraph interface"""
       
    48 
       
    49         containers('.IParagraphContainer')
       
    50 
       
    51 
       
    52     class IParagraphContainer(IOrderedContainer):
       
    53         """Paragraphs container"""
       
    54 
       
    55         contains(IBaseParagraph)
       
    56 
       
    57 
       
    58     class IParagraphContainerTarget(IAttributeAnnotatable):
       
    59         """Paragraphs container marker interface"""
       
    60 
       
    61 
       
    62     PARAGRAPH_CONTAINER_KEY = 'pyams_content.paragraph'
       
    63 
       
    64 
       
    65 - line 5 to 8: :class:`IBaseParagraph` is the base interface for all paragraphs; constraint implies that paragraphs
       
    66   can only be stored in a container implementing :class:`IParagraphContainer` interface.
       
    67 - line 11 to 14: :class:`IParagraphContainer` is the base interface for paragraphs containers; constraint implies that
       
    68   such a container can only contain objects implementing :class:`IBaseParagraph` interface.
       
    69 - line 17 to 18: :class:`IParagraphContainerTarget` is only a *marker* interface which doesn't provide any method or
       
    70   attribute; it only inherits from :class:`IAttributeAnnotatable`, which implies that classes implementing this
       
    71   interface allows other classes to add informations as annotations through a dedicated *__annotations__* attribute.
       
    72 - line 21: this is the key which will be used to store our annotation.
       
    73 
       
    74 
       
    75 Creating persistent classes
       
    76 +++++++++++++++++++++++++++
       
    77 
       
    78 The first step is to declare that a given content class can store paragraphs:
       
    79 
       
    80 .. code-block:: python
       
    81     :linenos:
       
    82 
       
    83     from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget
       
    84     from zope.interface import implementer
       
    85 
       
    86     @implementer(IParagraphContainerTarget)
       
    87     class WfNewsEvent(WfSharedContent):
       
    88         """News event class"""
       
    89 
       
    90 Here we just say "Well, I'm a shared content, and I'm OK to store paragraphs!".
       
    91 
       
    92 So we can design the paragraphs container class. It's this class which will *really* store the paragraphs:
       
    93 
       
    94 .. code-block:: python
       
    95     :linenos:
       
    96 
       
    97     @implementer(IParagraphContainer)
       
    98     class ParagraphContainer(BTreeOrderedContainer):
       
    99         """Paragraphs container"""
       
   100 
       
   101 The paragraphs container class inherits from a :class:`BTreeOrderedContainer` and implements
       
   102 :class:`IParagraphContainer`.
       
   103 
       
   104 The last operation is to create the adapter, which is the *glue* between the *target* class and the paragraphs
       
   105 container:
       
   106 
       
   107 .. code-block:: python
       
   108     :linenos:
       
   109 
       
   110     from pyams_utils.adapter import adapter_config, get_annotation_adapter
       
   111 
       
   112     @adapter_config(context=IParagraphContainerTarget, provides=IParagraphContainer)
       
   113     def paragraph_container_factory(target):
       
   114         """Paragraphs container factory"""
       
   115         return get_annotation_adapter(target,
       
   116                                       PARAGRAPH_CONTAINER_KEY,
       
   117                                       ParagraphContainer,
       
   118                                       name='++paras++')
       
   119 
       
   120 PyAMS provides a shortcut to create an annotation adapter in :func:`pyams_utils.adapter.get_annotation_adapter`.
       
   121 It's mandatory arguments are:
       
   122 
       
   123 - **context** (line 6): the context to which the adapter is applied
       
   124 - **key** (line 7): the string key used to access and store context's annotations
       
   125 - **factory** (line 8): if the requested annotation is missing, a new one is created using this factory (which can be a class or
       
   126   a function)
       
   127 
       
   128 Optional arguments are:
       
   129 
       
   130 - **markers** (None by default): if set, should be a list of marker interfaces which will be assigned to object
       
   131   created by the factory
       
   132 - **notify**: if *True* (default), an :class:`ObjectCreatedEvent` event is notified on object creation
       
   133 - **locate**: if *True* (default), context is set as *parent* of created object
       
   134 - **parent**: if *locate* is True and if *parent* is set, this is the object to which the new object should be *parented*
       
   135   instead of initial context
       
   136 - **name** (None by default): some objects need to be traversed, especially when you have to be able to access them through an URL; this
       
   137   is the name given to created object.
       
   138 
       
   139 
       
   140 Using your adapter
       
   141 ++++++++++++++++++
       
   142 
       
   143 Starting from your *content* object, it's then very simple to access to the paragraphs container:
       
   144 
       
   145 .. code-block:: python
       
   146     :linenos:
       
   147 
       
   148     event = WfNewsEvent()
       
   149     paragraphs_container = IParagraphContainer(event, None)
       
   150 
       
   151 And that's it! From now I can get access to all paragraphs associated with my initial content!!
       
   152 
       
   153 
       
   154 Managing traversal
       
   155 ++++++++++++++++++
       
   156 
       
   157 As said before, sometimes you have to be able to *traverse* from an initial content to a given sub-content
       
   158 managed by an adapter.
       
   159 
       
   160 PyAMS defines a custom :class:`pyams_utils.traversing.NamespaceTraverser`: when a request traversing subpath is
       
   161 starting with '++' characters, it is looking for a named traverser providing :class:`ITraversable` interface
       
   162 to the last traversed object.
       
   163 
       
   164 .. code-block:: python
       
   165     :linenos:
       
   166 
       
   167     @adapter_config(name='paras', context=IParagraphContainerTarget, provides=ITraversable)
       
   168     class ParagraphContainerNamespace(ContextAdapter):
       
   169         """++paras++ namespace adapter"""
       
   170 
       
   171         def traverse(self, name, furtherpath=None):
       
   172             return IParagraphContainer(self.context)
       
   173 
       
   174 - line 1: the adapter is named "paras"; this is matching the *++paras++* name which was given to our annotation adapter
       
   175 - line 2: the adapter is just a simple context adapter, so inheriting from :class:`pyams_utils.adapter.ContextAdapter`
       
   176 - lines 5 to 6: the *traverse* method is used to access the adapted content; if a name like "++ns++value" is given
       
   177   to an adapted object, the "value" part is given as *name" argument.
       
   178 
       
   179 From now, as soon as an URL like "/mycontent/++paras++/" will be used, you will get access to the paragraphs container.
       
   180 This is a standard BTree container, so will get access to it's sub-objects by key.