summaryrefslogtreecommitdiff
path: root/repoze/bfg/view.py
blob: e6cd11939d5378cdc9d39b5ddddc62168b0f1579 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import inspect

from paste.urlparser import StaticURLParser
from zope.component import queryMultiAdapter
from zope.deprecation import deprecated

from repoze.bfg.interfaces import IView
from repoze.bfg.path import caller_path
from repoze.bfg.security import view_execution_permitted
from repoze.bfg.security import Unauthorized

deprecated('view_execution_permitted',
    "('from repoze.bfg.view import view_execution_permitted' is now "
    "deprecated; instead use 'from repoze.bfg.security import "
    "view_execution_permitted')",
    )

_marker = object()

def render_view_to_response(context, request, name='', secure=True):
    """ Render the view named ``name`` against the specified
    ``context`` and ``request`` to an object implementing
    ``repoze.bfg.interfaces.IResponse`` or ``None`` if no such view
    exists.  This function will return ``None`` if a corresponding
    view cannot be found.  If ``secure`` is ``True``, and the view is
    protected by a permission, the permission will be checked before
    calling the view function.  If the permission check disallows view
    execution (based on the current security policy), a
    ``repoze.bfg.security.Unauthorized`` exception will be raised; its
    ``args`` attribute explains why the view access was disallowed.
    If ``secure`` is ``False``, no permission checking is done."""
    if secure:
        permitted = view_execution_permitted(context, request, name)
        if not permitted:
            raise Unauthorized(permitted)
        
    # It's no use trying to distinguish below whether response is None
    # because a) we were returned a default or b) because the view
    # function returned None: the zope.component/zope.interface
    # machinery doesn't distinguish a None returned from the view from
    # a sentinel None returned during queryMultiAdapter (even if we
    # pass a non-None default).

    return queryMultiAdapter((context, request), IView, name=name)

def render_view_to_iterable(context, request, name='', secure=True):
    """ Render the view named ``name`` against the specified
    ``context`` and ``request``, and return an iterable representing
    the view response's ``app_iter`` (see the interface named
    ``repoze.bfg.interfaces.IResponse``).  This function will return
    ``None`` if a corresponding view cannot be found.  Additionally,
    this function will raise a ``ValueError`` if a view function is
    found and called but the view does not return an object which
    implements ``repoze.bfg.interfaces.IResponse``.  You can usually
    get the string representation of the return value of this function
    by calling ``''.join(iterable)``, or just use ``render_view``
    instead.  If ``secure`` is ``True``, and the view is protected by
    a permission, the permission will be checked before calling the
    view function.  If the permission check disallows view execution
    (based on the current security policy), a
    ``repoze.bfg.security.Unauthorized`` exception will be raised; its
    ``args`` attribute explains why the view access was disallowed.
    If ``secure`` is ``False``, no permission checking is done."""
    response = render_view_to_response(context, request, name, secure)
    if response is None:
        return None
    return response.app_iter

def render_view(context, request, name='', secure=True):
    """ Render the view named ``name`` against the specified
    ``context`` and ``request``, and unwind the the view response's
    ``app_iter`` (see the interface named
    ``repoze.bfg.interfaces.IResponse``) into a single string.  This
    function will return ``None`` if a corresponding view cannot be
    found.  Additionally, this function will raise a ``ValueError`` if
    a view function is found and called but the view does not return
    an object which implements ``repoze.bfg.interfaces.IResponse``.
    If ``secure`` is ``True``, and the view is protected by a
    permission, the permission will be checked before calling the view
    function.  If the permission check disallows view execution (based
    on the current security policy), a
    ``repoze.bfg.security.Unauthorized`` exception will be raised; its
    ``args`` attribute explains why the view access was disallowed.
    If ``secure`` is ``False``, no permission checking is done."""
    iterable = render_view_to_iterable(context, request, name, secure)
    if iterable is None:
        return None
    return ''.join(iterable)

def is_response(ob):
    """ Return True if ``ob`` implements the
    ``repoze.bfg.interfaces.IResponse`` interface, False if not.  Note
    that this isn't actually a true Zope interface check, it's a
    duck-typing check, as response objects are not obligated to
    actually implement a Zope interface."""
    # response objects aren't obligated to implement a Zope interface,
    # so we do it the hard way
    if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
         hasattr(ob, 'status') ):
        if ( hasattr(ob.app_iter, '__iter__') and
             hasattr(ob.headerlist, '__iter__') and
             isinstance(ob.status, basestring) ) :
            return True
    return False

