##// END OF EJS Templates
integrations: added branch_head into URL variables. This allow triggering more explicit...
dan -
r2864:9b105d43 stable
parent child Browse files
Show More
@@ -1,311 +1,322 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import string
22 import string
23 import collections
23 import collections
24 import logging
24 import logging
25
25
26 from mako import exceptions
26 from mako import exceptions
27
27
28 from rhodecode.translation import _
28 from rhodecode.translation import _
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class IntegrationTypeBase(object):
34 class IntegrationTypeBase(object):
35 """ Base class for IntegrationType plugins """
35 """ Base class for IntegrationType plugins """
36 is_dummy = False
36 is_dummy = False
37 description = ''
37 description = ''
38
38
39 @classmethod
39 @classmethod
40 def icon(cls):
40 def icon(cls):
41 return '''
41 return '''
42 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
42 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
43 <svg
43 <svg
44 xmlns:dc="http://purl.org/dc/elements/1.1/"
44 xmlns:dc="http://purl.org/dc/elements/1.1/"
45 xmlns:cc="http://creativecommons.org/ns#"
45 xmlns:cc="http://creativecommons.org/ns#"
46 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
46 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
47 xmlns:svg="http://www.w3.org/2000/svg"
47 xmlns:svg="http://www.w3.org/2000/svg"
48 xmlns="http://www.w3.org/2000/svg"
48 xmlns="http://www.w3.org/2000/svg"
49 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
49 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
50 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
50 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
51 viewBox="0 -256 1792 1792"
51 viewBox="0 -256 1792 1792"
52 id="svg3025"
52 id="svg3025"
53 version="1.1"
53 version="1.1"
54 inkscape:version="0.48.3.1 r9886"
54 inkscape:version="0.48.3.1 r9886"
55 width="100%"
55 width="100%"
56 height="100%"
56 height="100%"
57 sodipodi:docname="cog_font_awesome.svg">
57 sodipodi:docname="cog_font_awesome.svg">
58 <metadata
58 <metadata
59 id="metadata3035">
59 id="metadata3035">
60 <rdf:RDF>
60 <rdf:RDF>
61 <cc:Work
61 <cc:Work
62 rdf:about="">
62 rdf:about="">
63 <dc:format>image/svg+xml</dc:format>
63 <dc:format>image/svg+xml</dc:format>
64 <dc:type
64 <dc:type
65 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
65 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
66 </cc:Work>
66 </cc:Work>
67 </rdf:RDF>
67 </rdf:RDF>
68 </metadata>
68 </metadata>
69 <defs
69 <defs
70 id="defs3033" />
70 id="defs3033" />
71 <sodipodi:namedview
71 <sodipodi:namedview
72 pagecolor="#ffffff"
72 pagecolor="#ffffff"
73 bordercolor="#666666"
73 bordercolor="#666666"
74 borderopacity="1"
74 borderopacity="1"
75 objecttolerance="10"
75 objecttolerance="10"
76 gridtolerance="10"
76 gridtolerance="10"
77 guidetolerance="10"
77 guidetolerance="10"
78 inkscape:pageopacity="0"
78 inkscape:pageopacity="0"
79 inkscape:pageshadow="2"
79 inkscape:pageshadow="2"
80 inkscape:window-width="640"
80 inkscape:window-width="640"
81 inkscape:window-height="480"
81 inkscape:window-height="480"
82 id="namedview3031"
82 id="namedview3031"
83 showgrid="false"
83 showgrid="false"
84 inkscape:zoom="0.13169643"
84 inkscape:zoom="0.13169643"
85 inkscape:cx="896"
85 inkscape:cx="896"
86 inkscape:cy="896"
86 inkscape:cy="896"
87 inkscape:window-x="0"
87 inkscape:window-x="0"
88 inkscape:window-y="25"
88 inkscape:window-y="25"
89 inkscape:window-maximized="0"
89 inkscape:window-maximized="0"
90 inkscape:current-layer="svg3025" />
90 inkscape:current-layer="svg3025" />
91 <g
91 <g
92 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
92 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
93 id="g3027">
93 id="g3027">
94 <path
94 <path
95 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
95 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
96 id="path3029"
96 id="path3029"
97 inkscape:connector-curvature="0"
97 inkscape:connector-curvature="0"
98 style="fill:currentColor" />
98 style="fill:currentColor" />
99 </g>
99 </g>
100 </svg>
100 </svg>
101 '''
101 '''
102
102
103 def __init__(self, settings):
103 def __init__(self, settings):
104 """
104 """
105 :param settings: dict of settings to be used for the integration
105 :param settings: dict of settings to be used for the integration
106 """
106 """
107 self.settings = settings
107 self.settings = settings
108
108
109 def settings_schema(self):
109 def settings_schema(self):
110 """
110 """
111 A colander schema of settings for the integration type
111 A colander schema of settings for the integration type
112 """
112 """
113 return colander.Schema()
113 return colander.Schema()
114
114
115
115
116 class EEIntegration(IntegrationTypeBase):
116 class EEIntegration(IntegrationTypeBase):
117 description = 'Integration available in RhodeCode EE edition.'
117 description = 'Integration available in RhodeCode EE edition.'
118 is_dummy = True
118 is_dummy = True
119
119
120 def __init__(self, name, key, settings=None):
120 def __init__(self, name, key, settings=None):
121 self.display_name = name
121 self.display_name = name
122 self.key = key
122 self.key = key
123 super(EEIntegration, self).__init__(settings)
123 super(EEIntegration, self).__init__(settings)
124
124
125
125
126 # Helpers #
126 # Helpers #
127 # updating this required to update the `common_vars` as well.
127 WEBHOOK_URL_VARS = [
128 WEBHOOK_URL_VARS = [
128 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
129 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
129 ('repo_name', 'Full name of the repository'),
130 ('repo_name', 'Full name of the repository'),
130 ('repo_type', 'VCS type of repository'),
131 ('repo_type', 'VCS type of repository'),
131 ('repo_id', 'Unique id of repository'),
132 ('repo_id', 'Unique id of repository'),
132 ('repo_url', 'Repository url'),
133 ('repo_url', 'Repository url'),
133 # extra repo fields
134 # extra repo fields
134 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
135 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
135
136
136 # special attrs below that we handle, using multi-call
137 # special attrs below that we handle, using multi-call
137 ('branch', 'Name of each brach submitted, if any.'),
138 ('branch', 'Name of each branch submitted, if any.'),
138 ('commit_id', 'Id of each commit submitted, if any.'),
139 ('branch_head', 'Head ID of pushed branch (full sha of last commit), if any.'),
140 ('commit_id', 'ID (full sha) of each commit submitted, if any.'),
139
141
140 # pr events vars
142 # pr events vars
141 ('pull_request_id', 'Unique ID of the pull request.'),
143 ('pull_request_id', 'Unique ID of the pull request.'),
142 ('pull_request_title', 'Title of the pull request.'),
144 ('pull_request_title', 'Title of the pull request.'),
143 ('pull_request_url', 'Pull request url.'),
145 ('pull_request_url', 'Pull request url.'),
144 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
146 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
145 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
147 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
146 'Changes after PR update'),
148 'Changes after PR update'),
147
149
148 # user who triggers the call
150 # user who triggers the call
149 ('username', 'User who triggered the call.'),
151 ('username', 'User who triggered the call.'),
150 ('user_id', 'User id who triggered the call.'),
152 ('user_id', 'User id who triggered the call.'),
151 ]
153 ]
152
154
153 # common vars for url template used for CI plugins. Shared with webhook
155 # common vars for url template used for CI plugins. Shared with webhook
154 CI_URL_VARS = WEBHOOK_URL_VARS
156 CI_URL_VARS = WEBHOOK_URL_VARS
155
157
156
158
157 class CommitParsingDataHandler(object):
159 class CommitParsingDataHandler(object):
158
160
159 def aggregate_branch_data(self, branches, commits):
161 def aggregate_branch_data(self, branches, commits):
160 branch_data = collections.OrderedDict()
162 branch_data = collections.OrderedDict()
161 for obj in branches:
163 for obj in branches:
162 branch_data[obj['name']] = obj
164 branch_data[obj['name']] = obj
163
165
164 branches_commits = collections.OrderedDict()
166 branches_commits = collections.OrderedDict()
165 for commit in commits:
167 for commit in commits:
166 if commit.get('git_ref_change'):
168 if commit.get('git_ref_change'):
167 # special case for GIT that allows creating tags,
169 # special case for GIT that allows creating tags,
168 # deleting branches without associated commit
170 # deleting branches without associated commit
169 continue
171 continue
170 commit_branch = commit['branch']
172 commit_branch = commit['branch']
171
173
172 if commit_branch not in branches_commits:
174 if commit_branch not in branches_commits:
173 _branch = branch_data[commit_branch] \
175 _branch = branch_data[commit_branch] \
174 if commit_branch else commit_branch
176 if commit_branch else commit_branch
175 branch_commits = {'branch': _branch,
177 branch_commits = {'branch': _branch,
178 'branch_head': '',
176 'commits': []}
179 'commits': []}
177 branches_commits[commit_branch] = branch_commits
180 branches_commits[commit_branch] = branch_commits
178
181
179 branch_commits = branches_commits[commit_branch]
182 branch_commits = branches_commits[commit_branch]
180 branch_commits['commits'].append(commit)
183 branch_commits['commits'].append(commit)
184 branch_commits['branch_head'] = commit['raw_id']
181 return branches_commits
185 return branches_commits
182
186
183
187
184 class WebhookDataHandler(CommitParsingDataHandler):
188 class WebhookDataHandler(CommitParsingDataHandler):
185 name = 'webhook'
189 name = 'webhook'
186
190
187 def __init__(self, template_url, headers):
191 def __init__(self, template_url, headers):
188 self.template_url = template_url
192 self.template_url = template_url
189 self.headers = headers
193 self.headers = headers
190
194
191 def get_base_parsed_template(self, data):
195 def get_base_parsed_template(self, data):
192 """
196 """
193 initially parses the passed in template with some common variables
197 initially parses the passed in template with some common variables
194 available on ALL calls
198 available on ALL calls
195 """
199 """
196 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
200 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
197 common_vars = {
201 common_vars = {
198 'repo_name': data['repo']['repo_name'],
202 'repo_name': data['repo']['repo_name'],
199 'repo_type': data['repo']['repo_type'],
203 'repo_type': data['repo']['repo_type'],
200 'repo_id': data['repo']['repo_id'],
204 'repo_id': data['repo']['repo_id'],
201 'repo_url': data['repo']['url'],
205 'repo_url': data['repo']['url'],
202 'username': data['actor']['username'],
206 'username': data['actor']['username'],
203 'user_id': data['actor']['user_id'],
207 'user_id': data['actor']['user_id'],
204 'event_name': data['name']
208 'event_name': data['name']
205 }
209 }
206
210
207 extra_vars = {}
211 extra_vars = {}
208 for extra_key, extra_val in data['repo']['extra_fields'].items():
212 for extra_key, extra_val in data['repo']['extra_fields'].items():
209 extra_vars['extra__{}'.format(extra_key)] = extra_val
213 extra_vars['extra__{}'.format(extra_key)] = extra_val
210 common_vars.update(extra_vars)
214 common_vars.update(extra_vars)
211
215
212 template_url = self.template_url.replace('${extra:', '${extra__')
216 template_url = self.template_url.replace('${extra:', '${extra__')
213 return string.Template(template_url).safe_substitute(**common_vars)
217 return string.Template(template_url).safe_substitute(**common_vars)
214
218
215 def repo_push_event_handler(self, event, data):
219 def repo_push_event_handler(self, event, data):
216 url = self.get_base_parsed_template(data)
220 url = self.get_base_parsed_template(data)
217 url_cals = []
221 url_cals = []
218
222
219 branches_commits = self.aggregate_branch_data(
223 branches_commits = self.aggregate_branch_data(
220 data['push']['branches'], data['push']['commits'])
224 data['push']['branches'], data['push']['commits'])
221 if '${branch}' in url:
225 if '${branch}' in url or '${branch_head}' in url:
222 # call it multiple times, for each branch if used in variables
226 # call it multiple times, for each branch if used in variables
223 for branch, commit_ids in branches_commits.items():
227 for branch, commit_ids in branches_commits.items():
224 branch_url = string.Template(url).safe_substitute(branch=branch)
228 branch_url = string.Template(url).safe_substitute(branch=branch)
229
230 if '${branch_head}' in branch_url:
231 # last commit in the aggregate is the head of the branch
232 branch_head = commit_ids['branch_head']
233 branch_url = string.Template(branch_url).safe_substitute(
234 branch_head=branch_head)
235
225 # call further down for each commit if used
236 # call further down for each commit if used
226 if '${commit_id}' in branch_url:
237 if '${commit_id}' in branch_url:
227 for commit_data in commit_ids['commits']:
238 for commit_data in commit_ids['commits']:
228 commit_id = commit_data['raw_id']
239 commit_id = commit_data['raw_id']
229 commit_url = string.Template(branch_url).safe_substitute(
240 commit_url = string.Template(branch_url).safe_substitute(
230 commit_id=commit_id)
241 commit_id=commit_id)
231 # register per-commit call
242 # register per-commit call
232 log.debug(
243 log.debug(
233 'register %s call(%s) to url %s',
244 'register %s call(%s) to url %s',
234 self.name, event, commit_url)
245 self.name, event, commit_url)
235 url_cals.append(
246 url_cals.append(
236 (commit_url, self.headers, data))
247 (commit_url, self.headers, data))
237
248
238 else:
249 else:
239 # register per-branch call
250 # register per-branch call
240 log.debug(
251 log.debug(
241 'register %s call(%s) to url %s',
252 'register %s call(%s) to url %s',
242 self.name, event, branch_url)
253 self.name, event, branch_url)
243 url_cals.append(
254 url_cals.append(
244 (branch_url, self.headers, data))
255 (branch_url, self.headers, data))
245
256
246 else:
257 else:
247 log.debug(
258 log.debug(
248 'register %s call(%s) to url %s', self.name, event, url)
259 'register %s call(%s) to url %s', self.name, event, url)
249 url_cals.append((url, self.headers, data))
260 url_cals.append((url, self.headers, data))
250
261
251 return url_cals
262 return url_cals
252
263
253 def repo_create_event_handler(self, event, data):
264 def repo_create_event_handler(self, event, data):
254 url = self.get_base_parsed_template(data)
265 url = self.get_base_parsed_template(data)
255 log.debug(
266 log.debug(
256 'register %s call(%s) to url %s', self.name, event, url)
267 'register %s call(%s) to url %s', self.name, event, url)
257 return [(url, self.headers, data)]
268 return [(url, self.headers, data)]
258
269
259 def pull_request_event_handler(self, event, data):
270 def pull_request_event_handler(self, event, data):
260 url = self.get_base_parsed_template(data)
271 url = self.get_base_parsed_template(data)
261 log.debug(
272 log.debug(
262 'register %s call(%s) to url %s', self.name, event, url)
273 'register %s call(%s) to url %s', self.name, event, url)
263 url = string.Template(url).safe_substitute(
274 url = string.Template(url).safe_substitute(
264 pull_request_id=data['pullrequest']['pull_request_id'],
275 pull_request_id=data['pullrequest']['pull_request_id'],
265 pull_request_title=data['pullrequest']['title'],
276 pull_request_title=data['pullrequest']['title'],
266 pull_request_url=data['pullrequest']['url'],
277 pull_request_url=data['pullrequest']['url'],
267 pull_request_shadow_url=data['pullrequest']['shadow_url'],
278 pull_request_shadow_url=data['pullrequest']['shadow_url'],
268 pull_request_commits_uid=data['pullrequest']['commits_uid'],
279 pull_request_commits_uid=data['pullrequest']['commits_uid'],
269 )
280 )
270 return [(url, self.headers, data)]
281 return [(url, self.headers, data)]
271
282
272 def __call__(self, event, data):
283 def __call__(self, event, data):
273 from rhodecode import events
284 from rhodecode import events
274
285
275 if isinstance(event, events.RepoPushEvent):
286 if isinstance(event, events.RepoPushEvent):
276 return self.repo_push_event_handler(event, data)
287 return self.repo_push_event_handler(event, data)
277 elif isinstance(event, events.RepoCreateEvent):
288 elif isinstance(event, events.RepoCreateEvent):
278 return self.repo_create_event_handler(event, data)
289 return self.repo_create_event_handler(event, data)
279 elif isinstance(event, events.PullRequestEvent):
290 elif isinstance(event, events.PullRequestEvent):
280 return self.pull_request_event_handler(event, data)
291 return self.pull_request_event_handler(event, data)
281 else:
292 else:
282 raise ValueError(
293 raise ValueError(
283 'event type `%s` not in supported list: %s' % (
294 'event type `%s` not in supported list: %s' % (
284 event.__class__, events))
295 event.__class__, events))
285
296
286
297
287 def get_auth(settings):
298 def get_auth(settings):
288 from requests.auth import HTTPBasicAuth
299 from requests.auth import HTTPBasicAuth
289 username = settings.get('username')
300 username = settings.get('username')
290 password = settings.get('password')
301 password = settings.get('password')
291 if username and password:
302 if username and password:
292 return HTTPBasicAuth(username, password)
303 return HTTPBasicAuth(username, password)
293 return None
304 return None
294
305
295
306
296 def get_web_token(settings):
307 def get_web_token(settings):
297 return settings['secret_token']
308 return settings['secret_token']
298
309
299
310
300 def get_url_vars(url_vars):
311 def get_url_vars(url_vars):
301 return '\n'.join(
312 return '\n'.join(
302 '{} - {}'.format('${' + key + '}', explanation)
313 '{} - {}'.format('${' + key + '}', explanation)
303 for key, explanation in url_vars)
314 for key, explanation in url_vars)
304
315
305
316
306 def render_with_traceback(template, *args, **kwargs):
317 def render_with_traceback(template, *args, **kwargs):
307 try:
318 try:
308 return template.render(*args, **kwargs)
319 return template.render(*args, **kwargs)
309 except Exception:
320 except Exception:
310 log.error(exceptions.text_error_template().render())
321 log.error(exceptions.text_error_template().render())
311 raise
322 raise
General Comments 0
You need to be logged in to leave comments. Login now