Merge branch 'master' into merge-python3-port
[mediagoblin.git] / mediagoblin / tools / template.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 import six
18
19 import jinja2
20 from jinja2.ext import Extension
21 from jinja2.nodes import Include, Const
22
23 from babel.localedata import exists
24 from werkzeug.urls import url_quote_plus
25
26 from mediagoblin import mg_globals
27 from mediagoblin import messages
28 from mediagoblin import _version
29 from mediagoblin.tools import common
30 from mediagoblin.tools.translate import is_rtl
31 from mediagoblin.tools.translate import set_thread_locale
32 from mediagoblin.tools.translate import get_locale_from_request
33 from mediagoblin.tools.pluginapi import get_hook_templates, hook_transform
34 from mediagoblin.tools.timesince import timesince
35 from mediagoblin.meddleware.csrf import render_csrf_form_token
36
37 SETUP_JINJA_ENVS = {}
38
39
40 def get_jinja_env(template_loader, locale):
41 """
42 Set up the Jinja environment,
43
44 (In the future we may have another system for providing theming;
45 for now this is good enough.)
46 """
47 set_thread_locale(locale)
48
49 # If we have a jinja environment set up with this locale, just
50 # return that one.
51 if locale in SETUP_JINJA_ENVS:
52 return SETUP_JINJA_ENVS[locale]
53
54 # The default config does not require a [jinja2] block.
55 # You may create one if you wish to enable additional jinja2 extensions,
56 # see example in config_spec.ini
57 jinja2_config = mg_globals.global_config.get('jinja2', {})
58 local_exts = jinja2_config.get('extensions', [])
59
60 # jinja2.StrictUndefined will give exceptions on references
61 # to undefined/unknown variables in templates.
62 template_env = jinja2.Environment(
63 loader=template_loader, autoescape=True,
64 undefined=jinja2.StrictUndefined,
65 extensions=[
66 'jinja2.ext.i18n', 'jinja2.ext.autoescape',
67 TemplateHookExtension] + local_exts)
68
69 if six.PY2:
70 template_env.install_gettext_callables(mg_globals.thread_scope.translations.ugettext,
71 mg_globals.thread_scope.translations.ungettext)
72 else:
73 template_env.install_gettext_callables(mg_globals.thread_scope.translations.gettext,
74 mg_globals.thread_scope.translations.ngettext)
75
76 # All templates will know how to ...
77 # ... fetch all waiting messages and remove them from the queue
78 # ... construct a grid of thumbnails or other media
79 # ... have access to the global and app config
80 # ... determine if the language is rtl or ltr
81 template_env.globals['fetch_messages'] = messages.fetch_messages
82 template_env.globals['app_config'] = mg_globals.app_config
83 template_env.globals['global_config'] = mg_globals.global_config
84 template_env.globals['version'] = _version.__version__
85 template_env.globals['auth'] = mg_globals.app.auth
86 template_env.globals['is_rtl'] = is_rtl(locale)
87 template_env.filters['urlencode'] = url_quote_plus
88
89 # add human readable fuzzy date time
90 template_env.globals['timesince'] = timesince
91
92 # allow for hooking up plugin templates
93 template_env.globals['get_hook_templates'] = get_hook_templates
94
95 template_env.globals = hook_transform(
96 'template_global_context', template_env.globals)
97
98 #### THIS IS TEMPORARY, PLEASE FIX IT
99 ## Notifications stuff is not yet a plugin (and we're not sure it will be),
100 ## but it needs to add stuff to the context. This is THE WRONG WAY TO DO IT
101 from mediagoblin import notifications
102 template_env.globals['get_notifications'] = notifications.get_notifications
103 template_env.globals[
104 'get_notification_count'] = notifications.get_notification_count
105 template_env.globals[
106 'get_comment_subscription'] = notifications.get_comment_subscription
107
108 if exists(locale):
109 SETUP_JINJA_ENVS[locale] = template_env
110
111 return template_env
112
113
114 # We'll store context information here when doing unit tests
115 TEMPLATE_TEST_CONTEXT = {}
116
117
118 def render_template(request, template_path, context):
119 """
120 Render a template with context.
121
122 Always inserts the request into the context, so you don't have to.
123 Also stores the context if we're doing unit tests. Helpful!
124 """
125 template = request.template_env.get_template(
126 template_path)
127 context['request'] = request
128 rendered_csrf_token = render_csrf_form_token(request)
129 if rendered_csrf_token is not None:
130 context['csrf_token'] = render_csrf_form_token(request)
131
132 # allow plugins to do things to the context
133 if request.controller_name:
134 context = hook_transform(
135 (request.controller_name, template_path),
136 context)
137
138 # More evil: allow plugins to possibly do something to the context
139 # in every request ever with access to the request and other
140 # variables. Note: this is slower than using
141 # template_global_context
142 context = hook_transform(
143 'template_context_prerender', context)
144
145 rendered = template.render(context)
146
147 if common.TESTS_ENABLED:
148 TEMPLATE_TEST_CONTEXT[template_path] = context
149
150 return rendered
151
152
153 def clear_test_template_context():
154 global TEMPLATE_TEST_CONTEXT
155 TEMPLATE_TEST_CONTEXT = {}
156
157
158 class TemplateHookExtension(Extension):
159 """
160 Easily loop through a bunch of templates from a template hook.
161
162 Use:
163 {% template_hook("comment_extras") %}
164
165 ... will include all templates hooked into the comment_extras section.
166 """
167
168 tags = set(["template_hook"])
169
170 def parse(self, parser):
171 includes = []
172 expr = parser.parse_expression()
173 lineno = expr.lineno
174 hook_name = expr.args[0].value
175
176 for template_name in get_hook_templates(hook_name):
177 includes.append(
178 parser.parse_import_context(
179 Include(Const(template_name), True, False, lineno=lineno),
180 True))
181
182 return includes