Source code for grokcore.view.components

##############################################################################
#
# Copyright (c) 2006-2007 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Grok components"""

import fnmatch
import os
import sys
import warnings

import martian.util
from zope import component
from zope import interface
from zope.browserresource import directory
from zope.browserresource.interfaces import IResourceFactoryFactory
from zope.contentprovider.provider import ContentProviderBase
from zope.pagetemplate import pagetemplate
from zope.pagetemplate import pagetemplatefile
from zope.pagetemplate.engine import TrustedAppPT
from zope.ptresource.ptresource import PageTemplateResourceFactory
from zope.publisher.browser import BrowserPage
from zope.publisher.interfaces import NotFound
from zope.publisher.publish import mapply

from grokcore.view import interfaces
from grokcore.view import util


class ViewSupport:
    """Mixin class providing methods and properties generally
    useful for view-ish components.
    """

    @property
    def response(self):
        """The HTTP Response object that is associated with the request.

        This is also available as self.request.response, but the
        response attribute is provided as a convenience.
        """
        return self.request.response

    @property
    def body(self):
        """The text of the request body.
        """
        return self.request.bodyStream.getCacheStream().read()

    def redirect(self, url, status=None, trusted=False):
        """Redirect to `url`.

        The headers of the :attr:`response` are modified so that the
        calling browser gets a redirect status code. Please note, that
        this method returns before actually sending the response to
        the browser.

        `url` is a string that can contain anything that makes sense
        to a browser. Also relative URIs are allowed.

        `status` is a number representing the HTTP status code sent
        back. If not given or ``None``, ``302`` or ``303`` will be
        sent, depending on the HTTP protocol version in use (HTTP/1.0
        or HTTP/1.1).

        `trusted` is a boolean telling whether we're allowed to
        redirect to 'external' hosts. Normally redirects to other
        hosts than the one the request was sent to are forbidden and
        will raise a :exc:`ValueError`.
        """
        return self.request.response.redirect(
            url, status=status, trusted=trusted)

    def url(self, obj=None, name=None, skin=util.ASIS, data=None):
        """Return string for the URL based on the obj and name.

        If no arguments given, construct URL to view itself.

        If only `obj` argument is given, construct URL to `obj`.

        If only name is given as the first argument, construct URL to
        `context/name`.

        If both object and name arguments are supplied, construct URL
        to `obj/name`.

        Optionally pass a `skin` keyword argument. This should be a
        skin component and the skin's name is taken from this
        component. The effect of this argument is a leading
        ``++skin++[skinname]/`` segment in the path-part of the URL.
        When the argument is not passed, whatever skin is currently set
        on the request will be effective in the URL.

        When passing ``None`` whatever skin is currently effective will
        be removed from the URLs.

        Optionally pass a `data` keyword argument which gets added to
        the URL as a CGI query string.

        """
        if isinstance(obj, str):
            if name is not None:
                raise TypeError(
                    'url() takes either obj argument, obj, string arguments, '
                    'or string argument')
            name = obj
            obj = None

        if name is None and obj is None:
            # create URL to view itself
            obj = self
        elif name is not None and obj is None:
            # create URL to view on context
            obj = self.context

        return util.url(self.request, obj, name, skin, data)


@interface.implementer(interfaces.IGrokView)
class View(ViewSupport, BrowserPage):

    def __init__(self, context, request):
        super().__init__(context, request)
        self.__name__ = getattr(self, '__view_name__', None)
        static_name = getattr(self, '__static_name__', None)
        if static_name is not None:
            self.static = component.queryAdapter(
                self.request,
                interface.Interface,
                name=static_name)
        else:
            self.static = None

    def __call__(self):
        mapply(self.update, (), self.request)
        if self.request.response.getStatus() in (302, 303):
            # A redirect was triggered somewhere in update().  Don't
            # continue rendering the template or doing anything else.
            return

        template = getattr(self, 'template', None)
        if template is not None:
            return self._render_template()
        return mapply(self.render, (), self.request)

    def _render_template(self):
        return self.template.render(self)

