Understanding PyAMS internals

Adapters

Adapters are important concepts of ZCA and PyAMS framework. If you don’t know what are adapters, see Zope Component Architecture with PyAMS.

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:

../_images/annotations-1.png

This example displays several annotations, each using it’s own namespace:

../_images/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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: IBaseParagraph is the base interface for all paragraphs; constraint implies that paragraphs can only be stored in a container implementing IParagraphContainer interface.
  • line 11 to 14: IParagraphContainer is the base interface for paragraphs containers; constraint implies that such a container can only contain objects implementing IBaseParagraph interface.
  • line 17 to 18: IParagraphContainerTarget is only a marker interface which doesn’t provide any method or attribute; it only inherits from 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:

1
2
3
4
5
6
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:

1
2
3
@implementer(IParagraphContainer)
class ParagraphContainer(BTreeOrderedContainer):
    """Paragraphs container"""

The paragraphs container class inherits from a BTreeOrderedContainer and implements IParagraphContainer.

The last operation is to create the adapter, which is the glue between the target class and the paragraphs container:

1
2
3
4
5
6
7
8
9
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 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 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:

1
2
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 pyams_utils.traversing.NamespaceTraverser: when a request traversing subpath is starting with ‘++’ characters, it is looking for a named traverser providing ITraversable interface to the last traversed object.

1
2
3
4
5
6
@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 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 pyams_utils.traversing.NamespaceTraverser: when a request traversing subpath is starting with ‘++’ characters, it is looking for a named traverser providing ITraversable interface to the last traversed object.

1
2
3
4
5
6
@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 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.

PyAMS namespace traverser

PyAMS_utils provide a custom URL traverser, defined in package pyams_utils.traversing.

The 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:

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

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…

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.

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 Extensions

PyAMS defines a custom expression for TALES called extension.

When this expression is encountered, the renderer is looking for an 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 render() method with the expression parameters as input parameters.

For example, the metas extension is an ITALESExtension adapter defined into 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:

<tal:var replace="structure extension:metas" />

This extension is defined like this:

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:

<tal:var define="logo config.logo">
    <img tal:attributes="src extension:absolute_url(logo, '++thumb++200x36.png');" />
</tal:var>

The extension is defined like this:

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)

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 <br class=’hidden-xs’> cats and dogs”.


  • html
    html(value)

Replaces line breaks in plain text with appropriate HTML (<br>).

If value is:

“Raining
cats
and
dogs”

The output will be “Raining<br>cats<br>and<br>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 (<picture>)
img_class
add a css class to the <img> 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 (<picture>)
img_class
add a css class to the <img> 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:

<tal:var define="tales:need_resource('pyams_content.zmi:pyams_content')" />

Sequences

Internal references

Versioning

Catalog

Views

Search engines

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 ServerTimezoneUtility which allows you to assign a default timezone to your server.

To display a datetime with correct timezone, you can use the tztime function, which assign server timezone to the given parameter:

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 tzinfo() adapter to get timezone info from session, request or user profile.