Zine

open source content publishing system


source: zine/application.py @ 1382:0c233d0a90dc

Revision 1382:0c233d0a90dc, 58.3 KB checked in by Christopher Grebs <cg@…>, 21 months ago (diff)

show theme contributors in theme list just as seen in the plugin list

Line 
1# -*- coding: utf-8 -*-
2"""
3    zine.application
4    ~~~~~~~~~~~~~~~~
5
6    This module implements the central application object :class:`Zine`
7    and a couple of helper functions and classes.
8
9
10    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
11    :license: BSD, see LICENSE for more details.
12"""
13import sys
14from os import path, remove, makedirs, walk, environ
15from time import time
16from urlparse import urlparse
17from collections import deque
18from inspect import getdoc
19from traceback import format_exception
20from StringIO import StringIO
21
22from babel import Locale
23
24from jinja2 import Environment, BaseLoader, TemplateNotFound
25
26from sqlalchemy.exceptions import SQLAlchemyError
27
28from werkzeug import Request as RequestBase, Response as ResponseBase, \
29     SharedDataMiddleware, url_quote, routing, redirect as _redirect, \
30     escape, cached_property, url_encode
31from werkzeug.exceptions import HTTPException, Forbidden, \
32     NotFound
33from werkzeug.contrib.securecookie import SecureCookie
34
35from zine import _core
36from zine.environment import SHARED_DATA, BUILTIN_TEMPLATE_PATH, \
37     BUILTIN_PLUGIN_FOLDER
38from zine.database import db, cleanup_session
39from zine.cache import get_cache
40from zine.utils import ClosingIterator, local, local_manager, dump_json, \
41     htmlhelpers
42from zine.utils.datastructures import ReadOnlyMultiMapping
43from zine.utils.exceptions import UserException
44
45
46#: the default theme settings
47DEFAULT_THEME_SETTINGS = {
48    # pagination defaults
49    'pagination.normal':            u'<a href="%(url)s">%(page)d</a>',
50    'pagination.active':            u'<strong>%(page)d</strong>',
51    'pagination.commata':           u'<span class="commata">,\n</span>',
52    'pagination.ellipsis':          u'<span class="ellipsis"> …\n</span>',
53    'pagination.threshold':         3,
54    'pagination.left_threshold':    2,
55    'pagination.right_threshold':   1,
56    'pagination.prev_link':         False,
57    'pagination.next_link':         False,
58    'pagination.gray_prev_link':    True,
59    'pagination.gray_next_link':    True,
60    'pagination.simple':            False,
61
62    # how many posts per page?
63    'author.per_page':              30,
64    'archive.per_page':             None,
65    'category.per_page':            None,
66    'tag.per_page':                 None,
67
68    # datetime formatting settings
69    'date.date_format.default':     'medium',
70    'date.datetime_format.default': 'medium',
71    'date.date_format.short':       None,
72    'date.date_format.medium':      None,
73    'date.date_format.full':        None,
74    'date.date_format.long':        None,
75    'date.datetime_format.short':   None,
76    'date.datetime_format.medium':  None,
77    'date.datetime_format.full':    None,
78    'date.datetime_format.long':    None,
79
80    # query optimizations for overview pages.  Themes can change the
81    # eager/lazy loading settings of some queries to remove unnecessary
82    # overhead that is not in use for what they want to display.  For
83    # example a theme that wants to load a headline-overview of all the
84    # posts in a specific tag but no text at all it makes no sense to
85    # load the text and more just to throw away the information.
86    # for more information have a look at PostQuery.lightweight
87    'sql.index.lazy':               frozenset(['comments']),
88    'sql.author.lazy':              frozenset(['comments']),
89    'sql.archive.lazy':             frozenset(['comments']),
90    'sql.category.lazy':            frozenset(['comments']),
91    'sql.tag.lazy':                 frozenset(['comments']),
92    'sql.index.deferred':           frozenset(),
93    'sql.author.deferred':          frozenset(),
94    'sql.archive.deferred':         frozenset(),
95    'sql.category.deferred':        frozenset(),
96    'sql.tag.deferred':             frozenset()
97}
98
99
100def get_request():
101    """Return the current request.  If no request is available this function
102    returns `None`.
103    """
104    return getattr(local, 'request', None)
105
106
107def get_application():
108    """Get the application instance.  If the application was not yet set up
109    the return value is `None`
110    """
111    return _core._application
112
113
114def url_for(endpoint, **args):
115    """Get the URL to an endpoint.  The keyword arguments provided are used
116    as URL values.  Unknown URL values are used as keyword argument.
117    Additionally there are some special keyword arguments:
118
119    `_anchor`
120        This string is used as URL anchor.
121
122    `_external`
123        If set to `True` the URL will be generated with the full server name
124        and `http://` prefix.
125    """
126    if hasattr(endpoint, 'get_url_values'):
127        rv = endpoint.get_url_values()
128        if rv is not None:
129            if isinstance(rv, basestring):
130                return make_external_url(rv)
131            endpoint, updated_args = rv
132            args.update(updated_args)
133    anchor = args.pop('_anchor', None)
134    external = args.pop('_external', False)
135    rv = get_application().url_adapter.build(endpoint, args,
136                                             force_external=external)
137    if anchor is not None:
138        rv += '#' + url_quote(anchor)
139    return rv
140
141
142def shared_url(spec):
143    """Returns a URL to a shared resource."""
144    endpoint, filename = spec.split('::', 1)
145    return url_for(endpoint + '/shared', filename=filename)
146
147
148def emit_event(event, *args, **kwargs):
149    """Emit a event and return a list of event results.  Each called
150    function contributes one item to the returned list.
151
152    This is equivalent to the following call to :func:`iter_listeners`::
153
154        result = []
155        for listener in iter_listeners(event):
156            result.append(listener(*args, **kwargs))
157    """
158    return [x(*args, **kwargs) for x in
159            get_application()._event_manager.iter(event)]
160
161
162def iter_listeners(event):
163    """Return an iterator for all the listeners for the event provided."""
164    return get_application()._event_manager.iter(event)
165
166
167def add_link(rel, href, type, title=None, charset=None, media=None):
168    """Add a new link to the metadata of the current page being processed."""
169    local.page_metadata.append(('link', {
170        'rel':      rel,
171        'href':     href,
172        'type':     type,
173        'title':    title,
174        'charset':  charset,
175        'media':    media
176    }))
177
178
179def add_meta(http_equiv=None, name=None, content=None):
180    """Add a new meta element to the metadata of the current page."""
181    local.page_metadata.append(('meta', {
182        'http_equiv':   http_equiv,
183        'name':         name,
184        'content':      content
185    }))
186
187
188def add_script(src, type='text/javascript'):
189    """Load a script."""
190    local.page_metadata.append(('script', {
191        'src':      src,
192        'type':     type
193    }))
194
195
196def add_header_snippet(html):
197    """Add some HTML as header snippet."""
198    local.page_metadata.append(('snippet', {
199        'html':     html
200    }))
201
202
203def select_template(templates):
204    """Selects the first template from a list of templates that exists."""
205    env = get_application().template_env
206    for template in templates:
207        if template is not None:
208            try:
209                return env.get_template(template)
210            except TemplateNotFound:
211                pass
212    raise TemplateNotFound('<multiple-choices>')
213
214
215def render_template(template_name, _stream=False, **context):
216    """Renders a template. If `_stream` is ``True`` the return value will be
217    a Jinja template stream and not an unicode object.
218    This is used by `render_response`.  If the `template_name` is a list of
219    strings the first template that exists is selected.
220    """
221    if not isinstance(template_name, basestring):
222        tmpl = select_template(template_name)
223        template_name = tmpl.name
224    else:
225        tmpl = get_application().template_env.get_template(template_name)
226
227    #! called right before a template is rendered, the return value is
228    #! ignored but the context can be modified in place.
229    emit_event('before-render-template', template_name, _stream, context)
230
231    if _stream:
232        return tmpl.stream(context)
233    return tmpl.render(context)
234
235
236def render_response(template_name, **context):
237    """Like render_template but returns a response. If `_stream` is ``True``
238    the response returned uses the Jinja stream processing. This is useful
239    for pages with lazy generated content or huge output where you don't
240    want the users to wait until the calculation ended. Use streaming only
241    in those situations because it's usually slower than bunch processing.
242    """
243    return Response(render_template(template_name, **context))
244
245
246class InternalError(UserException):
247    """Subclasses of this exception are used to signal internal errors that
248    should not happen, but may do if the configuration is garbage.  If an
249    internal error is raised during request handling they are converted into
250    normal server errors for anonymous users (but not logged!!!), but if the
251    current user is an administrator, the error is displayed.
252    """
253
254    help_text = None
255
256
257class Request(RequestBase):
258    """This class holds the incoming request data."""
259
260    def __init__(self, environ, app=None):
261        RequestBase.__init__(self, environ)
262        self.queries = []
263        if app is None:
264            app = get_application()
265        self.app = app
266
267        engine = self.app.database_engine
268
269        # get the session and try to get the user object for this request.
270        from zine.models import User
271        user = None
272        cookie_name = app.cfg['session_cookie_name']
273        session = SecureCookie.load_cookie(self, cookie_name,
274                                           app.secret_key)
275        user_id = session.get('uid')
276        if user_id:
277            user = User.query.options(db.eagerload('groups'),
278                                      db.eagerload('groups', '_privileges')) \
279                             .get(user_id)
280        if user is None:
281            user = User.query.get_nobody()
282        self.user = user
283        self.session = session
284
285    @property
286    def is_behind_proxy(self):
287        """Are we behind a proxy?"""
288        return environ.get('ZINE_BEHIND_PROXY') == '1'
289
290    def login(self, user, permanent=False):
291        """Log the given user in. Can be user_id, username or
292        a full blown user object.
293        """
294        from zine.models import User
295        if isinstance(user, (int, long)):
296            user = User.query.get(user)
297        elif isinstance(user, basestring):
298            user = User.query.filter_by(username=user).first()
299        if user is None:
300            raise RuntimeError('User does not exist')
301        self.user = user
302        #! called after a user was logged in successfully
303        emit_event('after-user-login', user)
304        self.session['uid'] = user.id
305        self.session['lt'] = time()
306        if permanent:
307            self.session['pmt'] = True
308
309    def logout(self):
310        """Log the current user out."""
311        from zine.models import User
312        user = self.user
313        self.user = User.query.get_nobody()
314        self.session.clear()
315        #! called after a user was logged out and the session cleared.
316        emit_event('after-user-logout', user)
317
318
319class Response(ResponseBase):
320    """This class holds the resonse data.  The default charset is utf-8
321    and the default mimetype ``'text/html'``.
322    """
323    default_mimetype = 'text/html'
324
325
326class EventManager(object):
327    """Helper class that handles event listeners and event emitting.
328
329    This is *not* a public interface. Always use the `emit_event` or
330    `iter_listeners` functions to access it or the `connect_event` or
331    `disconnect_event` methods on the application.
332    """
333
334    def __init__(self, app):
335        self.app = app
336        self._listeners = {}
337        self._last_listener = 0
338
339    def connect(self, event, callback, position='after'):
340        """Connect a callback to an event."""
341        assert position in ('before', 'after'), 'invalid position'
342        listener_id = self._last_listener
343        event = intern(event)
344        if event not in self._listeners:
345            self._listeners[event] = deque([callback])
346        elif position == 'after':
347            self._listeners[event].append(callback)
348        elif position == 'before':
349            self._listeners[event].appendleft(callback)
350        self._last_listener += 1
351        return listener_id
352
353    def remove(self, listener_id):
354        """Remove a callback again."""
355        for event in self._listeners:
356            try:
357                event.remove(listener_id)
358            except ValueError:
359                pass
360
361    def iter(self, event):
362        """Return an iterator for all listeners of a given name."""
363        if event not in self._listeners:
364            return iter(())
365        return iter(self._listeners[event])
366
367    def template_emit(self, event, *args, **kwargs):
368        """Emits events for the template context."""
369        results = []
370        for f in self.iter(event):
371            rv = f(*args, **kwargs)
372            if rv is not None:
373                results.append(rv)
374        return TemplateEventResult(results)
375
376
377class TemplateEventResult(list):
378    """A list subclass for results returned by the event listener that
379    concatenates the results if converted to string, otherwise it works
380    exactly like any other list.
381    """
382
383    def __init__(self, items):
384        list.__init__(self, items)
385
386    def __unicode__(self):
387        return u''.join(map(unicode, self))
388
389    def __str__(self):
390        return unicode(self).encode('utf-8')
391
392
393class Theme(object):
394    """Represents a theme and is created automatically by `add_theme`."""
395    app = None
396
397    def __init__(self, name, template_path, metadata=None,
398                 settings=None, configuration_page=None):
399        BaseLoader.__init__(self)
400        self.name = name
401        self.template_path = template_path
402        self.metadata = metadata or {}
403        self._settings = settings or {}
404        self.configuration_page = configuration_page
405
406    @property
407    def configurable(self):
408        return self.configuration_page is not None
409
410    @property
411    def preview_url(self):
412        if self.metadata.get('preview'):
413            return shared_url(self.metadata['preview'])
414
415    @property
416    def has_preview(self):
417        return bool(self.metadata.get('preview'))
418
419    @property
420    def is_current(self):
421        return self.name == self.app.cfg['theme']
422
423    @property
424    def display_name(self):
425        return self.metadata.get('name') or self.name.title()
426
427    @property
428    def description(self):
429        """Return the description of the theme."""
430        return self.metadata.get('description', u'')
431
432    @property
433    def has_author(self):
434        """Does the theme has an author at all?"""
435        return 'author' in self.metadata
436
437    @property
438    def author_info(self):
439        """The author, mail and author URL of the theme."""
440        from zine.utils.mail import split_email
441        return split_email(self.metadata.get('author', u'Nobody')) + \
442               (self.metadata.get('author_url'),)
443
444    @property
445    def html_author_info(self):
446        """Return the author info as html link."""
447        name, email, url = self.author_info
448        if not url:
449            if not email:
450                return escape(name)
451            url = 'mailto:%s' % url_quote(email)
452        return u'<a href="%s">%s</a>' % (
453            escape(url),
454            escape(name)
455        )
456
457    @property
458    def author(self):
459        """Return the author of the plugin."""
460        x = self.author_info
461        return x[0] or x[1]
462
463    @property
464    def author_email(self):
465        """Return the author email address of the theme."""
466        return self.author_info[1]
467
468    @property
469    def author_url(self):
470        """Return the URL of the author of the theme."""
471        return self.author_info[2]
472
473    @property
474    def contributors(self):
475        """The Contributors of the plugin."""
476        from zine.utils.mail import split_email
477        data = self.metadata.get('contributors', '')
478        if not data:
479            return []
480        return [split_email(c.strip()) for c in
481        self.metadata.get('contributors', '').split(',')]
482
483    @property
484    def html_contributors_info(self):
485        from zine.utils.mail import check, is_valid_email
486        result = []
487        for contributor in self.contributors:
488            name, contact = contributor
489            if not contact:
490                result.append(escape(name))
491            else:
492                result.append('<a href="%s">%s</a>' % (
493                    escape(check(is_valid_email, contact) and
494                           'mailto:' + contact or contact),
495                    escape(name)
496                ))
497        return u', '.join(result)
498
499    @cached_property
500    def settings(self):
501        return ReadOnlyMultiMapping(self._settings, DEFAULT_THEME_SETTINGS)
502
503    def get_url_values(self):
504        if self.configurable:
505            return self.name + '/configure', {}
506        raise TypeError('can\'t link to unconfigurable theme')
507
508    def get_source(self, name):
509        parts = [x for x in name.split('/') if not x == '..']
510        for fn in self.get_searchpath():
511            fn = path.join(fn, *parts)
512            if path.exists(fn):
513                f = file(fn)
514                try:
515                    contents = f.read().decode('utf-8')
516                finally:
517                    f.close()
518                mtime = path.getmtime(fn)
519                return contents, fn, lambda: mtime == path.getmtime(fn)
520
521    def get_overlay_path(self, template):
522        """Return the path to an overlay for a template."""
523        return path.join(self.app.instance_folder, 'overlays',
524                         self.name, template)
525
526    def overlay_exists(self, template):
527        """Check if an overlay for a given template exists."""
528        return path.exists(self.get_overlay_path(template))
529
530    def get_overlay(self, template):
531        """Return the source of an overlay."""
532        f = file(self.get_overlay_path(template))
533        try:
534            lines = f.read().decode('utf-8', 'ignore').splitlines()
535        finally:
536            f.close()
537        return u'\n'.join(lines)
538
539    def set_overlay(self, template, data):
540        """Set an overlay."""
541        filename = self.get_overlay_path(template)
542        try:
543            makedirs(path.dirname(filename))
544        except OSError:
545            pass
546        data = u'\n'.join(data.splitlines())
547        if not data.endswith('\n'):
548            data += '\n'
549        f = file(filename, 'w')
550        try:
551            f.write(data.encode('utf-8'))
552        finally:
553            f.close()
554
555    def remove_overlay(self, template, silent=False):
556        """Remove an overlay."""
557        try:
558            remove(self.get_overlay_path(template))
559        except OSError:
560            if not silent:
561                raise
562
563    def get_searchpath(self):
564        """Get the searchpath for this theme including plugins and
565        all other template locations.
566        """
567        # before loading the normal template paths we check for overlays
568        # in the instance overlay folder
569        searchpath = [path.join(self.app.instance_folder, 'overlays',
570                                self.name)]
571
572        # if we have a real theme add the template path to the searchpath
573        # on the highest position
574        if self.name != 'default':
575            searchpath.append(self.template_path)
576
577        # add the template locations of the plugins
578        searchpath.extend(self.app._template_searchpath)
579
580        # now after the plugin searchpaths add the builtin one
581        searchpath.append(BUILTIN_TEMPLATE_PATH)
582
583        return searchpath
584
585    def list_templates(self):
586        """Return a sorted list of all templates."""
587        templates = set()
588        for p in self.get_searchpath():
589            for dirpath, dirnames, filenames in walk(p):
590                dirpath = dirpath[len(p) + 1:]
591                if dirpath.startswith('.'):
592                    continue
593                for filename in filenames:
594                    if filename.startswith('.'):
595                        continue
596                    templates.add(path.join(dirpath, filename).
597                                  replace(path.sep, '/'))
598        return sorted(templates)
599
600    def format_datetime(self, datetime=None, format=None):
601        """Datetime formatting for the template.  the (`datetimeformat`
602        filter)
603        """
604        format = self._get_babel_format('datetime', format)
605        return i18n.format_datetime(datetime, format)
606
607    def format_date(self, date=None, format=None):
608        """Date formatting for the template.  (the `dateformat` filter)"""
609        format = self._get_babel_format('date', format)
610        return i18n.format_date(date, format)
611
612    def _get_babel_format(self, key, format):
613        """A small helper for the datetime formatting functions."""
614        if format is None:
615            format = self.settings['date.%s_format.default' % key]
616        if format in ('short', 'medium', 'full', 'long'):
617            rv = self.settings['date.%s_format.%s' % (key, format)]
618            if rv is not None:
619                format = rv
620        return format
621
622
623class ThemeLoader(BaseLoader):
624    """Forwards theme lookups to the current active theme."""
625
626    def __init__(self, app):
627        BaseLoader.__init__(self)
628        self.app = app
629
630    def get_source(self, environment, name):
631        rv = self.app.theme.get_source(name)
632        if rv is None:
633            raise TemplateNotFound(name)
634        return rv
635
636
637class Zine(object):
638    """The central application object.
639
640    Even though the :class:`Zine` class is a regular Python class, you can't
641    create instances by using the regular constructor.  The only documented way
642    to create this class is the :func:`zine._core.setup` function or by using
643    one of the dispatchers created by :func:`zine._core.get_wsgi_app`.
644    """
645
646    _setup_only = []
647    def setuponly(f, container=_setup_only):
648        """Mark a function as "setup only".  After the setup those
649        functions will be replaced with a dummy function that raises
650        an exception."""
651        container.append(f.__name__)
652        f.__doc__ = (getdoc(f) or '') + '\n\n*This function can only be ' \
653                    'called during application setup*'
654        return f
655
656    def __init__(self, instance_folder):
657        # this check ensures that only setup() can create Zine instances
658        if get_application() is not self:
659            raise TypeError('cannot create %r instances. use the '
660                            'zine._core.setup() factory function.' %
661                            self.__class__.__name__)
662        self.instance_folder = path.abspath(instance_folder)
663        self.upgrade_lockfile = path.join(instance_folder,
664                                          '.upgrade_in_progress')
665
666        # create the event manager, this is the first thing we have to
667        # do because it could happen that events are sent during setup
668        self.initialized = False
669        self._event_manager = EventManager(self)
670
671        # and instanciate the configuration. this won't fail,
672        # even if the database is not connected.
673        from zine.config import Configuration
674        self.cfg = Configuration(path.join(instance_folder, 'zine.ini'))
675        if not self.cfg.exists:
676            raise _core.InstanceNotInitialized()
677
678        # and hook in the logger
679        self.log = log.Logger(path.join(instance_folder, self.cfg['log_file']),
680                              self.cfg['log_level'])
681
682        # the iid of the application
683        self.iid = self.cfg['iid'].encode('utf-8')
684        if not self.iid:
685            self.iid = '%x' % id(self)
686
687        # connect to the database
688        self.database_engine = db.create_engine(self.cfg['database_uri'],
689                                                self.instance_folder,
690                                                self.cfg['database_debug'])
691
692        # now setup the cache system
693        self.cache = get_cache(self)
694
695        # setup core package urls and shared stuff
696        import zine
697        from zine.urls import make_urls
698        from zine.views import all_views, content_type_handlers, \
699             admin_content_type_handlers, absolute_url_handlers
700        from zine.services import all_services
701        from zine.parsers import all_parsers
702        self.views = all_views.copy()
703        self.content_type_handlers = content_type_handlers.copy()
704        self.admin_content_type_handlers = admin_content_type_handlers.copy()
705        self.parsers = dict((k, v(self)) for k, v in all_parsers.iteritems())
706        self.markup_extensions = []
707        self._url_rules = make_urls(self)
708        self._absolute_url_handlers = absolute_url_handlers[:]
709        self._services = all_services.copy()
710        self._shared_exports = {}
711        self._template_globals = {}
712        self._template_filters = {}
713        self._template_tests = {}
714        self._template_searchpath = []
715
716        # initialize i18n/l10n system
717        self.locale = Locale(self.cfg['language'])
718        self.translations = i18n.load_core_translations(self.locale)
719
720        # init themes
721        _ = i18n.gettext
722        default_theme = Theme('default', BUILTIN_TEMPLATE_PATH, {
723            'name':         _(u'Default Theme'),
724            'description':  _(u'Simple default theme that doesn\'t '
725                              'contain any style information.'),
726            'preview':      'core::default_preview.png'
727        })
728        default_theme.app = self
729        self.themes = {'default': default_theme}
730
731        self.apis = {}
732        self.importers = {}
733        self.feed_importer_extensions = []
734
735        # the notification manager
736        from zine.notifications import NotificationManager, \
737             DEFAULT_NOTIFICATION_SYSTEMS, DEFAULT_NOTIFICATION_TYPES
738
739        self.notification_manager = NotificationManager()
740        for system in DEFAULT_NOTIFICATION_SYSTEMS:
741            self.add_notification_system(system)
742        self.notification_types = DEFAULT_NOTIFICATION_TYPES.copy()
743
744        # register the pingback API.
745        from zine import pingback
746        self.add_api('pingback', True, pingback.service)
747        self.pingback_endpoints = pingback.endpoints.copy()
748        self.pingback_url_handlers = pingback.url_handlers[:]
749
750        # register our builtin importers
751        from zine.importers import importers
752        for importer in importers:
753            self.add_importer(importer)
754
755        # and the feed importer extensions
756        from zine.importers.feed import extensions
757        for extension in extensions:
758            self.add_feed_importer_extension(extension)
759
760        # register the default privileges
761        from zine.privileges import DEFAULT_PRIVILEGES, CONTENT_TYPE_PRIVILEGES
762        self.privileges = DEFAULT_PRIVILEGES.copy()
763        self.content_type_privileges = CONTENT_TYPE_PRIVILEGES.copy()
764
765        # insert list of widgets
766        from zine.widgets import all_widgets
767        self.widgets = dict((x.name, x) for x in all_widgets)
768
769        # load plugins
770        from zine.pluginsystem import find_plugins, set_plugin_searchpath
771        self.plugin_folder = path.join(instance_folder, 'plugins')
772        self.plugin_searchpath = [self.plugin_folder]
773        for folder in self.cfg['plugin_searchpath']:
774            folder = folder.strip()
775            if folder:
776                self.plugin_searchpath.append(
777                    path.join(self.instance_folder, folder))
778        self.plugin_searchpath.append(BUILTIN_PLUGIN_FOLDER)
779        set_plugin_searchpath(self.plugin_searchpath)
780
781        # load the plugins
782        self.plugins = {}
783        for plugin in find_plugins(self):
784            if plugin.active:
785                plugin.setup()
786                self.translations.merge(plugin.translations)
787            self.plugins[plugin.name] = plugin
788
789        # set the active theme based on the config.
790        theme = self.cfg['theme']
791        if theme not in self.themes:
792            log.warning(_(u'Theme “%s” is no longer available, falling back '
793                          u'to default theme.') % theme, 'core')
794            theme = 'default'
795            self.cfg.change_single('theme', theme)
796        self.theme = self.themes[theme]
797
798        # init the template system with the core stuff
799        from zine import models
800        env = Environment(loader=ThemeLoader(self),
801                          extensions=['jinja2.ext.i18n'])
802        env.globals.update(
803            cfg=self.cfg,
804            theme=self.theme,
805            h=htmlhelpers,
806            url_for=url_for,
807            shared_url=shared_url,
808            emit_event=self._event_manager.template_emit,
809            request=local('request'),
810            render_widgets=lambda: render_template('_widgets.html'),
811            get_page_metadata=self.get_page_metadata,
812            widgets=self.widgets,
813            zine={
814                'version':      zine.__version__,
815                'copyright':    _(u'Copyright %(years)s by the Zine Team')
816                                % {'years': '2008-2009'}
817            }
818        )
819
820        env.filters.update(
821            json=dump_json,
822            datetimeformat=self.theme.format_datetime,
823            dateformat=self.theme.format_date,
824            monthformat=i18n.format_month,
825            timedeltaformat=i18n.format_timedelta
826        )
827
828        env.install_gettext_translations(self.translations)
829
830        # set up plugin template extensions
831        env.globals.update(self._template_globals)
832        env.filters.update(self._template_filters)
833        env.tests.update(self._template_tests)
834        del self._template_globals, self._template_filters, \
835            self._template_tests
836        self.template_env = env
837
838        # now add the middleware for static file serving
839        self.add_shared_exports('core', SHARED_DATA)
840        self.add_middleware(SharedDataMiddleware, self._shared_exports)
841
842        # set up the urls
843        self.url_map = routing.Map(self._url_rules)
844        del self._url_rules
845
846        # and create a url adapter
847        scheme, netloc, script_name = urlparse(self.cfg['blog_url'])[:3]
848        self.url_adapter = self.url_map.bind(netloc, script_name,
849                                             url_scheme=scheme)
850
851        # mark the app as finished and override the setup functions
852        def _error(*args, **kwargs):
853            raise RuntimeError('Cannot register new callbacks after '
854                               'application setup phase.')
855        self.__dict__.update(dict.fromkeys(self._setup_only, _error))
856
857        self.cfg.config_vars['default_parser'].choices = \
858            self.cfg.config_vars['comment_parser'].choices = \
859            self.list_parsers()
860
861        # register Zine's upgrade repository
862        from zine.upgrades import REPOSITORY_PATH
863        self.register_upgrade_repository('Zine', REPOSITORY_PATH)
864        # allow plugins to register their upgrade repositories
865        emit_event('register-upgrade-repository')
866
867        self.initialized = True
868
869        #! called after the application and all plugins are initialized
870        emit_event('application-setup-done')
871
872    def register_upgrade_repository(self, repo_id, repo_path):
873        """This function is responsible for adding upgrade repositories to the
874        database.
875
876        repo_id can be either a string or a Plugin instance, in which case the
877        plugin name is used as the repository ID.
878        """
879        from zine.models import SchemaVersion
880        from zine.pluginsystem import Plugin
881        from zine.upgrades.customisation import Repository
882        if isinstance(repo_id, Plugin):
883            repo_id = repo_id.metadata.get('name')
884        repo_path = path.abspath(repo_path)
885        try:
886            sv = SchemaVersion.query.filter_by(repository_id=repo_id).first()
887            if not sv:
888                # this always starts with version 0
889                db.session.add(SchemaVersion(Repository(repo_path, repo_id)))
890                db.session.commit()
891        except (SQLAlchemyError, AttributeError):
892            # the schema_versions table does not yet exist, let's create it
893            db.session.rollback()
894            from zine.database import metadata, schema_versions
895            metadata.bind = self.database_engine
896            if not schema_versions.exists():
897                schema_versions.create(self.database_engine)
898            db.session.add(SchemaVersion(Repository(repo_path, repo_id)))
899            db.session.commit()
900
901    def check_if_upgrade_required(self):
902        """Check if all registered schema versions are the latest.
903
904        If an upgrade is required, this will raise
905        zine._core.InstanceUpgradeRequired.
906        """
907        from zine.models import SchemaVersion
908        from zine.upgrades.customisation import Repository
909
910        to_upgrade = []
911
912        for sv in SchemaVersion.query.all():
913            repository = Repository(sv.repository_path, sv.repository_id)
914            try:
915                self.repository_has_upgrade(repository, sv)
916            except _core.InstanceUpgradeRequired:
917                to_upgrade.append(sv.repository_id)
918
919        if to_upgrade:
920            # set Zine in maintenance mode
921            cfg = self.cfg.edit()
922            cfg['maintenance_mode'] = True
923            cfg.commit()
924            raise _core.InstanceUpgradeRequired(to_upgrade)
925
926        # we got here, let's check for a bad upgrade lockfile left behind
927        if path.isfile(self.upgrade_lockfile):
928            remove(self.upgrade_lockfile)
929
930    def repository_has_upgrade(self, repository, schema_version):
931        """Check for available upgrades in one repository."""
932        from zine.models import SchemaVersion
933        try:
934            if schema_version.version < repository.latest:
935                raise _core.InstanceUpgradeRequired()
936        except (SQLAlchemyError, AttributeError):
937            self.log.error('schema_versions table missing while checking '
938                           'for upgrades?')
939            # the schema_versions table does not yet exist, let's create it
940            # XXX can this happen at all?
941            db.session.rollback()
942            from zine.database import metadata, schema_versions
943            metadata.bind = self.database_engine
944            if not schema_versions.exists():
945                schema_versions.create(self.database_engine)
946            db.session.add(SchemaVersion(repository))
947            db.session.commit()
948            raise _core.InstanceUpgradeRequired()
949
950    @property
951    def wants_reload(self):
952        """True if the application requires a reload.  This is `True` if
953        the config was changed on the file system.  A dispatcher checks this
954        value every request and automatically unloads and reloads the
955        application if necessary.
956        """
957        return self.cfg.changed_external
958
959    @property
960    def secret_key(self):
961        """Returns the secret key for the instance (binary!)"""
962        return self.cfg['secret_key'].encode('utf-8')
963
964    @setuponly
965    def add_template_filter(self, name, callback):
966        """Add a Jinja2 template filter."""
967        self._template_filters[name] = callback
968
969    @setuponly
970    def add_template_test(self, name, callback):
971        """Add a Jinja2 template test."""
972        self._template_tests[name] = callback
973
974    @setuponly
975    def add_template_global(self, name, value):
976        """Add a template global.  Object's added that way are available in
977        the global template namespace.
978        """
979        self._template_globals[name] = value
980
981    @setuponly
982    def add_template_searchpath(self, path):
983        """Add a new template searchpath to the application.  This searchpath
984        is queried *after* the themes but *before* the builtin templates are
985        looked up.
986        """
987        self._template_searchpath.append(path)
988
989    @setuponly
990    def add_api(self, name, preferred, callback, blog_id=1, url_key=None):
991        """Add a new API to the blog.  The newly added API is available at
992        ``/_services/<name>`` and automatically exported in the RSD file.
993        The `blog_id` is an unused oddity of the RSD file, preferred an
994        indicator if this API is preferred or not.
995        The callback is called for all requests to the service URL.
996        """
997        endpoint = 'services/' + name
998        self.apis[name] = (blog_id, preferred, endpoint)
999        if url_key is None:
1000            url_key = name.lower()
1001        url = '/_services/' + url_key
1002        self.add_url_rule(url, endpoint=endpoint)
1003        self.add_view(endpoint, callback)
1004        return url
1005
1006    @setuponly
1007    def add_importer(self, importer):
1008        """Register an importer.  For more information about importers
1009        see the :mod:`zine.importers`.
1010        """
1011        importer = importer(self)
1012        endpoint = 'import/' + importer.name
1013        self.importers[importer.name] = importer
1014        self.add_url_rule('/maintenance/import/' + importer.name,
1015                          prefix='admin', endpoint=endpoint)
1016        self.add_view(endpoint, importer)
1017
1018    @setuponly
1019    def add_feed_importer_extension(self, extension):
1020        """Registers a feed importer extension.  This is for example used
1021        for to implement the ZXA importing in the feed importer.
1022
1023        All blogs that provide feeds that extend Atom (and in the future
1024        RSS) should be imported by registering an importer here.
1025        """
1026        self.feed_importer_extensions.append(extension)
1027
1028    @setuponly
1029    def add_pingback_endpoint(self, endpoint, callback):
1030        """Notify the pingback service that the endpoint provided supports
1031        pingbacks.  The second parameter must be the callback function
1032        called on pingbacks.
1033        """
1034        self.pingback_endpoints[endpoint] = callback
1035
1036    @setuponly
1037    def add_pingback_url_handler(self, callback):
1038        """Register a new URL handler as a pingback URL handler.  URL
1039        handlers for pingbacks are looked up after no endpoint could be
1040        found.
1041        """
1042        self.pingback_url_handlers.append(callback)
1043
1044    @setuponly
1045    def add_theme(self, name, template_path=None, metadata=None,
1046                  settings=None, configuration_page=None):
1047        """Add a theme. You have to provide the shortname for the theme
1048        which will be used in the admin panel etc. Then you have to provide
1049        the path for the templates. Usually this path is relative to the
1050        directory of the plugin's `__file__`.
1051
1052        The metadata can be ommited but in that case some information in
1053        the admin panel is not available.
1054
1055        Alternatively a custom :class:`Theme` object can be passed to this
1056        function as only argument.  This makes it possible to register
1057        custom theme subclasses too.
1058        """
1059        if isinstance(name, Theme):
1060            if template_path is not metadata is not settings \
1061               is not configuration_page is not None:
1062                raise TypeError('if a theme instance is provided extra '
1063                                'arguments must be ommited or None.')
1064            theme = name
1065        else:
1066            theme = Theme(name, template_path, metadata,
1067                          settings, configuration_page)
1068        if theme.app is not None:
1069            raise TypeError('theme is already registered to an application.')
1070        theme.app = self
1071        self.themes[theme.name] = theme
1072
1073    @setuponly
1074    def add_shared_exports(self, name, path):
1075        """Add a shared export for name that points to a given path and
1076        creates an url rule for <name>/shared that takes a filename
1077        parameter.  A shared export is some sort of static data from a
1078        plugin.  Per default Zine will shared the data on it's own but
1079        in the future it would be possible to generate an Apache/nginx
1080        config on the fly for the static data.
1081
1082        The static data is available at `/_shared/<name>` and points to
1083        `path` on the file system.  This also generates a URL rule named
1084        `<name>/shared` that accepts a `filename` parameter.  This can be
1085        used for URL generation.
1086        """
1087        self._shared_exports['/_shared/' + name] = path
1088        self.add_url_rule('/_shared/%s/<string:filename>' % name,
1089                          endpoint=name + '/shared', build_only=True)
1090
1091    @setuponly
1092    def add_middleware(self, middleware_factory, *args, **kwargs):
1093        """Add a middleware to the application.  The `middleware_factory`
1094        is a callable that is called with the active WSGI application as
1095        first argument, `args` as extra positional arguments and `kwargs`
1096        as keyword arguments.
1097
1098        The newly applied middleware wraps an internal WSGI application.
1099        """
1100        self.dispatch_wsgi = middleware_factory(self.dispatch_wsgi,
1101                                                   *args, **kwargs)
1102
1103    @setuponly
1104    def add_config_var(self, key, field):
1105        """Add a configuration variable to the application.  The config
1106        variable should be named ``<plugin_name>/<variable_name>``.  The
1107        `variable_name` itself must not contain another slash.  Variables
1108        that are not prefixed are reserved for Zine' internal usage.
1109        The `field` is an instance of a field class from zine.utils.forms
1110        that is used to validate the variable. It has to contain the default
1111        value for that variable.
1112
1113        Example usage::
1114
1115            app.add_config_var('my_plugin/my_var', BooleanField(default=True))
1116        """
1117        if key.count('/') > 1:
1118            raise ValueError('key might not have more than one slash')
1119        self.cfg.config_vars[key] = field
1120
1121    @setuponly
1122    def add_url_rule(self, rule, **kwargs):
1123        """Add a new URL rule to the url map.  This function accepts the same
1124        arguments as a werkzeug routing rule.  Additionally a `prefix`
1125        parameter is accepted that can be used to add the common prefixes
1126        based on the configuration.  Basically the following two calls
1127        do exactly the same::
1128
1129            app.add_url_rule('/foo', prefix='admin', ...)
1130            app.add_url_rule(app.cfg['admin_url_prefix'] + '/foo', ...)
1131
1132        It also takes a `view` keyword argument that, if given registers
1133        a view for the url view::
1134
1135            app.add_url_rule(..., endpoint='bar', view=bar)
1136
1137        is equivalent to::
1138
1139            app.add_url_rule(..., endpoint='bar')
1140            app.add_view('bar', bar)
1141        """
1142        prefix = kwargs.pop('prefix', None)
1143        if prefix is not None:
1144            rule = self.cfg[prefix + '_url_prefix'] + rule
1145        view = kwargs.pop('view', None)
1146        self._url_rules.append(routing.Rule(rule, **kwargs))
1147        if view is not None:
1148            self.views[kwargs['endpoint']] = view
1149
1150    @setuponly
1151    def add_absolute_url(self, handler):
1152        """Adds a new callback as handler for absolute URLs.  If the normal
1153        request handling was unable to find a proper response for the request
1154        the handler is called with the current request as argument and can
1155        return a response that is then used as normal response.
1156
1157        If a handler doesn't want to handle the response it may raise a
1158        `NotFound` exception or return `None`.
1159
1160        This is for example used to implement the pages support in Zine.
1161        """
1162        self._absolute_url_handlers.append(handler)
1163
1164    @setuponly
1165    def add_view(self, endpoint, callback):
1166        """Add a callback as view.  The endpoint is the endpoint for the URL
1167        rule and has to be equivalent to the endpoint passed to
1168        :meth:`add_url_rule`.
1169        """
1170        self.views[endpoint] = callback
1171
1172    @setuponly
1173    def add_content_type(self, content_type, callback, admin_callbacks=None,
1174                         create_privilege=None, edit_own_privilege=None,
1175                         edit_other_privilege=None):
1176        """Register a view handler for a content type."""
1177        self.content_type_handlers[content_type] = callback
1178        if admin_callbacks is not None:
1179            self.admin_content_type_handlers[content_type] = admin_callbacks
1180        self.content_type_privileges[content_type] = (
1181            create_privilege,
1182            edit_own_privilege,
1183            edit_other_privilege
1184        )
1185
1186    @setuponly
1187    def add_parser(self, name, class_):
1188        """Add a new parser class.  This parser has to be a subclass of
1189        :class:`zine.parsers.BaseParser`.
1190        """
1191        self.parsers[name] = class_(self)
1192
1193    @setuponly
1194    def add_markup_extension(self, extension):
1195        """Register a new markup extension."""
1196        self.markup_extensions.append(extension(self))
1197
1198    @setuponly
1199    def add_widget(self, widget):
1200        """Add a widget."""
1201        self.widgets[widget.name] = widget
1202
1203    @setuponly
1204    def add_servicepoint(self, identifier, callback):
1205        """Add a new function as servicepoint.  A service point is a function
1206        that is called by an external non-human interface such as an
1207        JavaScript or XMLRPC client.  It's automatically exposed to all
1208        service interfaces.
1209        """
1210        self._services[identifier] = callback
1211
1212    @setuponly
1213    def add_privilege(self, privilege):
1214        """Registers a new privilege."""
1215        self.privileges[privilege.name] = privilege
1216
1217    @setuponly
1218    def connect_event(self, event, callback, position='after'):
1219        """Connect a callback to an event.  Per default the callback is
1220        appended to the end of the handlers but handlers can ask for a higher
1221        privilege by setting `position` to ``'before'``.
1222
1223        Example usage::
1224
1225            def on_before_metadata_assembled(metadata):
1226                metadata.append('<!-- IM IN UR METADATA -->')
1227
1228            def setup(app):
1229                app.connect_event('before-metadata-assembled',
1230                                  on_before_metadata_assembled)
1231        """
1232        self._event_manager.connect(event, callback, position)
1233
1234    @setuponly
1235    def add_notification_system(self, system):
1236        """Add the notification system to the list of notification systems
1237        the NotificationManager holds.
1238        """
1239        self.notification_manager.systems[system.key] = system(self)
1240
1241    @setuponly
1242    def add_notification_type(self, type):
1243        """Registers a new notification type on the instance."""
1244        self.notification_manager.add_notification_type(type)
1245
1246    def list_parsers(self):
1247        """Return a sorted list of parsers (parser_id, parser_name)."""
1248        # we call unicode to resolve the translations once.  parser.name
1249        # will very likely be a lazy translation
1250        return sorted([(key, unicode(parser.name)) for key, parser in
1251                       self.parsers.iteritems()], key=lambda x: x[1].lower())
1252
1253    def list_privileges(self):
1254        """Return a sorted list of privileges."""
1255        # TODO: somehow add grouping...
1256        result = [(x.name, unicode(x.explanation)) for x in
1257                  self.privileges.values()]
1258        result.sort(key=lambda x: x[0] == 'BLOG_ADMIN' or x[1].lower())
1259        return result
1260
1261    def get_page_metadata(self):
1262        """Return the metadata as HTML part for templates.  This is normally
1263        called by the layout template to get the metadata for the head section.
1264        """
1265        from zine.utils import dump_json
1266        generators = {'script': htmlhelpers.script, 'meta': htmlhelpers.meta,
1267                      'link': htmlhelpers.link, 'snippet': lambda html: html}
1268        result = [
1269            htmlhelpers.meta(name='generator', content='Zine'),
1270            htmlhelpers.link('EditURI', url_for('blog/service_rsd'),
1271                             type='application/rsd+xml', title='RSD'),
1272            htmlhelpers.script(url_for('core/shared', filename='js/jQuery.js')),
1273            htmlhelpers.script(url_for('core/shared', filename='js/Zine.js')),
1274            htmlhelpers.script(url_for('blog/serve_translations'))
1275        ]
1276
1277        # the url information.  Only expose the admin url for admin users
1278        # or calls to this method without a request.
1279        base_url = self.cfg['blog_url'].rstrip('/')
1280        request = get_request()
1281        javascript = [
1282            'Zine.ROOT_URL = %s' % dump_json(base_url),
1283            'Zine.BLOG_URL = %s' % dump_json(base_url +
1284                                             self.cfg['blog_url_prefix'])
1285        ]
1286        if request is None or request.user.is_manager:
1287            javascript.append('Zine.ADMIN_URL = %s' %
1288                              dump_json(base_url +
1289                                        self.cfg['admin_url_prefix']))
1290        result.append(u'<script type="text/javascript">%s;</script>' %
1291                      '; '.join(javascript))
1292
1293        for type, attr in local.page_metadata:
1294            result.append(generators[type](**attr))
1295
1296        #! this is called before the page metadata is assembled with
1297        #! the list of already collected metadata.  You can extend the
1298        #! list in place to add some more html snippets to the page header.
1299        emit_event('before-metadata-assembled', result)
1300        return u'\n'.join(result)
1301
1302    def handle_not_found(self, request, exception):
1303        """Handle a not found exception.  This also dispatches to plugins
1304        that listen for for absolute urls.  See `add_absolute_url` for
1305        details.
1306        """
1307        for handler in self._absolute_url_handlers:
1308            try:
1309                rv = handler(request)
1310                if rv is not None:
1311                    return rv
1312            except NotFound:
1313                # a not found exception has the same effect as returning
1314                # None.  The next handler is processed.  All other http
1315                # exceptions are passed trough.
1316                pass
1317        response = render_response('404.html')
1318        response.status_code = 404
1319        return response
1320
1321    def send_error_notification(self, request, error):
1322        from zine.notifications import send_notification_template, ZINE_ERROR
1323        request_buffer = StringIO()
1324        request_buffer.seek(0)
1325        send_notification_template(
1326            ZINE_ERROR, 'notifications/on_server_error.zeml',
1327            user=request.user, summary=error.message,
1328            request_details=request_buffer.read(),
1329            longtext=''.join(format_exception(*sys.exc_info()))
1330        )
1331
1332    def handle_server_error(self, request, exc_info=None, suppress_log=False):
1333        """Called if a server error happens.  Logs the error and returns a
1334        response with an error message.
1335        """
1336        if not suppress_log:
1337            log.exception('Exception happened at "%s"' % request.path,
1338                          'core', exc_info)
1339        response = render_response('500.html')
1340        response.status_code = 500
1341        return response
1342
1343    def handle_internal_error(self, request, error, suppress_log=True):
1344        """Called if internal errors are caught."""
1345        if request.user.is_admin:
1346            response = render_response('internal_error.html', error=error)
1347            response.status_code = 500
1348            return response
1349        # We got here, meaning no admin has seen this error yet. Notify Them!
1350        self.send_error_notification(request, error)
1351        return self.handle_server_error(request, suppress_log=suppress_log)
1352
1353    def dispatch_request(self, request):
1354        #! the after-request-setup event can return a response
1355        #! or modify the request object in place. If we have a
1356        #! response we just send it, no other modifications are done.
1357        for callback in iter_listeners('after-request-setup'):
1358            result = callback(request)
1359            if result is not None:
1360                return result
1361
1362        # normal request dispatching
1363        try:
1364            try:
1365                endpoint, args = self.url_adapter.match(request.path)
1366                response = self.views[endpoint](request, **args)
1367            except NotFound, e:
1368                response = self.handle_not_found(request, e)
1369            except Forbidden, e:
1370                if request.user.is_somebody:
1371                    response = render_response('403.html')
1372                    response.status_code = 403
1373                else:
1374                    response = _redirect(url_for('account/login',
1375                                                 next=request.path))
1376        except HTTPException, e:
1377            response = e.get_response(request.environ)
1378        except SQLAlchemyError, e:
1379            # Some database screwup?! Don't let Zine stay dispatching 500's
1380            db.session.rollback()
1381            response = self.handle_internal_error(request, e,
1382                                                  suppress_log=False)
1383
1384        # in debug mode on HTML responses we inject the collected queries.
1385        if self.cfg['database_debug'] and \
1386           getattr(response, 'mimetype', None) == 'text/html' and \
1387           isinstance(response.response, (list, tuple)):
1388            from zine.utils.debug import inject_query_info
1389            inject_query_info(request, response)
1390
1391        return response
1392
1393    def dispatch_wsgi(self, environ, start_response):
1394        """This method is the internal WSGI request and is overridden by
1395        middlewares applied with :meth:`add_middleware`.  It handles the
1396        actual request dispatching.
1397        """
1398        # Create a new request object, register it with the application
1399        # and all the other stuff on the current thread but initialize
1400        # it afterwards.  We do this so that the request object can query
1401        # the database in the initialization method.
1402        request = object.__new__(Request)
1403        local.request = request
1404        local.page_metadata = []
1405        local.request_locals = {}
1406        request.__init__(environ, self)
1407
1408        # check if the blog is in maintenance_mode and the user is
1409        # not an administrator. in that case just show a message that
1410        # the user is not privileged to view the blog right now. Exception:
1411        # the page is the login page for the blog.
1412        # XXX: Remove 'admin_prefix' references for Zine 0.3
1413        #      It still exists because some themes might depend on it.
1414        js_translations = url_for('blog/serve_translations')
1415        admin_prefix = self.cfg['admin_url_prefix']
1416        account_prefix = self.cfg['account_url_prefix']
1417        if self.cfg['maintenance_mode'] and \
1418           request.path not in (account_prefix, admin_prefix, js_translations) \
1419           and not (request.path.startswith(admin_prefix + '/') or
1420                    request.path.startswith(account_prefix + '/')):
1421            if not request.user.has_privilege(
1422                                        self.privileges['ENTER_ADMIN_PANEL']):
1423                response = render_response('maintenance.html')
1424                response.status_code = 503
1425                return response(environ, start_response)
1426
1427        # if HTTPS enforcement is active, we redirect to HTTPS if
1428        # possibile without problems (no playload)
1429        if self.cfg['force_https'] and request.method in ('GET', 'HEAD') and \
1430           environ['wsgi.url_scheme'] == 'http':
1431            response = _redirect('https' + request.url[4:], 301)
1432            return response(environ, start_response)
1433
1434        # wrap the real dispatching in a try/except so that we can
1435        # intercept exceptions that happen in the application.
1436        try:
1437            response = self.dispatch_request(request)
1438
1439            # make sure the response object is one of ours
1440            response = Response.force_type(response, environ)
1441
1442            #! allow plugins to change the response object
1443            for callback in iter_listeners('before-response-processed'):
1444                result = callback(response)
1445                if result is not None:
1446                    response = result
1447        except InternalError, e:
1448            response = self.handle_internal_error(request, e)
1449        except:
1450            if self.cfg['passthrough_errors']:
1451                raise
1452            response = self.handle_server_error(request)
1453
1454        # update the session cookie at the request end if the
1455        # session data requires an update.
1456        if request.session.should_save:
1457            # set the secret key explicitly at the end of the request
1458            # to not log out the administrator if he changes the secret
1459            # key in the config editor.
1460            request.session.secret_key = self.secret_key
1461            cookie_name = self.cfg['session_cookie_name']
1462            if request.session.get('pmt'):
1463                max_age = 60 * 60 * 24 * 31
1464                expires = time() + max_age
1465            else:
1466                max_age = expires = None
1467            request.session.save_cookie(response, cookie_name, max_age=max_age,
1468                                        expires=expires, session_expires=expires)
1469
1470        return response(environ, start_response)
1471
1472    def perform_subrequest(self, path, query=None, method='GET', data=None,
1473                           timeout=None, response_wrapper=Response):
1474        """Perform an internal subrequest against Zine.  This method spawns a
1475        separate thread and lets an internal WSGI client answer the request.
1476        The return value is then converted into a zine response object and
1477        returned.
1478
1479        A separate thread is spawned so that the internal request does not
1480        cause troubles for the current one in terms of persistent database
1481        objects.
1482
1483        This is for example used in the `open_url` method to allow access to
1484        blog local resources without dead-locking if the WSGI server does not
1485        support concurrency (single threaded and just one process for example).
1486        """
1487        from werkzeug import Client
1488        from threading import Event, Thread
1489        event = Event()
1490        response = []
1491        input_stream = None
1492        if hasattr(data, 'read'):
1493            input_stream = data
1494            data = None
1495
1496        def make_request():
1497            try:
1498                client = Client(self, response_wrapper)
1499                response.append(client.open(path, self.cfg['blog_url'],
1500                                            method=method, data=data,
1501                                            query_string=url_encode(query),
1502                                            input_stream=input_stream))
1503            except:
1504                response.append(sys.exc_info())
1505            event.set()
1506
1507        Thread(target=make_request).start()
1508        event.wait(timeout)
1509        if not response:
1510            raise NetException('Timeout on internal subrequest')
1511        if isinstance(response[0], tuple):
1512            exc_type, exc_value, tb = response[0]
1513            raise exc_type, exc_value, tb
1514        return response[0]
1515
1516    def __call__(self, environ, start_response):
1517        """Make the application object a WSGI application."""
1518        return ClosingIterator(self.dispatch_wsgi(environ, start_response),
1519                               [local_manager.cleanup, cleanup_session])
1520
1521    def __repr__(self):
1522        return '<Zine %r [%s]>' % (
1523            self.instance_folder,
1524            self.iid
1525        )
1526
1527    # remove our decorator
1528    del setuponly
1529
1530
1531# import here because of circular dependencies
1532from zine import i18n
1533from zine.utils import log
1534from zine.utils.net import NetException
1535from zine.utils.http import make_external_url
Note: See TracBrowser for help on using the repository browser.