|
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. |