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 sys
import os
import warnings
import fnmatch

from zope import component
from zope import interface
from zope.browserresource import directory
from zope.browserresource.interfaces import IResourceFactoryFactory
from zope.pagetemplate import pagetemplate, 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

import martian.util
from grokcore.view import interfaces, util


class ViewSupport(object):
    """Mixin class providing methods and properties generally
    useful for view-ish components.
    """
    @property
    def response(self):
        return self.request.response

    @property
    def body(self):
        return self.request.bodyStream.getCacheStream().read()

    def redirect(self, url, status=None, trusted=False):
        return self.request.response.redirect(
            url, status=status, trusted=trusted)

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

        The data argument is used to form a CGI query string.
        """
        if isinstance(obj, basestring):
            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, data)


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

    def __init__(self, context, request):
        super(View, self).__init__(context, request)
        self.__name__ = getattr(self, '__view_name__', None)

        if getattr(self, 'module_info', None) is not None:
            self.static = component.queryAdapter(
                self.request,
                interface.Interface,
                name=self.module_info.package_dotted_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)

    def default_namespace(self):
        namespace = {}
        namespace['context'] = self.context
        namespace['request'] = self.request
        namespace['static'] = self.static
        namespace['view'] = self
        return namespace

    def namespace(self):
        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

    def update(self, **kwargs):
        pass

    def render(self, **kwargs):
        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

class BaseTemplate(object):
    """Any sort of page template"""

    interface.implements(interfaces.ITemplate)

    __grok_name__ = ''
    __grok_location__ = ''

    def __repr__(self):
        return '<%s template in %s>' % (self.__grok_name__,
                                        self.__grok_location__)

    def _annotateGrokInfo(self, name, location):
        self.__grok_name__ = name
        self.__grok_location__ = location

    def _initFactory(self, factory):
        pass


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 '<%s template in %s>' % (self.__grok_name__,
                                        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


class PageTemplate(GrokTemplate):

    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

    def setFromFilename(self, filename, _prefix=None):
        self._template = TrustedFilePageTemplate(filename, _prefix)

    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)

    def render(self, view):
        namespace = self.getNamespace(view)
        template = self._template
        namespace.update(template.pt_getContext())
        return template.pt_render(namespace)


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