class static(object):
    """ An instance of this class is a callable which can act as a BFG
    view; this view will serve static files from a directory on disk
    based on the ``root_dir`` you provide to its constructor.  The
    directory may contain subdirectories (recursively); the static
    view implementation will descend into these directories as
    necessary based on the components of the URL in order to resolve a
    path into a response.

    You may pass an absolute or relative filesystem path to the
    directory containing static files directory to the constructor as
    the ``root_dir`` argument.  If the path is relative, it will be
    considered relative to the directory in which the Python file
    which calls ``static`` resides.  ``cache_max_age`` influences the
    Expires and Max-Age response headers returned by the view (default
    is 3600 seconds or five minutes).  ``level`` influences how
    relative directories are resolved (the number of hops in the call
    stack), not used very often.
    """
    def __init__(self, root_dir, cache_max_age=3600, level=2):
        root_dir = caller_path(root_dir, level=level)
        self.app = StaticURLParser(root_dir, cache_max_age=cache_max_age)

    def __call__(self, context, request):
        subpath = '/'.join(request.subpath)
        request_copy = request.copy()
        # Fix up PATH_INFO to get rid of everything but the "subpath"
        # (the actual path to the file relative to the root dir).
        request_copy.environ['PATH_INFO'] = '/' + subpath
        # Zero out SCRIPT_NAME for good measure.
        request_copy.environ['SCRIPT_NAME'] = ''
        return request_copy.get_response(self.app)

class bfg_view(object):
    """ Function or class decorator which allows Python code to make
    view registrations instead of using ZCML for the same purpose.

    E.g. in the module ``views.py``::

      from models import IMyModel
      from repoze.bfg.interfaces import IRequest

      @bfg_view(name='my_view', request_type=IRequest, for_=IMyModel,
                permission='read'))
      def my_view(context, request):
          return render_template_to_response('templates/my.pt')

    Equates to the ZCML::

      <bfg:view
       for='.models.IMyModel'
       view='.views.my_view'
       name='my_view'
       permission='read'
       />

    If ``name`` is not supplied, the empty string is used (implying
    the default view).

    If ``request_type`` is not supplied, the interface
    ``repoze.bfg.interfaces.IRequest`` is used.

    If ``for_`` is not supplied, the interface
    ``zope.interface.Interface`` (implying *all* interfaces) is used.

    If ``permission`` is not supplied, no permission is registered for
    this view (it's accessible by any caller).

    Any individual or all parameters can be omitted.  The simplest
    bfg_view declaration then becomes::

        @bfg_view()
        def my_view(...):
            ...

    Such a registration implies that the view name will be
    ``my_view``, registered for models with the
    ``zope.interface.Interface`` interface, using no permission,
    registered against requests which implement the default IRequest
    interface.

    The ``bfg_view`` decorator can also be used as a class decorator
    in Python 2.6 and better (Python 2.5 and below do not support
    class decorators)::

        from webob import Response
        from repoze.bfg.view import bfg_view

        @bfg_view()
        class MyView(object):
            def __init__(self, context, request):
                self.context = context
                self.request = request
            def __call__(self):
                return Response('hello from %s!' % self.context)

    .. warning:: This feature is new in 0.8.1.

    .. note:: When a view is a class, the calling semantics are
              different than when it is a function or another
              non-class callable.  When a view is a class, the class'
              ``__init__`` is called with the context and the request
              parameters, creating an instance.  Subsequently that
              instance's ``__call__`` method is invoked with no
              parameters.  The class' ``__call__`` method must return a
              response.  This provides behavior similar to a Zope
              'browser view' (Zope 'browser views' are typically classes
              instead of simple callables).

    To make use of any bfg_view declaration, you *must* insert the
    following boilerplate into your application registry's ZCML::
    
      <scan package="."/>
    """
    def __init__(self, name='', request_type=None, for_=None, permission=None):
        self.name = name
        self.request_type = request_type
        self.for_ = for_
        self.permission = permission

    def __call__(self, wrapped):
        _bfg_view = wrapped
        if inspect.isclass(_bfg_view):
            # If the object we're decorating is a class, turn it into
            # a function that operates like a Zope view (when it's
            # invoked, construct an instance using 'context' and
            # 'request' as position arguments, then immediately invoke
            # the __call__ method of the instance with no arguments;
            # __call__ should return an IResponse).
            def _bfg_class_view(context, request):
                inst = wrapped(context, request)
                return inst()
            _bfg_class_view.__module__ = wrapped.__module__
            _bfg_class_view.__name__ = wrapped.__name__
            _bfg_class_view.__doc__ = wrapped.__doc__
            _bfg_view = _bfg_class_view
        _bfg_view.__is_bfg_view__ = True
        _bfg_view.__permission__ = self.permission
        _bfg_view.__for__ = self.for_
        _bfg_view.__view_name__ = self.name
        _bfg_view.__request_type__ = self.request_type
        return _bfg_view