.. _internals: Understanding PyAMS internals ============================= Adapters -------- Adapters are important concepts of ZCA and PyAMS framework. If you don't know what are adapters, see :ref:`zca`. 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!! Traversal --------- 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. 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. .. _traverser: PyAMS namespace traverser ------------------------- PyAMS_utils provide a custom URL traverser, defined in package :py:mod:`pyams_utils.traversing`. The :py:class:`NamespaceTraverser ` is a custom traverser based on default Pyramid's *ResourceTreeAdapter*, but it adds the ability to use *namespaces*. Inherited from *Zope3* concept, a namespace is a resource path element starting with the « *++* » characters, like this: .. code-block:: none http://localhost:5432/folder/content/++ns++argument/@@view.html In this sample, *ns* is the namespace name. When the traverser detects a namespace, it looks for several named adapters (or multi-adapters) to the :py:class:`ITraversable ` interface defined in *zope.traversing* package. Adapters lookup with name *ns* is done for the current *context* and *request*, then only for the context and finally for the request, in this order. If a traversing adapter is found, it's :py:func:`traverse` method is called, with the *attr* value as first argument, and the rest of the traversal stack as second one. This is for example how a custom *etc* namespace traverser is defined: .. code-block:: python from pyams_utils.interfaces.site import ISiteRoot from zope.traversing.interfaces import ITraversable from pyams_utils.adapter import adapter_config, ContextAdapter @adapter_config(name='etc', context=ISiteRoot, provides=ITraversable) class SiteRootEtcTraverser(ContextAdapter): """Site root ++etc++ namespace traverser""" def traverse(self, name, furtherpath=None): if name == 'site': return self.context.getSiteManager() raise NotFound By using an URL like '++etc++site' on your site root, you can then get access to your local site manager. *argument* is not mandatory for the namespace traverser. If it is not provided, the *traverse* method is called with an empty string (with is a default adapter name) as first argument. Several PyAMS components use custom traversal adapters. For example, getting thumbnails from an image is done through a traversing adapter, which results in nicer URLs than when using classic URLs with arguments... .. _renderer: Renderers --------- **Renderer** are the layout of the utility data content. A renderer combine un context, a skin and a template to produce the front office html To create new renderer you can override an already exist renderer or create a new one from scratch. Steps below we will create a renderer for a `IContact` paragraph HTML renderer ------------- Chameleon ^^^^^^^^^ To generate html page with dynamic content PyAMS renderer use Chameleon as a template engine Once *pyramid_chameleon* been activated **.pt** templates can be loaded either by looking names that would be found on the Chameleon search path. .. seealso:: https://chameleon.readthedocs.io/en/latest/ https://zope.readthedocs.io/en/latest/zope2book/ZPT.html PyAMS defines a custom expression for TALES called *extension*. This expression are used in the html template (.pt) to ease to display or manipulate content. .. _tales: TALES Extensions ---------------- PyAMS defines a custom expression for TALES called *extension*. When this expression is encountered, the renderer is looking for an :py:class:`ITALESExtension ` multi-adapter for the current *context*, *request* and *view*, for the current *context* and *request*, or only for the current *context*, in this order. If an adapter is found, the renderer call it's :py:func:`render` method with the expression parameters as input parameters. For example, the *metas* extension is an *ITALESExtension* adapter defined into :py:mod:`pyams_skin.metas` module which can be used to include all required headers in a page template. Extension is used like this in the page layout template: .. code-block:: html This extension is defined like this: .. code-block:: python from pyams_skin.interfaces.metas import IHTMLContentMetas from pyams_utils.interfaces.tales import ITALESExtension from pyramid.interfaces import IRequest from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter @adapter_config(name='metas', context=(Interface, IRequest, Interface), provides=ITALESExtension) class MetasTalesExtension(ContextRequestViewAdapter): '''extension:metas TALES extension''' def render(self, context=None): if context is None: context = self.context result = [] for name, adapter in sorted(self.request.registry.getAdapters((context, self.request, self.view), IHTMLContentMetas), key=lambda x: getattr(x[1], 'order', 9999)): result.extend([meta.render() for meta in adapter.get_metas()]) return '\n\t'.join(result) Some TALES extensions can require or accept arguments. For example, the *absolute_url* extension can accept a context and a view name: .. code-block:: html The extension is defined like this: .. code-block:: python from persistent.interfaces import IPersistent from pyams_utils.interfaces.tales import ITALESExtension from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter from pyramid.url import resource_url from zope.interface import Interface @adapter_config(name='absolute_url', context=(IPersistent, Interface, Interface), provides=ITALESExtension) class AbsoluteUrlTalesExtension(ContextRequestViewAdapter): '''extension:absolute_url(context, view_name) TALES extension''' def render(self, context=None, view_name=None): if context is None: context = self.context return resource_url(context, self.request, view_name) .. _talesext: List of PyAMS TALES extensions ------------------------------ Reformat text ^^^^^^^^^^^^^ - br *br*\(value, class) TALES expression This expression can be used to context a given character (‘|’ by default) into HTML breaks with given CSS class. For example: ${tales:br(value, 'hidden-xs')} If value is "Raining| cats and dogs", the output will be "Raining cats and dogs". ---- - html *html*\(value) Replaces line breaks in plain text with appropriate HTML (
). If value is: | "Raining | cats | and | dogs" The output will be "Raining
cats
and
dogs". ---- - truncate *truncate*\(value, length, max) Truncates a string if it is longer than the specified length of characters. Truncated strings will end with ellipsis sequence (“…”). For example: ${tales:truncate(value,9, 0)} If value is "Raining cats and dogs", the output will be "Raining...". ${tales:truncate(value,9, 1)} If value is "Raining cats and dogs", the output will be "Raining cats...". ---- - i18n *i18n*\(value) Return sentence according to the context user’s language, the value is a dict. Utilities ^^^^^^^^^ - oid *oid*\(value) Return the **oid** of the value ---- - cache_key *cache_key*\(value) Return an unique ID based of the component value ---- - timestamp Return incremental number based on the time past in second Media ^^^^^ - picture *picture*\(value, lg_thumb='banner', md_thumb='banner', sm_thumb='banner',xs_thumb='banner', css_class='inner-header__img', alt=alt_title) Search the illustration associated with the value. Return the first illustration found, by order: the component definition, content illustration navigation and content illustration [lg, md, sm, xs]*_thumb - banner - pano - portrait - square [lg, md, sm, xs]*_width [1-12] bootstrap column size css_class add a css class to the container of this illustration () img_class add a css class to the balise alt alternative title ---- - thumbnails *thumbnails*\(value) ---- - thumbnail *thumbnail*\(value, (value, lg_thumb='banner', md_thumb='banner', sm_thumb='banner',xs_thumb='banner', css_class='inner-header__img', alt=alt_title) Search the image associated with the value. [lg, md, sm, xs]*_thumb - banner - pano - portrait - square [lg, md, sm, xs]*_width [1-12] boostrat column size css_class add a css class to the container of this illustration () img_class add a css class to the balise alt alternative title) ---- - conversions *conversion*\(value) Return the list of conversion format supported by the value ---- - audio_type *audio_type*\(value) Return the type of the audio media ---- - video_type* *video_type*\(value) Return the type of the video media Find a resource ^^^^^^^^^^^^^^^ - absolute_url *absolute_url*\(object) Used to get access to an object URL ---- - canonical_url *canonical_url*\(context,request) Used to get access to an uniq object URL, based on current request display context ---- - relative_url *relative_url*\(context, request) Used to get an object's relative URL based on current request display context ---- - *resource_path*\(value) Generates an URL matching a given Fanstatic resource. Resource is given as a string made of package name as value (in dotted form) followed by a colon and by the resource name. ---- - *need_resource*\(value) This extension generates a call to Fanstatic resource.need() function to include given resource into generated HTML code. Resource is given as a string made of package name as value (in dotted form) followed by a colon and by the resource name. For example: .. code-block:: genshi ---- Sequences --------- Internal references ------------------- Versioning ---------- Catalog ------- Views ----- Search engines -------------- .. _utilities: PyAMS Utilities --------------- PyAMS_utils provides a small set of utilities. You can create some of them as global utilities registered in the global components registry; other ones can be created manually by a site administrator and are then registered automatically. Server timezone ^^^^^^^^^^^^^^^ To manage timezones correctly, and display datetimes based on current server timezone, all datetimes should be defined and stored in UTC. PyAMS_utils provides a :py:class:`ServerTimezoneUtility ` which allows you to assign a default timezone to your server. To display a datetime with correct timezone, you can use the :py:func:`tztime ` function, which assign server timezone to the given parameter: .. code-block:: python from datetime import datetime from pyams_utils.timezone import tztime now = datetime.utcnow() my_date = tztime(now) # converts *now* to server timezone We could imagine that datetimes could be displayed with current user timezone. But it's quite impossible to know the user timazone from a server request. The only options are: - you ask an authenticated user to update a timezone setting in his profile - you can include Javascript libraries which will try to detect browser timezone from their computer configuration, and do an AJAX request to update data in their session. That should require an update of :py:func:`tzinfo` adapter to get timezone info from session, request or user profile.