[docs] def default_namespace(self): """Returns a dictionary of namespaces that the template implementation expects to always be available. This method is **not** intended to be overridden by application developers. """ namespace = {} namespace['context'] = self.context namespace['request'] = self.request namespace['static'] = self.static namespace['view'] = self return namespace
[docs] def namespace(self): """Returns a dictionary that is injected in the template namespace in addition to the default namespace. This method **is** intended to be overridden by the application developer. """ return {}
def __getitem__(self, key): # This is BBB code for Zope page templates only: if not isinstance(self.template, PageTemplate): raise AttributeError("View has no item %s" % key) value = self.template._template.macros[key] # When this deprecation is done with, this whole __getitem__ can # be removed. warnings.warn("Calling macros directly on the view is deprecated. " "Please use context/@@viewname/macros/macroname\n" "View %r, macro %s" % (self, key), DeprecationWarning, 1) return value
[docs] def update(self, **kwargs): """This method is meant to be implemented by subclasses. It will be called before the view's associated template is rendered and can be used to pre-compute values for the template. update() accepts arbitrary keyword parameters which will be filled in from the request (in that case they **must** be present in the request). """ pass
[docs] def render(self, **kwargs): """A view can either be rendered by an associated template, or it can implement this method to render itself from Python. This is useful if the view's output isn't XML/HTML but something computed in Python (plain text, PDF, etc.) render() can take arbitrary keyword parameters which will be filled in from the request (in that case they *must* be present in the request). """ pass
render.base_method = True # backwards compatibility. Probably not needed by many, but just in case. # please start using grokcore.view.View again. CodeView = View @interface.implementer(interfaces.ITemplate) class BaseTemplate: """Any sort of page template""" __grok_name__ = '' __grok_location__ = '' def __repr__(self): return f'<{self.__grok_name__} template in {self.__grok_location__}>' def _annotateGrokInfo(self, name, location): self.__grok_name__ = name self.__grok_location__ = location def _initFactory(self, factory): pass @interface.implementer(interfaces.IContentProvider) class ContentProvider(ContentProviderBase): template = None def __init__(self, context, request, view): super().__init__(context, request, view) self.context = context self.request = request self.view = view self.__name__ = self.__view_name__ self.static = component.queryAdapter( self.request, interface.Interface, name=self.module_info.package_dotted_name, ) def default_namespace(self): namespace = {} namespace['context'] = self.context namespace['provider'] = self namespace['request'] = self.request namespace['static'] = self.static namespace['view'] = self.view return namespace def namespace(self): return {} def _render_template(self): return self.template.render(self) def render(self, **kwargs): """A content provider can either be rendered by an associated template, or it can implement this method to render itself from Python. This is useful if the view's output isn't XML/HTML but something computed in Python (plain text, PDF, etc.) render() can take arbitrary keyword parameters which will be filled in from the request (in that case they *must* be present in the request). """ return self._render_template() render.base_method = True class GrokTemplate(BaseTemplate): """A slightly more advanced page template This provides most of what a page template needs and is a good base for writing your own page template""" def __init__(self, string=None, filename=None, _prefix=None): # __grok_module__ is needed to make defined_locally() return True for # inline templates # XXX unfortunately using caller_module means that care must be taken # when GrokTemplate is subclassed. You can not do a super().__init__ # for example. self.__grok_module__ = martian.util.caller_module() if not (string is None) ^ (filename is None): raise AssertionError( "You must pass in template or filename, but not both.") if string: self.setFromString(string) else: if _prefix is None: module = sys.modules[self.__grok_module__] _prefix = os.path.dirname(module.__file__) self.setFromFilename(filename, _prefix) def __repr__(self): return f'<{self.__grok_name__} template in {self.__grok_location__}>' def _annotateGrokInfo(self, name, location): self.__grok_name__ = name self.__grok_location__ = location def _initFactory(self, factory): pass def namespace(self, view): # By default use the namespaces that are defined as the # default by the view implementation. return view.default_namespace() def getNamespace(self, view): namespace = self.namespace(view) namespace.update(view.namespace()) return namespace class TrustedPageTemplate(TrustedAppPT, pagetemplate.PageTemplate): pass class TrustedFilePageTemplate(TrustedAppPT, pagetemplatefile.PageTemplateFile): pass
[docs] class PageTemplate(GrokTemplate):
[docs] def setFromString(self, string): zpt = TrustedPageTemplate() if martian.util.not_unicode_or_ascii(string): raise ValueError("Invalid page template. Page templates must be " "unicode or ASCII.") zpt.write(string) self._template = zpt
[docs] def setFromFilename(self, filename, _prefix=None): self._template = TrustedFilePageTemplate(filename, _prefix)
[docs] def _initFactory(self, factory): def _get_macros(self): return self.template._template.macros # _template.macros is a property that does template reloading in debug # mode. A direct "factory.macros = macros" basically caches the # template. So we use a property. factory.macros = property(_get_macros)
[docs] def render(self, view): namespace = self.getNamespace(view) template = self._template namespace.update(template.pt_getContext()) return template.pt_render(namespace)
[docs] class PageTemplateFile(PageTemplate): # For BBB def __init__(self, filename, _prefix=None): self.__grok_module__ = martian.util.caller_module() if _prefix is None: module = sys.modules[self.__grok_module__] _prefix = os.path.dirname(module.__file__) self.setFromFilename(filename, _prefix)
_marker = object() class DirectoryResource(directory.DirectoryResource): forbidden_names = ('.svn', ) def get(self, name, default=_marker): for pat in self.forbidden_names: if fnmatch.fnmatch(name, pat): if default is _marker: raise NotFound(None, name) else: return default path = self.context.path filename = os.path.join(path, name) isfile = os.path.isfile(filename) isdir = os.path.isdir(filename) if not (isfile or isdir): if default is _marker: raise NotFound(None, name) return default if isfile: ext = os.path.splitext(os.path.normcase(name))[1][1:] factory = component.queryUtility(IResourceFactoryFactory, ext, self.default_factory) if factory is PageTemplateResourceFactory: factory = self.default_factory else: factory = self.directory_factory rname = self.__name__ + '/' + name resource = factory(filename, self.context.checker, rname)(self.request) resource.__parent__ = self return resource class DirectoryResourceFactory(directory.DirectoryResourceFactory): # We need this to allow hooking up our own DirectoryResource class. factoryClass = DirectoryResource DirectoryResource.directory_factory = DirectoryResourceFactory