##// END OF EJS Templates
slack: fix wrong named function
dan -
r417:cd14395c default
parent child Browse files
Show More
@@ -1,388 +1,389 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 """
21 """
22 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25
25
26 from paste.registry import RegistryManager
26 from paste.registry import RegistryManager
27 from paste.gzipper import make_gzip_middleware
27 from paste.gzipper import make_gzip_middleware
28 from pylons.wsgiapp import PylonsApp
28 from pylons.wsgiapp import PylonsApp
29 from pyramid.authorization import ACLAuthorizationPolicy
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
30 from pyramid.config import Configurator
31 from pyramid.static import static_view
31 from pyramid.static import static_view
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
35 import pyramid.httpexceptions as httpexceptions
35 import pyramid.httpexceptions as httpexceptions
36 from pyramid.renderers import render_to_response, render
36 from pyramid.renderers import render_to_response, render
37 from routes.middleware import RoutesMiddleware
37 from routes.middleware import RoutesMiddleware
38 import routes.util
38 import routes.util
39
39
40 import rhodecode
40 import rhodecode
41 import rhodecode.integrations # do not remove this as it registers celery tasks
41 from rhodecode.config import patches
42 from rhodecode.config import patches
42 from rhodecode.config.environment import (
43 from rhodecode.config.environment import (
43 load_environment, load_pyramid_environment)
44 load_environment, load_pyramid_environment)
44 from rhodecode.lib.middleware import csrf
45 from rhodecode.lib.middleware import csrf
45 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
47 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.vcs import VCSMiddleware
49 from rhodecode.lib.middleware.vcs import VCSMiddleware
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50
51
51
52
52 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
53
54
54
55
55 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
56 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
56 """Create a Pylons WSGI application and return it
57 """Create a Pylons WSGI application and return it
57
58
58 ``global_conf``
59 ``global_conf``
59 The inherited configuration for this application. Normally from
60 The inherited configuration for this application. Normally from
60 the [DEFAULT] section of the Paste ini file.
61 the [DEFAULT] section of the Paste ini file.
61
62
62 ``full_stack``
63 ``full_stack``
63 Whether or not this application provides a full WSGI stack (by
64 Whether or not this application provides a full WSGI stack (by
64 default, meaning it handles its own exceptions and errors).
65 default, meaning it handles its own exceptions and errors).
65 Disable full_stack when this application is "managed" by
66 Disable full_stack when this application is "managed" by
66 another WSGI middleware.
67 another WSGI middleware.
67
68
68 ``app_conf``
69 ``app_conf``
69 The application's local configuration. Normally specified in
70 The application's local configuration. Normally specified in
70 the [app:<name>] section of the Paste ini file (where <name>
71 the [app:<name>] section of the Paste ini file (where <name>
71 defaults to main).
72 defaults to main).
72
73
73 """
74 """
74 # Apply compatibility patches
75 # Apply compatibility patches
75 patches.kombu_1_5_1_python_2_7_11()
76 patches.kombu_1_5_1_python_2_7_11()
76 patches.inspect_getargspec()
77 patches.inspect_getargspec()
77
78
78 # Configure the Pylons environment
79 # Configure the Pylons environment
79 config = load_environment(global_conf, app_conf)
80 config = load_environment(global_conf, app_conf)
80
81
81 # The Pylons WSGI app
82 # The Pylons WSGI app
82 app = PylonsApp(config=config)
83 app = PylonsApp(config=config)
83 if rhodecode.is_test:
84 if rhodecode.is_test:
84 app = csrf.CSRFDetector(app)
85 app = csrf.CSRFDetector(app)
85
86
86 expected_origin = config.get('expected_origin')
87 expected_origin = config.get('expected_origin')
87 if expected_origin:
88 if expected_origin:
88 # The API can be accessed from other Origins.
89 # The API can be accessed from other Origins.
89 app = csrf.OriginChecker(app, expected_origin,
90 app = csrf.OriginChecker(app, expected_origin,
90 skip_urls=[routes.util.url_for('api')])
91 skip_urls=[routes.util.url_for('api')])
91
92
92
93
93 if asbool(full_stack):
94 if asbool(full_stack):
94
95
95 # Appenlight monitoring and error handler
96 # Appenlight monitoring and error handler
96 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
97 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
97
98
98 # we want our low level middleware to get to the request ASAP. We don't
99 # we want our low level middleware to get to the request ASAP. We don't
99 # need any pylons stack middleware in them
100 # need any pylons stack middleware in them
100 app = VCSMiddleware(app, config, appenlight_client)
101 app = VCSMiddleware(app, config, appenlight_client)
101
102
102 # Establish the Registry for this application
103 # Establish the Registry for this application
103 app = RegistryManager(app)
104 app = RegistryManager(app)
104
105
105 app.config = config
106 app.config = config
106
107
107 return app
108 return app
108
109
109
110
110 def make_pyramid_app(global_config, **settings):
111 def make_pyramid_app(global_config, **settings):
111 """
112 """
112 Constructs the WSGI application based on Pyramid and wraps the Pylons based
113 Constructs the WSGI application based on Pyramid and wraps the Pylons based
113 application.
114 application.
114
115
115 Specials:
116 Specials:
116
117
117 * We migrate from Pylons to Pyramid. While doing this, we keep both
118 * We migrate from Pylons to Pyramid. While doing this, we keep both
118 frameworks functional. This involves moving some WSGI middlewares around
119 frameworks functional. This involves moving some WSGI middlewares around
119 and providing access to some data internals, so that the old code is
120 and providing access to some data internals, so that the old code is
120 still functional.
121 still functional.
121
122
122 * The application can also be integrated like a plugin via the call to
123 * The application can also be integrated like a plugin via the call to
123 `includeme`. This is accompanied with the other utility functions which
124 `includeme`. This is accompanied with the other utility functions which
124 are called. Changing this should be done with great care to not break
125 are called. Changing this should be done with great care to not break
125 cases when these fragments are assembled from another place.
126 cases when these fragments are assembled from another place.
126
127
127 """
128 """
128 # The edition string should be available in pylons too, so we add it here
129 # The edition string should be available in pylons too, so we add it here
129 # before copying the settings.
130 # before copying the settings.
130 settings.setdefault('rhodecode.edition', 'Community Edition')
131 settings.setdefault('rhodecode.edition', 'Community Edition')
131
132
132 # As long as our Pylons application does expect "unprepared" settings, make
133 # As long as our Pylons application does expect "unprepared" settings, make
133 # sure that we keep an unmodified copy. This avoids unintentional change of
134 # sure that we keep an unmodified copy. This avoids unintentional change of
134 # behavior in the old application.
135 # behavior in the old application.
135 settings_pylons = settings.copy()
136 settings_pylons = settings.copy()
136
137
137 sanitize_settings_and_apply_defaults(settings)
138 sanitize_settings_and_apply_defaults(settings)
138 config = Configurator(settings=settings)
139 config = Configurator(settings=settings)
139 add_pylons_compat_data(config.registry, global_config, settings_pylons)
140 add_pylons_compat_data(config.registry, global_config, settings_pylons)
140
141
141 load_pyramid_environment(global_config, settings)
142 load_pyramid_environment(global_config, settings)
142
143
143 includeme(config)
144 includeme(config)
144 includeme_last(config)
145 includeme_last(config)
145 pyramid_app = config.make_wsgi_app()
146 pyramid_app = config.make_wsgi_app()
146 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
147 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
147 return pyramid_app
148 return pyramid_app
148
149
149
150
150 def add_pylons_compat_data(registry, global_config, settings):
151 def add_pylons_compat_data(registry, global_config, settings):
151 """
152 """
152 Attach data to the registry to support the Pylons integration.
153 Attach data to the registry to support the Pylons integration.
153 """
154 """
154 registry._pylons_compat_global_config = global_config
155 registry._pylons_compat_global_config = global_config
155 registry._pylons_compat_settings = settings
156 registry._pylons_compat_settings = settings
156
157
157
158
158 def webob_to_pyramid_http_response(webob_response):
159 def webob_to_pyramid_http_response(webob_response):
159 ResponseClass = httpexceptions.status_map[webob_response.status_int]
160 ResponseClass = httpexceptions.status_map[webob_response.status_int]
160 pyramid_response = ResponseClass(webob_response.status)
161 pyramid_response = ResponseClass(webob_response.status)
161 pyramid_response.status = webob_response.status
162 pyramid_response.status = webob_response.status
162 pyramid_response.headers.update(webob_response.headers)
163 pyramid_response.headers.update(webob_response.headers)
163 if pyramid_response.headers['content-type'] == 'text/html':
164 if pyramid_response.headers['content-type'] == 'text/html':
164 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
165 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
165 return pyramid_response
166 return pyramid_response
166
167
167
168
168 def error_handler(exception, request):
169 def error_handler(exception, request):
169 # TODO: dan: replace the old pylons error controller with this
170 # TODO: dan: replace the old pylons error controller with this
170 from rhodecode.model.settings import SettingsModel
171 from rhodecode.model.settings import SettingsModel
171 from rhodecode.lib.utils2 import AttributeDict
172 from rhodecode.lib.utils2 import AttributeDict
172
173
173 try:
174 try:
174 rc_config = SettingsModel().get_all_settings()
175 rc_config = SettingsModel().get_all_settings()
175 except Exception:
176 except Exception:
176 log.exception('failed to fetch settings')
177 log.exception('failed to fetch settings')
177 rc_config = {}
178 rc_config = {}
178
179
179 base_response = HTTPInternalServerError()
180 base_response = HTTPInternalServerError()
180 # prefer original exception for the response since it may have headers set
181 # prefer original exception for the response since it may have headers set
181 if isinstance(exception, HTTPError):
182 if isinstance(exception, HTTPError):
182 base_response = exception
183 base_response = exception
183
184
184 c = AttributeDict()
185 c = AttributeDict()
185 c.error_message = base_response.status
186 c.error_message = base_response.status
186 c.error_explanation = base_response.explanation or str(base_response)
187 c.error_explanation = base_response.explanation or str(base_response)
187 c.visual = AttributeDict()
188 c.visual = AttributeDict()
188
189
189 c.visual.rhodecode_support_url = (
190 c.visual.rhodecode_support_url = (
190 request.registry.settings.get('rhodecode_support_url') or
191 request.registry.settings.get('rhodecode_support_url') or
191 request.route_url('rhodecode_support')
192 request.route_url('rhodecode_support')
192 )
193 )
193 c.redirect_time = 0
194 c.redirect_time = 0
194 c.rhodecode_name = rc_config.get('rhodecode_title', '')
195 c.rhodecode_name = rc_config.get('rhodecode_title', '')
195 if not c.rhodecode_name:
196 if not c.rhodecode_name:
196 c.rhodecode_name = 'Rhodecode'
197 c.rhodecode_name = 'Rhodecode'
197
198
198 response = render_to_response(
199 response = render_to_response(
199 '/errors/error_document.html', {'c': c}, request=request,
200 '/errors/error_document.html', {'c': c}, request=request,
200 response=base_response)
201 response=base_response)
201
202
202 return response
203 return response
203
204
204
205
205 def includeme(config):
206 def includeme(config):
206 settings = config.registry.settings
207 settings = config.registry.settings
207
208
208 if asbool(settings.get('appenlight', 'false')):
209 if asbool(settings.get('appenlight', 'false')):
209 config.include('appenlight_client.ext.pyramid_tween')
210 config.include('appenlight_client.ext.pyramid_tween')
210
211
211 # Includes which are required. The application would fail without them.
212 # Includes which are required. The application would fail without them.
212 config.include('pyramid_mako')
213 config.include('pyramid_mako')
213 config.include('pyramid_beaker')
214 config.include('pyramid_beaker')
214 config.include('rhodecode.admin')
215 config.include('rhodecode.admin')
215 config.include('rhodecode.authentication')
216 config.include('rhodecode.authentication')
216 config.include('rhodecode.integrations')
217 config.include('rhodecode.integrations')
217 config.include('rhodecode.login')
218 config.include('rhodecode.login')
218 config.include('rhodecode.tweens')
219 config.include('rhodecode.tweens')
219 config.include('rhodecode.api')
220 config.include('rhodecode.api')
220 config.add_route(
221 config.add_route(
221 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
222 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
222
223
223 # Set the authorization policy.
224 # Set the authorization policy.
224 authz_policy = ACLAuthorizationPolicy()
225 authz_policy = ACLAuthorizationPolicy()
225 config.set_authorization_policy(authz_policy)
226 config.set_authorization_policy(authz_policy)
226
227
227 # Set the default renderer for HTML templates to mako.
228 # Set the default renderer for HTML templates to mako.
228 config.add_mako_renderer('.html')
229 config.add_mako_renderer('.html')
229
230
230 # plugin information
231 # plugin information
231 config.registry.rhodecode_plugins = {}
232 config.registry.rhodecode_plugins = {}
232
233
233 config.add_directive(
234 config.add_directive(
234 'register_rhodecode_plugin', register_rhodecode_plugin)
235 'register_rhodecode_plugin', register_rhodecode_plugin)
235 # include RhodeCode plugins
236 # include RhodeCode plugins
236 includes = aslist(settings.get('rhodecode.includes', []))
237 includes = aslist(settings.get('rhodecode.includes', []))
237 for inc in includes:
238 for inc in includes:
238 config.include(inc)
239 config.include(inc)
239
240
240 pylons_app = make_app(
241 pylons_app = make_app(
241 config.registry._pylons_compat_global_config,
242 config.registry._pylons_compat_global_config,
242 **config.registry._pylons_compat_settings)
243 **config.registry._pylons_compat_settings)
243 config.registry._pylons_compat_config = pylons_app.config
244 config.registry._pylons_compat_config = pylons_app.config
244
245
245 pylons_app_as_view = wsgiapp(pylons_app)
246 pylons_app_as_view = wsgiapp(pylons_app)
246
247
247 # Protect from VCS Server error related pages when server is not available
248 # Protect from VCS Server error related pages when server is not available
248 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
249 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
249 if not vcs_server_enabled:
250 if not vcs_server_enabled:
250 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
251 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
251
252
252
253
253 def pylons_app_with_error_handler(context, request):
254 def pylons_app_with_error_handler(context, request):
254 """
255 """
255 Handle exceptions from rc pylons app:
256 Handle exceptions from rc pylons app:
256
257
257 - old webob type exceptions get converted to pyramid exceptions
258 - old webob type exceptions get converted to pyramid exceptions
258 - pyramid exceptions are passed to the error handler view
259 - pyramid exceptions are passed to the error handler view
259 """
260 """
260 try:
261 try:
261 response = pylons_app_as_view(context, request)
262 response = pylons_app_as_view(context, request)
262 if 400 <= response.status_int <= 599: # webob type error responses
263 if 400 <= response.status_int <= 599: # webob type error responses
263 return error_handler(
264 return error_handler(
264 webob_to_pyramid_http_response(response), request)
265 webob_to_pyramid_http_response(response), request)
265 except HTTPError as e: # pyramid type exceptions
266 except HTTPError as e: # pyramid type exceptions
266 return error_handler(e, request)
267 return error_handler(e, request)
267 except Exception:
268 except Exception:
268 if settings.get('debugtoolbar.enabled', False):
269 if settings.get('debugtoolbar.enabled', False):
269 raise
270 raise
270 return error_handler(HTTPInternalServerError(), request)
271 return error_handler(HTTPInternalServerError(), request)
271 return response
272 return response
272
273
273 # This is the glue which allows us to migrate in chunks. By registering the
274 # This is the glue which allows us to migrate in chunks. By registering the
274 # pylons based application as the "Not Found" view in Pyramid, we will
275 # pylons based application as the "Not Found" view in Pyramid, we will
275 # fallback to the old application each time the new one does not yet know
276 # fallback to the old application each time the new one does not yet know
276 # how to handle a request.
277 # how to handle a request.
277 config.add_notfound_view(pylons_app_with_error_handler)
278 config.add_notfound_view(pylons_app_with_error_handler)
278
279
279 if settings.get('debugtoolbar.enabled', False):
280 if settings.get('debugtoolbar.enabled', False):
280 # if toolbar, then only http type exceptions get caught and rendered
281 # if toolbar, then only http type exceptions get caught and rendered
281 ExcClass = HTTPError
282 ExcClass = HTTPError
282 else:
283 else:
283 # if no toolbar, then any exception gets caught and rendered
284 # if no toolbar, then any exception gets caught and rendered
284 ExcClass = Exception
285 ExcClass = Exception
285 config.add_view(error_handler, context=ExcClass)
286 config.add_view(error_handler, context=ExcClass)
286
287
287
288
288 def includeme_last(config):
289 def includeme_last(config):
289 """
290 """
290 The static file catchall needs to be last in the view configuration.
291 The static file catchall needs to be last in the view configuration.
291 """
292 """
292 settings = config.registry.settings
293 settings = config.registry.settings
293
294
294 # Note: johbo: I would prefer to register a prefix for static files at some
295 # Note: johbo: I would prefer to register a prefix for static files at some
295 # point, e.g. move them under '_static/'. This would fully avoid that we
296 # point, e.g. move them under '_static/'. This would fully avoid that we
296 # can have name clashes with a repository name. Imaging someone calling his
297 # can have name clashes with a repository name. Imaging someone calling his
297 # repo "css" ;-) Also having an external web server to serve out the static
298 # repo "css" ;-) Also having an external web server to serve out the static
298 # files seems to be easier to set up if they have a common prefix.
299 # files seems to be easier to set up if they have a common prefix.
299 #
300 #
300 # Example: config.add_static_view('_static', path='rhodecode:public')
301 # Example: config.add_static_view('_static', path='rhodecode:public')
301 #
302 #
302 # It might be an option to register both paths for a while and then migrate
303 # It might be an option to register both paths for a while and then migrate
303 # over to the new location.
304 # over to the new location.
304
305
305 # Serving static files with a catchall.
306 # Serving static files with a catchall.
306 if settings['static_files']:
307 if settings['static_files']:
307 config.add_route('catchall_static', '/*subpath')
308 config.add_route('catchall_static', '/*subpath')
308 config.add_view(
309 config.add_view(
309 static_view('rhodecode:public'), route_name='catchall_static')
310 static_view('rhodecode:public'), route_name='catchall_static')
310
311
311
312
312 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
313 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
313 """
314 """
314 Apply outer WSGI middlewares around the application.
315 Apply outer WSGI middlewares around the application.
315
316
316 Part of this has been moved up from the Pylons layer, so that the
317 Part of this has been moved up from the Pylons layer, so that the
317 data is also available if old Pylons code is hit through an already ported
318 data is also available if old Pylons code is hit through an already ported
318 view.
319 view.
319 """
320 """
320 settings = config.registry.settings
321 settings = config.registry.settings
321
322
322 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
323 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
323 pyramid_app = HttpsFixup(pyramid_app, settings)
324 pyramid_app = HttpsFixup(pyramid_app, settings)
324
325
325 # Add RoutesMiddleware to support the pylons compatibility tween during
326 # Add RoutesMiddleware to support the pylons compatibility tween during
326
327
327 # migration to pyramid.
328 # migration to pyramid.
328 pyramid_app = RoutesMiddleware(
329 pyramid_app = RoutesMiddleware(
329 pyramid_app, config.registry._pylons_compat_config['routes.map'])
330 pyramid_app, config.registry._pylons_compat_config['routes.map'])
330
331
331 if asbool(settings.get('appenlight', 'false')):
332 if asbool(settings.get('appenlight', 'false')):
332 pyramid_app, _ = wrap_in_appenlight_if_enabled(
333 pyramid_app, _ = wrap_in_appenlight_if_enabled(
333 pyramid_app, config.registry._pylons_compat_config)
334 pyramid_app, config.registry._pylons_compat_config)
334
335
335 # TODO: johbo: Don't really see why we enable the gzip middleware when
336 # TODO: johbo: Don't really see why we enable the gzip middleware when
336 # serving static files, might be something that should have its own setting
337 # serving static files, might be something that should have its own setting
337 # as well?
338 # as well?
338 if settings['static_files']:
339 if settings['static_files']:
339 pyramid_app = make_gzip_middleware(
340 pyramid_app = make_gzip_middleware(
340 pyramid_app, settings, compress_level=1)
341 pyramid_app, settings, compress_level=1)
341
342
342 return pyramid_app
343 return pyramid_app
343
344
344
345
345 def sanitize_settings_and_apply_defaults(settings):
346 def sanitize_settings_and_apply_defaults(settings):
346 """
347 """
347 Applies settings defaults and does all type conversion.
348 Applies settings defaults and does all type conversion.
348
349
349 We would move all settings parsing and preparation into this place, so that
350 We would move all settings parsing and preparation into this place, so that
350 we have only one place left which deals with this part. The remaining parts
351 we have only one place left which deals with this part. The remaining parts
351 of the application would start to rely fully on well prepared settings.
352 of the application would start to rely fully on well prepared settings.
352
353
353 This piece would later be split up per topic to avoid a big fat monster
354 This piece would later be split up per topic to avoid a big fat monster
354 function.
355 function.
355 """
356 """
356
357
357 # Pyramid's mako renderer has to search in the templates folder so that the
358 # Pyramid's mako renderer has to search in the templates folder so that the
358 # old templates still work. Ported and new templates are expected to use
359 # old templates still work. Ported and new templates are expected to use
359 # real asset specifications for the includes.
360 # real asset specifications for the includes.
360 mako_directories = settings.setdefault('mako.directories', [
361 mako_directories = settings.setdefault('mako.directories', [
361 # Base templates of the original Pylons application
362 # Base templates of the original Pylons application
362 'rhodecode:templates',
363 'rhodecode:templates',
363 ])
364 ])
364 log.debug(
365 log.debug(
365 "Using the following Mako template directories: %s",
366 "Using the following Mako template directories: %s",
366 mako_directories)
367 mako_directories)
367
368
368 # Default includes, possible to change as a user
369 # Default includes, possible to change as a user
369 pyramid_includes = settings.setdefault('pyramid.includes', [
370 pyramid_includes = settings.setdefault('pyramid.includes', [
370 'rhodecode.lib.middleware.request_wrapper',
371 'rhodecode.lib.middleware.request_wrapper',
371 ])
372 ])
372 log.debug(
373 log.debug(
373 "Using the following pyramid.includes: %s",
374 "Using the following pyramid.includes: %s",
374 pyramid_includes)
375 pyramid_includes)
375
376
376 # TODO: johbo: Re-think this, usually the call to config.include
377 # TODO: johbo: Re-think this, usually the call to config.include
377 # should allow to pass in a prefix.
378 # should allow to pass in a prefix.
378 settings.setdefault('rhodecode.api.url', '/_admin/api')
379 settings.setdefault('rhodecode.api.url', '/_admin/api')
379
380
380 _bool_setting(settings, 'vcs.server.enable', 'true')
381 _bool_setting(settings, 'vcs.server.enable', 'true')
381 _bool_setting(settings, 'static_files', 'true')
382 _bool_setting(settings, 'static_files', 'true')
382 _bool_setting(settings, 'is_test', 'false')
383 _bool_setting(settings, 'is_test', 'false')
383
384
384 return settings
385 return settings
385
386
386
387
387 def _bool_setting(settings, name, default):
388 def _bool_setting(settings, name, default):
388 settings[name] = asbool(settings.get(name, default))
389 settings[name] = asbool(settings.get(name, default))
@@ -1,202 +1,202 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22
22
23 import re
23 import re
24 import logging
24 import logging
25 import requests
25 import requests
26 import colander
26 import colander
27 from celery.task import task
27 from celery.task import task
28 from mako.template import Template
28 from mako.template import Template
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import lazy_ugettext
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib.celerylib import run_task
33 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.integrations.types.base import IntegrationTypeBase
35 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
37
37
38 log = logging.getLogger()
38 log = logging.getLogger()
39
39
40
40
41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
42 service = colander.SchemaNode(
42 service = colander.SchemaNode(
43 colander.String(),
43 colander.String(),
44 title=lazy_ugettext('Slack service URL'),
44 title=lazy_ugettext('Slack service URL'),
45 description=h.literal(lazy_ugettext(
45 description=h.literal(lazy_ugettext(
46 'This can be setup at the '
46 'This can be setup at the '
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
48 'slack app manager</a>')),
48 'slack app manager</a>')),
49 default='',
49 default='',
50 placeholder='https://hooks.slack.com/services/...',
50 placeholder='https://hooks.slack.com/services/...',
51 preparer=strip_whitespace,
51 preparer=strip_whitespace,
52 validator=colander.url,
52 validator=colander.url,
53 widget='string'
53 widget='string'
54 )
54 )
55 username = colander.SchemaNode(
55 username = colander.SchemaNode(
56 colander.String(),
56 colander.String(),
57 title=lazy_ugettext('Username'),
57 title=lazy_ugettext('Username'),
58 description=lazy_ugettext('Username to show notifications coming from.'),
58 description=lazy_ugettext('Username to show notifications coming from.'),
59 missing='Rhodecode',
59 missing='Rhodecode',
60 preparer=strip_whitespace,
60 preparer=strip_whitespace,
61 widget='string',
61 widget='string',
62 placeholder='Rhodecode'
62 placeholder='Rhodecode'
63 )
63 )
64 channel = colander.SchemaNode(
64 channel = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 title=lazy_ugettext('Channel'),
66 title=lazy_ugettext('Channel'),
67 description=lazy_ugettext('Channel to send notifications to.'),
67 description=lazy_ugettext('Channel to send notifications to.'),
68 missing='',
68 missing='',
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 widget='string',
70 widget='string',
71 placeholder='#general'
71 placeholder='#general'
72 )
72 )
73 icon_emoji = colander.SchemaNode(
73 icon_emoji = colander.SchemaNode(
74 colander.String(),
74 colander.String(),
75 title=lazy_ugettext('Emoji'),
75 title=lazy_ugettext('Emoji'),
76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
77 missing='',
77 missing='',
78 preparer=strip_whitespace,
78 preparer=strip_whitespace,
79 widget='string',
79 widget='string',
80 placeholder=':studio_microphone:'
80 placeholder=':studio_microphone:'
81 )
81 )
82
82
83
83
84 repo_push_template = Template(r'''
84 repo_push_template = Template(r'''
85 *${data['actor']['username']}* pushed to \
85 *${data['actor']['username']}* pushed to \
86 %if data['push']['branches']:
86 %if data['push']['branches']:
87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
89 %else:
89 %else:
90 unknown branch \
90 unknown branch \
91 %endif
91 %endif
92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
93 >>>
93 >>>
94 %for commit in data['push']['commits']:
94 %for commit in data['push']['commits']:
95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
96 %endfor
96 %endfor
97 ''')
97 ''')
98
98
99
99
100 class SlackIntegrationType(IntegrationTypeBase):
100 class SlackIntegrationType(IntegrationTypeBase):
101 key = 'slack'
101 key = 'slack'
102 display_name = lazy_ugettext('Slack')
102 display_name = lazy_ugettext('Slack')
103 SettingsSchema = SlackSettingsSchema
103 SettingsSchema = SlackSettingsSchema
104 valid_events = [
104 valid_events = [
105 events.PullRequestCloseEvent,
105 events.PullRequestCloseEvent,
106 events.PullRequestMergeEvent,
106 events.PullRequestMergeEvent,
107 events.PullRequestUpdateEvent,
107 events.PullRequestUpdateEvent,
108 events.PullRequestReviewEvent,
108 events.PullRequestReviewEvent,
109 events.PullRequestCreateEvent,
109 events.PullRequestCreateEvent,
110 events.RepoPushEvent,
110 events.RepoPushEvent,
111 events.RepoCreateEvent,
111 events.RepoCreateEvent,
112 ]
112 ]
113
113
114 def send_event(self, event):
114 def send_event(self, event):
115 if event.__class__ not in self.valid_events:
115 if event.__class__ not in self.valid_events:
116 log.debug('event not valid: %r' % event)
116 log.debug('event not valid: %r' % event)
117 return
117 return
118
118
119 if event.name not in self.settings['events']:
119 if event.name not in self.settings['events']:
120 log.debug('event ignored: %r' % event)
120 log.debug('event ignored: %r' % event)
121 return
121 return
122
122
123 data = event.as_dict()
123 data = event.as_dict()
124
124
125 text = '*%s* caused a *%s* event' % (
125 text = '*%s* caused a *%s* event' % (
126 data['actor']['username'], event.name)
126 data['actor']['username'], event.name)
127
127
128 log.debug('handling slack event for %s' % event.name)
128 log.debug('handling slack event for %s' % event.name)
129
129
130 if isinstance(event, events.PullRequestEvent):
130 if isinstance(event, events.PullRequestEvent):
131 text = self.format_pull_request_event(event, data)
131 text = self.format_pull_request_event(event, data)
132 elif isinstance(event, events.RepoPushEvent):
132 elif isinstance(event, events.RepoPushEvent):
133 text = self.format_repo_push_event(data)
133 text = self.format_repo_push_event(data)
134 elif isinstance(event, events.RepoCreateEvent):
134 elif isinstance(event, events.RepoCreateEvent):
135 text = self.format_repo_create_event(data)
135 text = self.format_repo_create_event(data)
136 else:
136 else:
137 log.error('unhandled event type: %r' % event)
137 log.error('unhandled event type: %r' % event)
138
138
139 run_task(post_text_to_slack, self.settings, text)
139 run_task(post_text_to_slack, self.settings, text)
140
140
141 @classmethod
141 @classmethod
142 def settings_schema(cls):
142 def settings_schema(cls):
143 schema = SlackSettingsSchema()
143 schema = SlackSettingsSchema()
144 schema.add(colander.SchemaNode(
144 schema.add(colander.SchemaNode(
145 colander.Set(),
145 colander.Set(),
146 widget='checkbox_list',
146 widget='checkbox_list',
147 choices=sorted([e.name for e in cls.valid_events]),
147 choices=sorted([e.name for e in cls.valid_events]),
148 description="Events activated for this integration",
148 description="Events activated for this integration",
149 default=[e.name for e in cls.valid_events],
149 default=[e.name for e in cls.valid_events],
150 name='events'
150 name='events'
151 ))
151 ))
152 return schema
152 return schema
153
153
154 def format_pull_request_event(self, event, data):
154 def format_pull_request_event(self, event, data):
155 action = {
155 action = {
156 events.PullRequestCloseEvent: 'closed',
156 events.PullRequestCloseEvent: 'closed',
157 events.PullRequestMergeEvent: 'merged',
157 events.PullRequestMergeEvent: 'merged',
158 events.PullRequestUpdateEvent: 'updated',
158 events.PullRequestUpdateEvent: 'updated',
159 events.PullRequestReviewEvent: 'reviewed',
159 events.PullRequestReviewEvent: 'reviewed',
160 events.PullRequestCreateEvent: 'created',
160 events.PullRequestCreateEvent: 'created',
161 }.get(event.__class__, '<unknown action>')
161 }.get(event.__class__, '<unknown action>')
162
162
163 return ('Pull request <{url}|#{number}> ({title}) '
163 return ('Pull request <{url}|#{number}> ({title}) '
164 '{action} by {user}').format(
164 '{action} by {user}').format(
165 user=data['actor']['username'],
165 user=data['actor']['username'],
166 number=data['pullrequest']['pull_request_id'],
166 number=data['pullrequest']['pull_request_id'],
167 url=data['pullrequest']['url'],
167 url=data['pullrequest']['url'],
168 title=data['pullrequest']['title'],
168 title=data['pullrequest']['title'],
169 action=action
169 action=action
170 )
170 )
171
171
172 def format_repo_push_event(self, data):
172 def format_repo_push_event(self, data):
173 result = repo_push_template.render(
173 result = repo_push_template.render(
174 data=data,
174 data=data,
175 html_to_slack_links=html_to_slack_links,
175 html_to_slack_links=html_to_slack_links,
176 )
176 )
177 return result
177 return result
178
178
179 def format_repo_create_msg(self, data):
179 def format_repo_create_event(self, data):
180 return '<{}|{}> ({}) repository created by *{}*'.format(
180 return '<{}|{}> ({}) repository created by *{}*'.format(
181 data['repo']['url'],
181 data['repo']['url'],
182 data['repo']['repo_name'],
182 data['repo']['repo_name'],
183 data['repo']['repo_type'],
183 data['repo']['repo_type'],
184 data['actor']['username'],
184 data['actor']['username'],
185 )
185 )
186
186
187
187
188 def html_to_slack_links(message):
188 def html_to_slack_links(message):
189 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
189 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
190 r'<\1|\2>', message)
190 r'<\1|\2>', message)
191
191
192
192
193 @task(ignore_result=True)
193 @task(ignore_result=True)
194 def post_text_to_slack(settings, text):
194 def post_text_to_slack(settings, text):
195 log.debug('sending %s to slack %s' % (text, settings['service']))
195 log.debug('sending %s to slack %s' % (text, settings['service']))
196 resp = requests.post(settings['service'], json={
196 resp = requests.post(settings['service'], json={
197 "channel": settings.get('channel', ''),
197 "channel": settings.get('channel', ''),
198 "username": settings.get('username', 'Rhodecode'),
198 "username": settings.get('username', 'Rhodecode'),
199 "text": text,
199 "text": text,
200 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
200 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
201 })
201 })
202 resp.raise_for_status() # raise exception on a failed request
202 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now