Source code for portal_web.resources

# For copyright and license terms, see COPYRIGHT.rst (top level of repository)
# Repository: https://github.com/C3S/portal_web

"""
Base resources including base, web-/apirootfactory, back-/frontend, debug
"""

import os
import logging
import pprint
from copy import deepcopy
from collections import (
    defaultdict,
    OrderedDict,
)
from collections.abc import Mapping

from pyramid import threadlocal
from pyramid.authorization import (
    Allow,
    Authenticated,
)

from .models import Tdb

log = logging.getLogger(__name__)
pp = pprint.PrettyPrinter(indent=4)


[docs] class PrettyDefaultdict(defaultdict): __repr__ = dict.__repr__
[docs] class ResourceBase: """ Base class for `traversal`_ based resources providing a content registry. Resources are used to form flexible, hierarchical parent-child structures to reflect the url. In the first step of traversal, the URL is mapped to a resource and in the second step the resource is mapped to a pyramid view. Children may be added to resources via the `add_child()` function. The content registry may be used to assign different types of content to a resource, which is provided to pyramid views and the template engine as `context`. Different types might be: - meta (e.g. browser title, keywords, desciption, etc.) - content (e.g. page content, news articles, etc.) - static (e.g. css files, logo images, etc.) - menues (e.g. top navigation, sidebar navigation, etc.) - widgets (dynamic content elements used in different locations) The registry of a parent class is inherited to and may be extended by its children. This allows for content elements to be present on a whole branch of resources, like logos or menues. The registry may be extended by the `extend_registry` decorator function. Note: In later versions the content of the registry might be stored in and retrieved from a database to provide CMS features. Args: request (pyramid.request.Request): Current request. Classattributes: __name__ (str): URL path segment. __parent__ (obj): Instance of parent resource. __children__ (dict): Children resources. __registry__ (dict): Content registry (dictionary or property function returning a dictionary). __acl__ (list): Access control list (list of tupels or property function returning a list of tupels). Attributes: _registry_cache (dict): Cached content registry. request (pyramid.request.Request): Current request. .. _traversal: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/traversal.html """ __name__ = None __parent__ = None __children__ = None __registry__ = {} __acl__ = [] _write = [] _rdbglog = "/shared/tmp/logs/registry.log" def __init__(self, request, context=None): Parent = self.__parent__ if Parent and isinstance(Parent, type): self.__parent__ = Parent(request) self.request = request self.context = context self.readonly = True def __getitem__(self, key): """ Gets the next child resource. Args: key (str): Next URL path segment. Returns: obj: Instance of a child resource, which matches the key. Raises: KeyError: if no child resource is found. """ if self.__children__ and key in self.__children__: return self.__children__[key](self.request) if key in self._write: self.readonly = False raise KeyError(key) def __str__(self): """ Renders the resource as a string. Returns: str: Resource string. """ name = 'None' if hasattr(self.__parent__, '__name__'): name = self.__parent__.__name__ return ( "context: %s.%s\n" "context.__parent__: %s\n" "context.__children__: %s\n" "context.registry:\n%s" ) % ( self.__class__.__module__, self.__class__.__name__, name, pp.pformat(self.__children__), pp.pformat(self.registry), ) @classmethod def _rdbg(cls, caller, original, update, extended): settings = threadlocal.get_current_registry().settings if not settings or 'debug.res.registry' not in settings: return if settings['debug.res.registry'] == 'true': with open(cls._rdbglog, "a") as f: f.write(("-"*3 + " %s.%s() " + "-"*60 + "\n\n" "original: %s\nupdate: %s\nextended: %s\n\n") % ( cls.__name__, caller, pp.pformat(original), pp.pformat(update), pp.pformat(extended))) os.chmod(cls._rdbglog, 775)
[docs] @classmethod def add_child(cls, val): """ Adds a child resource to this resource. Args: val (class): Class of child resource. Returns: None. Examples: >>> ParentResource.add_child(ChildResource) """ val.__parent__ = cls if '__name__' in val.__dict__: if not cls.__children__: cls.__children__ = {} cls.__children__[val.__dict__['__name__']] = val
[docs] @classmethod def merge_registry(cls, orig_dict, new_dict): """ Recursively merges dict-like objects. Note: If a value in new_dict is `{}`, the key is removed in orig_dict Args: orig_dict (dict): Original dictionary to be merged with. new_dict (dict): New dictionary to be merged. Returns: dict: Merged dict. Examples: >>> orig_dict = { ... 'A': { ... 'A1': 'A1', ... 'A2': 'A2' ... }, ... 'B': 'B' ... } >>> new_dict = { ... 'A': { ... 'A2': 'XX' ... }, ... 'B': {}, ... 'C': 'C' ... } >>> print(cls.merge_registry(orig_dict, new_dict)) { 'A': { 'A1': 'A1', 'A2': 'XX' }, 'C': 'C' } """ for key, val in new_dict.items(): # delete key if val == {} if isinstance(val, Mapping) and not val: orig_dict.pop(key, None) # update with OrderedDict if isinstance(val, OrderedDict): r = cls.merge_registry( OrderedDict(orig_dict.get(key, {})), val ) orig_dict[key] = r # update with Mapping elif isinstance(val, Mapping): r = cls.merge_registry(orig_dict.get(key, {}), val) orig_dict[key] = r # update with other objects elif isinstance(orig_dict, Mapping): orig_dict[key] = new_dict[key] else: orig_dict = {key: new_dict[key]} return orig_dict
[docs] @classmethod def extend_registry(cls, func): """ Decorator function to extend the registry. The `__registry__` class attribute might contain a dictionary or a property function returning a dictionary. By extending the registry, the original class attribute is replaced by a property function, which merges the original registry with a new registry returned by `func`. Registries might be extended several times. Args: func (function): Function extending the registry. Returns: None """ _original_registry = cls.__registry__ def _registry_extension(self): if isinstance(_original_registry, property): original = _original_registry.fget(self) else: original = deepcopy(_original_registry) update = func(self) extended = cls.merge_registry(original, update) self._rdbg("extend_registry", _original_registry, update, extended) return extended cls.__registry__ = property(_registry_extension)
@property def registry(self): """ Gets the current registry of the resource. The registry is retrieved by merging the (possibly extended) registry with all (possibly extended) registries of the current resource branch back to the root parent once and is cached. Additional calls will return the cached registry. Returns: dict: Current registry """ if not hasattr(self, '_registry_cache'): if not self.__parent__: parent = {} update = self.__registry__ extended = update else: parent = self.__parent__.registry if isinstance(parent, property): parent = parent.fget(self) update = self.__registry__ extended = self.merge_registry(deepcopy(parent), update) self._registry_cache = extended self._rdbg("registry", parent, update, extended) return self._registry_cache
[docs] def dict(self): """ Returns an autovivid dictionary for easy extension of the registry. Returns: dict: Autovivid dictionary Examples: >>> reg = self.dict() >>> print(reg['create']['key']['path'] = 'onthefly') { 'create': { 'key': { path': 'onthefly' } } } """ return PrettyDefaultdict(self.dict)
# triggered by ContextFound event to load resources after traversal def _context_found(self): if not hasattr(self, 'context_found'): return if not self.readonly: self._context_found_writable() else: self.context_found() # wrapping function needed for writable transaction decorator @Tdb.transaction(readonly=False) def _context_found_writable(self): self.context_found()
[docs] def context_found(self): pass
[docs] class ModelResource(ResourceBase): _write = [] def __init__(self, request, code): self.__parent__ = self.__parent__(request) self.__name__ = code self.readonly = True self.request = request self.code = code # traversal def __getitem__(self, key): # views needing writable transactions if key in self._write: self.readonly = False raise KeyError(key)
[docs] class WebRootFactory(object): """ Root resource factory for web service. Args: request (pyramid.request.Request): Current request. Returns: BackendResource: If user is logged in FrontendResource: If user is not logged in """ def __new__(cls, request): if request.authenticated_userid: return BackendResource(request) return FrontendResource(request)
[docs] class ApiRootFactory(ResourceBase): """ Root resource factory for api service. Args: request (pyramid.request.Request): Current request. Access: No permissions (not needed for api views) """ __name__ = "" __parent__ = None __children__ = {} __acl__ = []
[docs] class FrontendResource(ResourceBase): """ Root resource for users not logged in ("frontend"). Args: request (pyramid.request.Request): Current request. Access: No permissions (not needed for page views) """ __name__ = "" __parent__ = None __children__ = {} __registry__ = { 'meta': {}, 'content': {}, 'static': {}, 'menues': {}, 'widgets': {} } __acl__ = [] _write = ['register', 'verify_email']
[docs] class BackendResource(ResourceBase): """ Root resource for users logged in ("backend"). Args: request (pyramid.request.Request): Current request. Access: Authenticated: read """ __name__ = "" __parent__ = None __children__ = {} __registry__ = { 'meta': {}, 'content': {}, 'static': {}, 'menues': {}, 'widgets': {} } __acl__ = [ (Allow, Authenticated, 'authenticated') ] _write = ['register', 'verify_email']
[docs] class ProfileResource(ResourceBase): """ Profile resource for managing the user profile. Args: request (pyramid.request.Request): Current request. Access: Authenticated: read """ __name__ = "profile" __parent__ = BackendResource __children__ = {} __acl__ = [] _write = ['edit']
[docs] class DebugResource(ResourceBase): """ Root resource for debug views. Note: To be included in the resource tree in development environment only. Args: request (pyramid.request.Request): Current request. Access: No permissions (not needed for debug views) """ __name__ = "debug" __parent__ = BackendResource __registry__ = {} __acl__ = []
[docs] class NewsResource(ResourceBase): """ Example for a news resource for news content provided by the registry. Args: request (pyramid.request.Request): Current request. Access: No permissions """ __name__ = "news" __parent__ = BackendResource __children__ = {} __registry__ = {} __acl__ = [] def __getitem__(self, key): if key in self.registry['content']['news']: article = ArticleResource(self.request, key) self.__children__[key] = article article.__name__ = key return article raise KeyError(key)
[docs] class ArticleResource(ResourceBase): """ Example for an article resource for news content provided by the registry. Args: request (pyramid.request.Request): Current request. id (int): Article id. Access: No permissions """ __name__ = "article" __parent__ = NewsResource __children__ = {} __registry__ = {} __acl__ = [] def __init__(self, request, id): super(ArticleResource, self).__init__(request) self.article = self.registry['content']['news'][id]