• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / grokcore.view / 16248935885

18 Jun 2025 06:52AM UTC coverage: 93.234%. Remained the same
16248935885

push

github

icemac
Back to development: 5.1

151 of 180 branches covered (83.89%)

Branch coverage included in aggregate %.

1475 of 1564 relevant lines covered (94.31%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.09
/src/grokcore/view/components.py
1
##############################################################################
2
#
3
# Copyright (c) 2006-2007 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
"""Grok components"""
15

16
import fnmatch
1✔
17
import os
1✔
18
import sys
1✔
19
import warnings
1✔
20

21
import martian.util
1✔
22
from zope import component
1✔
23
from zope import interface
1✔
24
from zope.browserresource import directory
1✔
25
from zope.browserresource.interfaces import IResourceFactoryFactory
1✔
26
from zope.contentprovider.provider import ContentProviderBase
1✔
27
from zope.pagetemplate import pagetemplate
1✔
28
from zope.pagetemplate import pagetemplatefile
1✔
29
from zope.pagetemplate.engine import TrustedAppPT
1✔
30
from zope.ptresource.ptresource import PageTemplateResourceFactory
1✔
31
from zope.publisher.browser import BrowserPage
1✔
32
from zope.publisher.interfaces import NotFound
1✔
33
from zope.publisher.publish import mapply
1✔
34

35
from grokcore.view import interfaces
1✔
36
from grokcore.view import util
1✔
37

38

39
class ViewSupport:
1✔
40
    """Mixin class providing methods and properties generally
41
    useful for view-ish components.
42
    """
43

44
    @property
1✔
45
    def response(self):
1✔
46
        """The HTTP Response object that is associated with the request.
47

48
        This is also available as self.request.response, but the
49
        response attribute is provided as a convenience.
50
        """
51
        return self.request.response
1✔
52

53
    @property
1✔
54
    def body(self):
1✔
55
        """The text of the request body.
56
        """
57
        return self.request.bodyStream.getCacheStream().read()
×
58

59
    def redirect(self, url, status=None, trusted=False):
1✔
60
        """Redirect to `url`.
61

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

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

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

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

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

86
        If no arguments given, construct URL to view itself.
87

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

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

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

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

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

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

109
        """
110
        if isinstance(obj, str):
1✔
111
            if name is not None:
1✔
112
                raise TypeError(
1✔
113
                    'url() takes either obj argument, obj, string arguments, '
114
                    'or string argument')
115
            name = obj
1✔
116
            obj = None
1✔
117

118
        if name is None and obj is None:
1✔
119
            # create URL to view itself
120
            obj = self
1✔
121
        elif name is not None and obj is None:
1✔
122
            # create URL to view on context
123
            obj = self.context
1✔
124

125
        return util.url(self.request, obj, name, skin, data)
1✔
126

127

128
@interface.implementer(interfaces.IGrokView)
1✔
129
class View(ViewSupport, BrowserPage):
1✔
130

131
    def __init__(self, context, request):
1✔
132
        super().__init__(context, request)
1✔
133
        self.__name__ = getattr(self, '__view_name__', None)
1✔
134
        static_name = getattr(self, '__static_name__', None)
1✔
135
        if static_name is not None:
1✔
136
            self.static = component.queryAdapter(
1✔
137
                self.request,
138
                interface.Interface,
139
                name=static_name)
140
        else:
141
            self.static = None
1✔
142

143
    def __call__(self):
1✔
144
        mapply(self.update, (), self.request)
1✔
145
        if self.request.response.getStatus() in (302, 303):
1✔
146
            # A redirect was triggered somewhere in update().  Don't
147
            # continue rendering the template or doing anything else.
148
            return
1✔
149

150
        template = getattr(self, 'template', None)
1✔
151
        if template is not None:
1✔
152
            return self._render_template()
1✔
153
        return mapply(self.render, (), self.request)
1✔
154

155
    def _render_template(self):
1✔
156
        return self.template.render(self)
1✔
157

158
    def default_namespace(self):
1✔
159
        """Returns a dictionary of namespaces that the template implementation
160
        expects to always be available.
161

162
        This method is **not** intended to be overridden by
163
        application developers.
164
        """
165
        namespace = {}
1✔
166
        namespace['context'] = self.context
1✔
167
        namespace['request'] = self.request
1✔
168
        namespace['static'] = self.static
1✔
169
        namespace['view'] = self
1✔
170
        return namespace
1✔
171

172
    def namespace(self):
1✔
173
        """Returns a dictionary that is injected in the template namespace in
174
        addition to the default namespace.
175

176
        This method **is** intended to be overridden by the application
177
        developer.
178
        """
179
        return {}
1✔
180

181
    def __getitem__(self, key):
1✔
182
        # This is BBB code for Zope page templates only:
183
        if not isinstance(self.template, PageTemplate):
1!
184
            raise AttributeError("View has no item %s" % key)
×
185

186
        value = self.template._template.macros[key]
1✔
187
        # When this deprecation is done with, this whole __getitem__ can
188
        # be removed.
189
        warnings.warn("Calling macros directly on the view is deprecated. "
1✔
190
                      "Please use context/@@viewname/macros/macroname\n"
191
                      "View %r, macro %s" % (self, key),
192
                      DeprecationWarning, 1)
193
        return value
1✔
194

195
    def update(self, **kwargs):
1✔
196
        """This method is meant to be implemented by subclasses. It
197
        will be called before the view's associated template is
198
        rendered and can be used to pre-compute values for the
199
        template.
200

201
        update() accepts arbitrary keyword parameters which will be
202
        filled in from the request (in that case they **must** be
203
        present in the request).
204
        """
205
        pass
1✔
206

207
    def render(self, **kwargs):
1✔
208
        """A view can either be rendered by an associated template, or
209
        it can implement this method to render itself from Python.
210
        This is useful if the view's output isn't XML/HTML but
211
        something computed in Python (plain text, PDF, etc.)
212

213
        render() can take arbitrary keyword parameters which will be
214
        filled in from the request (in that case they *must* be
215
        present in the request).
216
        """
217
        pass
×
218

219
    render.base_method = True
1✔
220

221

222
# backwards compatibility. Probably not needed by many, but just in case.
223
# please start using grokcore.view.View again.
224
CodeView = View
1✔
225

226

227
@interface.implementer(interfaces.ITemplate)
1✔
228
class BaseTemplate:
1✔
229
    """Any sort of page template"""
230

231
    __grok_name__ = ''
1✔
232
    __grok_location__ = ''
1✔
233

234
    def __repr__(self):
1✔
235
        return f'<{self.__grok_name__} template in {self.__grok_location__}>'
×
236

237
    def _annotateGrokInfo(self, name, location):
1✔
238
        self.__grok_name__ = name
×
239
        self.__grok_location__ = location
×
240

241
    def _initFactory(self, factory):
1✔
242
        pass
×
243

244

245
@interface.implementer(interfaces.IContentProvider)
1✔
246
class ContentProvider(ContentProviderBase):
1✔
247

248
    template = None
1✔
249

250
    def __init__(self, context, request, view):
1✔
251
        super().__init__(context, request, view)
1✔
252
        self.context = context
1✔
253
        self.request = request
1✔
254
        self.view = view
1✔
255
        self.__name__ = self.__view_name__
1✔
256
        self.static = component.queryAdapter(
1✔
257
            self.request,
258
            interface.Interface,
259
            name=self.module_info.package_dotted_name,
260
        )
261

262
    def default_namespace(self):
1✔
263
        namespace = {}
1✔
264
        namespace['context'] = self.context
1✔
265
        namespace['provider'] = self
1✔
266
        namespace['request'] = self.request
1✔
267
        namespace['static'] = self.static
1✔
268
        namespace['view'] = self.view
1✔
269
        return namespace
1✔
270

271
    def namespace(self):
1✔
272
        return {}
1✔
273

274
    def _render_template(self):
1✔
275
        return self.template.render(self)
1✔
276

277
    def render(self, **kwargs):
1✔
278
        """A content provider can either be rendered by an associated
279
        template, or it can implement this method to render itself from
280
        Python.  This is useful if the view's output isn't XML/HTML but
281
        something computed in Python (plain text, PDF, etc.)
282

283
        render() can take arbitrary keyword parameters which will be
284
        filled in from the request (in that case they *must* be
285
        present in the request).
286
        """
287
        return self._render_template()
1✔
288

289
    render.base_method = True
1✔
290

291

292
class GrokTemplate(BaseTemplate):
1✔
293
    """A slightly more advanced page template
294

295
    This provides most of what a page template needs and is a good base for
296
    writing your own page template"""
297

298
    def __init__(self, string=None, filename=None, _prefix=None):
1✔
299

300
        # __grok_module__ is needed to make defined_locally() return True for
301
        # inline templates
302
        # XXX unfortunately using caller_module means that care must be taken
303
        # when GrokTemplate is subclassed. You can not do a super().__init__
304
        # for example.
305
        self.__grok_module__ = martian.util.caller_module()
1✔
306

307
        if not (string is None) ^ (filename is None):
1✔
308
            raise AssertionError(
309
                "You must pass in template or filename, but not both.")
310

311
        if string:
1✔
312
            self.setFromString(string)
1✔
313
        else:
314
            if _prefix is None:
1✔
315
                module = sys.modules[self.__grok_module__]
1✔
316
                _prefix = os.path.dirname(module.__file__)
1✔
317
            self.setFromFilename(filename, _prefix)
1✔
318

319
    def __repr__(self):
1✔
320
        return f'<{self.__grok_name__} template in {self.__grok_location__}>'
×
321

322
    def _annotateGrokInfo(self, name, location):
1✔
323
        self.__grok_name__ = name
1✔
324
        self.__grok_location__ = location
1✔
325

326
    def _initFactory(self, factory):
1✔
327
        pass
1✔
328

329
    def namespace(self, view):
1✔
330
        # By default use the namespaces that are defined as the
331
        # default by the view implementation.
332
        return view.default_namespace()
1✔
333

334
    def getNamespace(self, view):
1✔
335
        namespace = self.namespace(view)
1✔
336
        namespace.update(view.namespace())
1✔
337
        return namespace
1✔
338

339

340
class TrustedPageTemplate(TrustedAppPT, pagetemplate.PageTemplate):
1✔
341
    pass
1✔
342

343

344
class TrustedFilePageTemplate(TrustedAppPT, pagetemplatefile.PageTemplateFile):
1✔
345
    pass
1✔
346

347

348
class PageTemplate(GrokTemplate):
1✔
349

350
    def setFromString(self, string):
1✔
351
        zpt = TrustedPageTemplate()
1✔
352
        if martian.util.not_unicode_or_ascii(string):
1!
353
            raise ValueError("Invalid page template. Page templates must be "
×
354
                             "unicode or ASCII.")
355
        zpt.write(string)
1✔
356
        self._template = zpt
1✔
357

358
    def setFromFilename(self, filename, _prefix=None):
1✔
359
        self._template = TrustedFilePageTemplate(filename, _prefix)
1✔
360

361
    def _initFactory(self, factory):
1✔
362

363
        def _get_macros(self):
1✔
364
            return self.template._template.macros
1✔
365
        # _template.macros is a property that does template reloading in debug
366
        # mode.  A direct "factory.macros = macros" basically caches the
367
        # template.  So we use a property.
368
        factory.macros = property(_get_macros)
1✔
369

370
    def render(self, view):
1✔
371
        namespace = self.getNamespace(view)
1✔
372
        template = self._template
1✔
373
        namespace.update(template.pt_getContext())
1✔
374
        return template.pt_render(namespace)
1✔
375

376

377
class PageTemplateFile(PageTemplate):
1✔
378
    # For BBB
379

380
    def __init__(self, filename, _prefix=None):
1✔
381
        self.__grok_module__ = martian.util.caller_module()
×
382
        if _prefix is None:
×
383
            module = sys.modules[self.__grok_module__]
×
384
            _prefix = os.path.dirname(module.__file__)
×
385
        self.setFromFilename(filename, _prefix)
×
386

387

388
_marker = object()
1✔
389

390

391
class DirectoryResource(directory.DirectoryResource):
1✔
392
    forbidden_names = ('.svn', )
1✔
393

394
    def get(self, name, default=_marker):
1✔
395

396
        for pat in self.forbidden_names:
1✔
397
            if fnmatch.fnmatch(name, pat):
1!
398
                if default is _marker:
×
399
                    raise NotFound(None, name)
×
400
                else:
401
                    return default
×
402

403
        path = self.context.path
1✔
404
        filename = os.path.join(path, name)
1✔
405
        isfile = os.path.isfile(filename)
1✔
406
        isdir = os.path.isdir(filename)
1✔
407

408
        if not (isfile or isdir):
1✔
409
            if default is _marker:
1!
410
                raise NotFound(None, name)
1✔
411
            return default
×
412

413
        if isfile:
1✔
414
            ext = os.path.splitext(os.path.normcase(name))[1][1:]
1✔
415
            factory = component.queryUtility(IResourceFactoryFactory, ext,
1✔
416
                                             self.default_factory)
417
            if factory is PageTemplateResourceFactory:
1!
418
                factory = self.default_factory
×
419
        else:
420
            factory = self.directory_factory
1✔
421

422
        rname = self.__name__ + '/' + name
1✔
423
        resource = factory(filename, self.context.checker, rname)(self.request)
1✔
424
        resource.__parent__ = self
1✔
425
        return resource
1✔
426

427

428
class DirectoryResourceFactory(directory.DirectoryResourceFactory):
1✔
429
    # We need this to allow hooking up our own DirectoryResource class.
430
    factoryClass = DirectoryResource
1✔
431

432

433
DirectoryResource.directory_factory = DirectoryResourceFactory
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc