##// END OF EJS Templates
events: use a distinction between RhodeCodeEvent which is a base class and it used by all events, and...
marcink -
r2921:042146f6 default
parent child Browse files
Show More
@@ -1,522 +1,518 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import traceback
25 25 import collections
26 26 import tempfile
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pyramid.wsgi import wsgiapp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.httpexceptions import (
34 34 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
35 35 from pyramid.events import ApplicationCreated
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 44 from rhodecode.lib.request import Request
45 45 from rhodecode.lib.vcs import VCSCommunicationError
46 46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 49 from rhodecode.lib.celerylib.loader import configure_celery
50 50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 52 from rhodecode.lib.exc_tracking import store_exception
53 53 from rhodecode.subscribers import (
54 54 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 55 write_metadata_if_needed, inject_app_settings)
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 def is_http_error(response):
62 62 # error which should have traceback
63 63 return response.status_code > 499
64 64
65 65
66 66 def make_pyramid_app(global_config, **settings):
67 67 """
68 68 Constructs the WSGI application based on Pyramid.
69 69
70 70 Specials:
71 71
72 72 * The application can also be integrated like a plugin via the call to
73 73 `includeme`. This is accompanied with the other utility functions which
74 74 are called. Changing this should be done with great care to not break
75 75 cases when these fragments are assembled from another place.
76 76
77 77 """
78 78
79 79 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
80 80 # will be replaced by the value of the environment variable "NAME" in this case.
81 81 environ = {
82 82 'ENV_{}'.format(key): value for key, value in os.environ.items()}
83 83
84 84 global_config = _substitute_values(global_config, environ)
85 85 settings = _substitute_values(settings, environ)
86 86
87 87 sanitize_settings_and_apply_defaults(settings)
88 88
89 89 config = Configurator(settings=settings)
90 90
91 91 # Apply compatibility patches
92 92 patches.inspect_getargspec()
93 93
94 94 load_pyramid_environment(global_config, settings)
95 95
96 96 # Static file view comes first
97 97 includeme_first(config)
98 98
99 99 includeme(config)
100 100
101 101 pyramid_app = config.make_wsgi_app()
102 102 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
103 103 pyramid_app.config = config
104 104
105 105 config.configure_celery(global_config['__file__'])
106 106 # creating the app uses a connection - return it after we are done
107 107 meta.Session.remove()
108 108
109 109 log.info('Pyramid app %s created and configured.', pyramid_app)
110 110 return pyramid_app
111 111
112 112
113 113 def not_found_view(request):
114 114 """
115 115 This creates the view which should be registered as not-found-view to
116 116 pyramid.
117 117 """
118 118
119 119 if not getattr(request, 'vcs_call', None):
120 120 # handle like regular case with our error_handler
121 121 return error_handler(HTTPNotFound(), request)
122 122
123 123 # handle not found view as a vcs call
124 124 settings = request.registry.settings
125 125 ae_client = getattr(request, 'ae_client', None)
126 126 vcs_app = VCSMiddleware(
127 127 HTTPNotFound(), request.registry, settings,
128 128 appenlight_client=ae_client)
129 129
130 130 return wsgiapp(vcs_app)(None, request)
131 131
132 132
133 133 def error_handler(exception, request):
134 134 import rhodecode
135 135 from rhodecode.lib import helpers
136 136
137 137 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
138 138
139 139 base_response = HTTPInternalServerError()
140 140 # prefer original exception for the response since it may have headers set
141 141 if isinstance(exception, HTTPException):
142 142 base_response = exception
143 143 elif isinstance(exception, VCSCommunicationError):
144 144 base_response = VCSServerUnavailable()
145 145
146 146 if is_http_error(base_response):
147 147 log.exception(
148 148 'error occurred handling this request for path: %s', request.path)
149 149
150 150 error_explanation = base_response.explanation or str(base_response)
151 151 if base_response.status_code == 404:
152 152 error_explanation += " Or you don't have permission to access it."
153 153 c = AttributeDict()
154 154 c.error_message = base_response.status
155 155 c.error_explanation = error_explanation
156 156 c.visual = AttributeDict()
157 157
158 158 c.visual.rhodecode_support_url = (
159 159 request.registry.settings.get('rhodecode_support_url') or
160 160 request.route_url('rhodecode_support')
161 161 )
162 162 c.redirect_time = 0
163 163 c.rhodecode_name = rhodecode_title
164 164 if not c.rhodecode_name:
165 165 c.rhodecode_name = 'Rhodecode'
166 166
167 167 c.causes = []
168 168 if is_http_error(base_response):
169 169 c.causes.append('Server is overloaded.')
170 170 c.causes.append('Server database connection is lost.')
171 171 c.causes.append('Server expected unhandled error.')
172 172
173 173 if hasattr(base_response, 'causes'):
174 174 c.causes = base_response.causes
175 175
176 176 c.messages = helpers.flash.pop_messages(request=request)
177 177
178 178 exc_info = sys.exc_info()
179 179 c.exception_id = id(exc_info)
180 180 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
181 181 or base_response.status_code > 499
182 182 c.exception_id_url = request.route_url(
183 183 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
184 184
185 185 if c.show_exception_id:
186 186 store_exception(c.exception_id, exc_info)
187 187
188 188 response = render_to_response(
189 189 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
190 190 response=base_response)
191 191
192 192 return response
193 193
194 194
195 195 def includeme_first(config):
196 196 # redirect automatic browser favicon.ico requests to correct place
197 197 def favicon_redirect(context, request):
198 198 return HTTPFound(
199 199 request.static_path('rhodecode:public/images/favicon.ico'))
200 200
201 201 config.add_view(favicon_redirect, route_name='favicon')
202 202 config.add_route('favicon', '/favicon.ico')
203 203
204 204 def robots_redirect(context, request):
205 205 return HTTPFound(
206 206 request.static_path('rhodecode:public/robots.txt'))
207 207
208 208 config.add_view(robots_redirect, route_name='robots')
209 209 config.add_route('robots', '/robots.txt')
210 210
211 211 config.add_static_view(
212 212 '_static/deform', 'deform:static')
213 213 config.add_static_view(
214 214 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
215 215
216 216
217 217 def includeme(config):
218 218 settings = config.registry.settings
219 219 config.set_request_factory(Request)
220 220
221 221 # plugin information
222 222 config.registry.rhodecode_plugins = collections.OrderedDict()
223 223
224 224 config.add_directive(
225 225 'register_rhodecode_plugin', register_rhodecode_plugin)
226 226
227 227 config.add_directive('configure_celery', configure_celery)
228 228
229 229 if asbool(settings.get('appenlight', 'false')):
230 230 config.include('appenlight_client.ext.pyramid_tween')
231 231
232 232 # Includes which are required. The application would fail without them.
233 233 config.include('pyramid_mako')
234 234 config.include('pyramid_beaker')
235 235 config.include('rhodecode.lib.caches')
236 236 config.include('rhodecode.lib.rc_cache')
237 237
238 238 config.include('rhodecode.authentication')
239 239 config.include('rhodecode.integrations')
240 240
241 241 # apps
242 242 config.include('rhodecode.apps._base')
243 243 config.include('rhodecode.apps.ops')
244 244
245 245 config.include('rhodecode.apps.admin')
246 246 config.include('rhodecode.apps.channelstream')
247 247 config.include('rhodecode.apps.login')
248 248 config.include('rhodecode.apps.home')
249 249 config.include('rhodecode.apps.journal')
250 250 config.include('rhodecode.apps.repository')
251 251 config.include('rhodecode.apps.repo_group')
252 252 config.include('rhodecode.apps.user_group')
253 253 config.include('rhodecode.apps.search')
254 254 config.include('rhodecode.apps.user_profile')
255 255 config.include('rhodecode.apps.user_group_profile')
256 256 config.include('rhodecode.apps.my_account')
257 257 config.include('rhodecode.apps.svn_support')
258 258 config.include('rhodecode.apps.ssh_support')
259 259 config.include('rhodecode.apps.gist')
260 260
261 261 config.include('rhodecode.apps.debug_style')
262 262 config.include('rhodecode.tweens')
263 263 config.include('rhodecode.api')
264 264
265 265 config.add_route(
266 266 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
267 267
268 268 config.add_translation_dirs('rhodecode:i18n/')
269 269 settings['default_locale_name'] = settings.get('lang', 'en')
270 270
271 271 # Add subscribers.
272 272 config.add_subscriber(inject_app_settings, ApplicationCreated)
273 273 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
274 274 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
275 275 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
276 276
277 # events
278 # TODO(marcink): this should be done when pyramid migration is finished
279 # config.add_subscriber(
280 # 'rhodecode.integrations.integrations_event_handler',
281 # 'rhodecode.events.RhodecodeEvent')
282 277
283 278 # request custom methods
284 279 config.add_request_method(
285 280 'rhodecode.lib.partial_renderer.get_partial_renderer',
286 281 'get_partial_renderer')
287 282
288 283 # Set the authorization policy.
289 284 authz_policy = ACLAuthorizationPolicy()
290 285 config.set_authorization_policy(authz_policy)
291 286
292 287 # Set the default renderer for HTML templates to mako.
293 288 config.add_mako_renderer('.html')
294 289
295 290 config.add_renderer(
296 291 name='json_ext',
297 292 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
298 293
299 294 # include RhodeCode plugins
300 295 includes = aslist(settings.get('rhodecode.includes', []))
301 296 for inc in includes:
302 297 config.include(inc)
303 298
304 299 # custom not found view, if our pyramid app doesn't know how to handle
305 300 # the request pass it to potential VCS handling ap
306 301 config.add_notfound_view(not_found_view)
307 302 if not settings.get('debugtoolbar.enabled', False):
308 303 # disabled debugtoolbar handle all exceptions via the error_handlers
309 304 config.add_view(error_handler, context=Exception)
310 305
311 306 # all errors including 403/404/50X
312 307 config.add_view(error_handler, context=HTTPError)
313 308
314 309
315 310 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
316 311 """
317 312 Apply outer WSGI middlewares around the application.
318 313 """
319 settings = config.registry.settings
314 registry = config.registry
315 settings = registry.settings
320 316
321 317 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
322 318 pyramid_app = HttpsFixup(pyramid_app, settings)
323 319
324 320 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
325 321 pyramid_app, settings)
326 config.registry.ae_client = _ae_client
322 registry.ae_client = _ae_client
327 323
328 324 if settings['gzip_responses']:
329 325 pyramid_app = make_gzip_middleware(
330 326 pyramid_app, settings, compress_level=1)
331 327
332 328 # this should be the outer most middleware in the wsgi stack since
333 329 # middleware like Routes make database calls
334 330 def pyramid_app_with_cleanup(environ, start_response):
335 331 try:
336 332 return pyramid_app(environ, start_response)
337 333 finally:
338 334 # Dispose current database session and rollback uncommitted
339 335 # transactions.
340 336 meta.Session.remove()
341 337
342 338 # In a single threaded mode server, on non sqlite db we should have
343 339 # '0 Current Checked out connections' at the end of a request,
344 340 # if not, then something, somewhere is leaving a connection open
345 341 pool = meta.Base.metadata.bind.engine.pool
346 342 log.debug('sa pool status: %s', pool.status())
347 343
348 344 return pyramid_app_with_cleanup
349 345
350 346
351 347 def sanitize_settings_and_apply_defaults(settings):
352 348 """
353 349 Applies settings defaults and does all type conversion.
354 350
355 351 We would move all settings parsing and preparation into this place, so that
356 352 we have only one place left which deals with this part. The remaining parts
357 353 of the application would start to rely fully on well prepared settings.
358 354
359 355 This piece would later be split up per topic to avoid a big fat monster
360 356 function.
361 357 """
362 358
363 359 settings.setdefault('rhodecode.edition', 'Community Edition')
364 360
365 361 if 'mako.default_filters' not in settings:
366 362 # set custom default filters if we don't have it defined
367 363 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
368 364 settings['mako.default_filters'] = 'h_filter'
369 365
370 366 if 'mako.directories' not in settings:
371 367 mako_directories = settings.setdefault('mako.directories', [
372 368 # Base templates of the original application
373 369 'rhodecode:templates',
374 370 ])
375 371 log.debug(
376 372 "Using the following Mako template directories: %s",
377 373 mako_directories)
378 374
379 375 # Default includes, possible to change as a user
380 376 pyramid_includes = settings.setdefault('pyramid.includes', [
381 377 'rhodecode.lib.middleware.request_wrapper',
382 378 ])
383 379 log.debug(
384 380 "Using the following pyramid.includes: %s",
385 381 pyramid_includes)
386 382
387 383 # TODO: johbo: Re-think this, usually the call to config.include
388 384 # should allow to pass in a prefix.
389 385 settings.setdefault('rhodecode.api.url', '/_admin/api')
390 386
391 387 # Sanitize generic settings.
392 388 _list_setting(settings, 'default_encoding', 'UTF-8')
393 389 _bool_setting(settings, 'is_test', 'false')
394 390 _bool_setting(settings, 'gzip_responses', 'false')
395 391
396 392 # Call split out functions that sanitize settings for each topic.
397 393 _sanitize_appenlight_settings(settings)
398 394 _sanitize_vcs_settings(settings)
399 395 _sanitize_cache_settings(settings)
400 396
401 397 # configure instance id
402 398 config_utils.set_instance_id(settings)
403 399
404 400 return settings
405 401
406 402
407 403 def _sanitize_appenlight_settings(settings):
408 404 _bool_setting(settings, 'appenlight', 'false')
409 405
410 406
411 407 def _sanitize_vcs_settings(settings):
412 408 """
413 409 Applies settings defaults and does type conversion for all VCS related
414 410 settings.
415 411 """
416 412 _string_setting(settings, 'vcs.svn.compatible_version', '')
417 413 _string_setting(settings, 'git_rev_filter', '--all')
418 414 _string_setting(settings, 'vcs.hooks.protocol', 'http')
419 415 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
420 416 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
421 417 _string_setting(settings, 'vcs.server', '')
422 418 _string_setting(settings, 'vcs.server.log_level', 'debug')
423 419 _string_setting(settings, 'vcs.server.protocol', 'http')
424 420 _bool_setting(settings, 'startup.import_repos', 'false')
425 421 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
426 422 _bool_setting(settings, 'vcs.server.enable', 'true')
427 423 _bool_setting(settings, 'vcs.start_server', 'false')
428 424 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
429 425 _int_setting(settings, 'vcs.connection_timeout', 3600)
430 426
431 427 # Support legacy values of vcs.scm_app_implementation. Legacy
432 428 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
433 429 # which is now mapped to 'http'.
434 430 scm_app_impl = settings['vcs.scm_app_implementation']
435 431 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
436 432 settings['vcs.scm_app_implementation'] = 'http'
437 433
438 434
439 435 def _sanitize_cache_settings(settings):
440 436 _string_setting(settings, 'cache_dir',
441 437 os.path.join(tempfile.gettempdir(), 'rc_cache'))
442 438 # cache_perms
443 439 _string_setting(
444 440 settings,
445 441 'rc_cache.cache_perms.backend',
446 442 'dogpile.cache.rc.file_namespace')
447 443 _int_setting(
448 444 settings,
449 445 'rc_cache.cache_perms.expiration_time',
450 446 60)
451 447 _string_setting(
452 448 settings,
453 449 'rc_cache.cache_perms.arguments.filename',
454 450 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
455 451
456 452 # cache_repo
457 453 _string_setting(
458 454 settings,
459 455 'rc_cache.cache_repo.backend',
460 456 'dogpile.cache.rc.file_namespace')
461 457 _int_setting(
462 458 settings,
463 459 'rc_cache.cache_repo.expiration_time',
464 460 60)
465 461 _string_setting(
466 462 settings,
467 463 'rc_cache.cache_repo.arguments.filename',
468 464 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
469 465
470 466 # sql_cache_short
471 467 _string_setting(
472 468 settings,
473 469 'rc_cache.sql_cache_short.backend',
474 470 'dogpile.cache.rc.memory_lru')
475 471 _int_setting(
476 472 settings,
477 473 'rc_cache.sql_cache_short.expiration_time',
478 474 30)
479 475 _int_setting(
480 476 settings,
481 477 'rc_cache.sql_cache_short.max_size',
482 478 10000)
483 479
484 480
485 481 def _int_setting(settings, name, default):
486 482 settings[name] = int(settings.get(name, default))
487 483
488 484
489 485 def _bool_setting(settings, name, default):
490 486 input_val = settings.get(name, default)
491 487 if isinstance(input_val, unicode):
492 488 input_val = input_val.encode('utf8')
493 489 settings[name] = asbool(input_val)
494 490
495 491
496 492 def _list_setting(settings, name, default):
497 493 raw_value = settings.get(name, default)
498 494
499 495 old_separator = ','
500 496 if old_separator in raw_value:
501 497 # If we get a comma separated list, pass it to our own function.
502 498 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
503 499 else:
504 500 # Otherwise we assume it uses pyramids space/newline separation.
505 501 settings[name] = aslist(raw_value)
506 502
507 503
508 504 def _string_setting(settings, name, default, lower=True):
509 505 value = settings.get(name, default)
510 506 if lower:
511 507 value = value.lower()
512 508 settings[name] = value
513 509
514 510
515 511 def _substitute_values(mapping, substitutions):
516 512 result = {
517 513 # Note: Cannot use regular replacements, since they would clash
518 514 # with the implementation of ConfigParser. Using "format" instead.
519 515 key: value.format(**substitutions)
520 516 for key, value in mapping.items()
521 517 }
522 518 return result
@@ -1,84 +1,76 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 from pyramid.threadlocal import get_current_registry
21 from rhodecode.events.base import RhodecodeEvent
21 from rhodecode.events.base import RhodeCodeIntegrationEvent
22 22
23 23
24 24 log = logging.getLogger(__name__)
25 25
26 26
27 27 def trigger(event, registry=None):
28 28 """
29 29 Helper method to send an event. This wraps the pyramid logic to send an
30 30 event.
31 31 """
32 32 # For the first step we are using pyramids thread locals here. If the
33 33 # event mechanism works out as a good solution we should think about
34 34 # passing the registry as an argument to get rid of it.
35 35 registry = registry or get_current_registry()
36 36 registry.notify(event)
37 37 log.debug('event %s triggered using registry %s', event.__class__, registry)
38 38
39 # Until we can work around the problem that VCS operations do not have a
40 # pyramid context to work with, we send the events to integrations directly
41
42 # Later it will be possible to use regular pyramid subscribers ie:
43 # config.add_subscriber(
44 # 'rhodecode.integrations.integrations_event_handler',
45 # 'rhodecode.events.RhodecodeEvent')
46 # trigger(event, request.registry)
47
39 # Send the events to integrations directly
48 40 from rhodecode.integrations import integrations_event_handler
49 if isinstance(event, RhodecodeEvent):
41 if isinstance(event, RhodeCodeIntegrationEvent):
50 42 integrations_event_handler(event)
51 43
52 44
53 45 from rhodecode.events.user import ( # noqa
54 46 UserPreCreate,
55 47 UserPostCreate,
56 48 UserPreUpdate,
57 49 UserRegistered,
58 50 UserPermissionsChange,
59 51 )
60 52
61 53 from rhodecode.events.repo import ( # noqa
62 54 RepoEvent,
63 55 RepoPreCreateEvent, RepoCreateEvent,
64 56 RepoPreDeleteEvent, RepoDeleteEvent,
65 57 RepoPrePushEvent, RepoPushEvent,
66 58 RepoPrePullEvent, RepoPullEvent,
67 59 )
68 60
69 61 from rhodecode.events.repo_group import ( # noqa
70 62 RepoGroupEvent,
71 63 RepoGroupCreateEvent,
72 64 RepoGroupUpdateEvent,
73 65 RepoGroupDeleteEvent,
74 66 )
75 67
76 68 from rhodecode.events.pullrequest import ( # noqa
77 69 PullRequestEvent,
78 70 PullRequestCreateEvent,
79 71 PullRequestUpdateEvent,
80 72 PullRequestCommentEvent,
81 73 PullRequestReviewEvent,
82 74 PullRequestMergeEvent,
83 75 PullRequestCloseEvent,
84 76 )
@@ -1,112 +1,118 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19 import datetime
20 20
21 21 from zope.cachedescriptors.property import Lazy as LazyProperty
22 22 from pyramid.threadlocal import get_current_request
23 23
24 24 from rhodecode.lib.utils2 import AttributeDict
25 25
26 26
27 27 # this is a user object to be used for events caused by the system (eg. shell)
28 28 SYSTEM_USER = AttributeDict(dict(
29 29 username='__SYSTEM__',
30 30 user_id='__SYSTEM_ID__'
31 31 ))
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class RhodecodeEvent(object):
37 37 """
38 38 Base event class for all RhodeCode events
39 39 """
40 40 name = "RhodeCodeEvent"
41 41 no_url_set = '<no server_url available>'
42 42
43 43 def __init__(self, request=None):
44 44 self._request = request
45 45 self.utc_timestamp = datetime.datetime.utcnow()
46 46
47 47 def get_request(self):
48 48 if self._request:
49 49 return self._request
50 50 return get_current_request()
51 51
52 52 @LazyProperty
53 53 def request(self):
54 54 return self.get_request()
55 55
56 56 @property
57 57 def auth_user(self):
58 58 if not self.request:
59 59 return
60 60
61 61 user = getattr(self.request, 'user', None)
62 62 if user:
63 63 return user
64 64
65 65 api_user = getattr(self.request, 'rpc_user', None)
66 66 if api_user:
67 67 return api_user
68 68
69 69 @property
70 70 def actor(self):
71 71 auth_user = self.auth_user
72 72 if auth_user:
73 73 instance = auth_user.get_instance()
74 74 if not instance:
75 75 return AttributeDict(dict(
76 76 username=auth_user.username,
77 77 user_id=auth_user.user_id,
78 78 ))
79 79 return instance
80 80
81 81 return SYSTEM_USER
82 82
83 83 @property
84 84 def actor_ip(self):
85 85 auth_user = self.auth_user
86 86 if auth_user:
87 87 return auth_user.ip_addr
88 88 return '<no ip available>'
89 89
90 90 @property
91 91 def server_url(self):
92 92 if self.request:
93 93 try:
94 94 return self.request.route_url('home')
95 95 except Exception:
96 96 log.exception('Failed to fetch URL for server')
97 97 return self.no_url_set
98 98
99 99 return self.no_url_set
100 100
101 101 def as_dict(self):
102 102 data = {
103 103 'name': self.name,
104 104 'utc_timestamp': self.utc_timestamp,
105 105 'actor_ip': self.actor_ip,
106 106 'actor': {
107 107 'username': self.actor.username,
108 108 'user_id': self.actor.user_id
109 109 },
110 110 'server_url': self.server_url
111 111 }
112 112 return data
113
114
115 class RhodeCodeIntegrationEvent(RhodecodeEvent):
116 """
117 Special subclass for Integration events
118 """
@@ -1,356 +1,356 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import collections
20 20 import logging
21 21 import datetime
22 22
23 23 from rhodecode.translation import lazy_ugettext
24 24 from rhodecode.model.db import User, Repository, Session
25 from rhodecode.events.base import RhodecodeEvent
25 from rhodecode.events.base import RhodeCodeIntegrationEvent
26 26 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def _commits_as_dict(event, commit_ids, repos):
32 32 """
33 33 Helper function to serialize commit_ids
34 34
35 35 :param event: class calling this method
36 36 :param commit_ids: commits to get
37 37 :param repos: list of repos to check
38 38 """
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.lib.helpers import (
41 41 urlify_commit_message, process_patterns, chop_at_smart)
42 42 from rhodecode.model.repo import RepoModel
43 43
44 44 if not repos:
45 45 raise Exception('no repo defined')
46 46
47 47 if not isinstance(repos, (tuple, list)):
48 48 repos = [repos]
49 49
50 50 if not commit_ids:
51 51 return []
52 52
53 53 needed_commits = list(commit_ids)
54 54
55 55 commits = []
56 56 reviewers = []
57 57 for repo in repos:
58 58 if not needed_commits:
59 59 return commits # return early if we have the commits we need
60 60
61 61 vcs_repo = repo.scm_instance(cache=False)
62 62
63 63 try:
64 64 # use copy of needed_commits since we modify it while iterating
65 65 for commit_id in list(needed_commits):
66 66 if commit_id.startswith('tag=>'):
67 67 raw_id = commit_id[5:]
68 68 cs_data = {
69 69 'raw_id': commit_id, 'short_id': commit_id,
70 70 'branch': None,
71 71 'git_ref_change': 'tag_add',
72 72 'message': 'Added new tag {}'.format(raw_id),
73 73 'author': event.actor.full_contact,
74 74 'date': datetime.datetime.now(),
75 75 'refs': {
76 76 'branches': [],
77 77 'bookmarks': [],
78 78 'tags': []
79 79 }
80 80 }
81 81 commits.append(cs_data)
82 82
83 83 elif commit_id.startswith('delete_branch=>'):
84 84 raw_id = commit_id[15:]
85 85 cs_data = {
86 86 'raw_id': commit_id, 'short_id': commit_id,
87 87 'branch': None,
88 88 'git_ref_change': 'branch_delete',
89 89 'message': 'Deleted branch {}'.format(raw_id),
90 90 'author': event.actor.full_contact,
91 91 'date': datetime.datetime.now(),
92 92 'refs': {
93 93 'branches': [],
94 94 'bookmarks': [],
95 95 'tags': []
96 96 }
97 97 }
98 98 commits.append(cs_data)
99 99
100 100 else:
101 101 try:
102 102 cs = vcs_repo.get_changeset(commit_id)
103 103 except CommitDoesNotExistError:
104 104 continue # maybe its in next repo
105 105
106 106 cs_data = cs.__json__()
107 107 cs_data['refs'] = cs._get_refs()
108 108
109 109 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
110 110 cs_data['reviewers'] = reviewers
111 111 cs_data['url'] = RepoModel().get_commit_url(
112 112 repo, cs_data['raw_id'], request=event.request)
113 113 cs_data['permalink_url'] = RepoModel().get_commit_url(
114 114 repo, cs_data['raw_id'], request=event.request,
115 115 permalink=True)
116 116 urlified_message, issues_data = process_patterns(
117 117 cs_data['message'], repo.repo_name)
118 118 cs_data['issues'] = issues_data
119 119 cs_data['message_html'] = urlify_commit_message(
120 120 cs_data['message'], repo.repo_name)
121 121 cs_data['message_html_title'] = chop_at_smart(
122 122 cs_data['message'], '\n', suffix_if_chopped='...')
123 123 commits.append(cs_data)
124 124
125 125 needed_commits.remove(commit_id)
126 126
127 127 except Exception:
128 128 log.exception('Failed to extract commits data')
129 129 # we don't send any commits when crash happens, only full list
130 130 # matters we short circuit then.
131 131 return []
132 132
133 133 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
134 134 if missing_commits:
135 135 log.error('Inconsistent repository state. '
136 136 'Missing commits: %s' % ', '.join(missing_commits))
137 137
138 138 return commits
139 139
140 140
141 141 def _issues_as_dict(commits):
142 142 """ Helper function to serialize issues from commits """
143 143 issues = {}
144 144 for commit in commits:
145 145 for issue in commit['issues']:
146 146 issues[issue['id']] = issue
147 147 return issues
148 148
149 149
150 class RepoEvent(RhodecodeEvent):
150 class RepoEvent(RhodeCodeIntegrationEvent):
151 151 """
152 152 Base class for events acting on a repository.
153 153
154 154 :param repo: a :class:`Repository` instance
155 155 """
156 156
157 157 def __init__(self, repo):
158 158 super(RepoEvent, self).__init__()
159 159 self.repo = repo
160 160
161 161 def as_dict(self):
162 162 from rhodecode.model.repo import RepoModel
163 163 data = super(RepoEvent, self).as_dict()
164 164
165 165 extra_fields = collections.OrderedDict()
166 166 for field in self.repo.extra_fields:
167 167 extra_fields[field.field_key] = field.field_value
168 168
169 169 data.update({
170 170 'repo': {
171 171 'repo_id': self.repo.repo_id,
172 172 'repo_name': self.repo.repo_name,
173 173 'repo_type': self.repo.repo_type,
174 174 'url': RepoModel().get_url(
175 175 self.repo, request=self.request),
176 176 'permalink_url': RepoModel().get_url(
177 177 self.repo, request=self.request, permalink=True),
178 178 'extra_fields': extra_fields
179 179 }
180 180 })
181 181 return data
182 182
183 183
184 184 class RepoPreCreateEvent(RepoEvent):
185 185 """
186 186 An instance of this class is emitted as an :term:`event` before a repo is
187 187 created.
188 188 """
189 189 name = 'repo-pre-create'
190 190 display_name = lazy_ugettext('repository pre create')
191 191
192 192
193 193 class RepoCreateEvent(RepoEvent):
194 194 """
195 195 An instance of this class is emitted as an :term:`event` whenever a repo is
196 196 created.
197 197 """
198 198 name = 'repo-create'
199 199 display_name = lazy_ugettext('repository created')
200 200
201 201
202 202 class RepoPreDeleteEvent(RepoEvent):
203 203 """
204 204 An instance of this class is emitted as an :term:`event` whenever a repo is
205 205 created.
206 206 """
207 207 name = 'repo-pre-delete'
208 208 display_name = lazy_ugettext('repository pre delete')
209 209
210 210
211 211 class RepoDeleteEvent(RepoEvent):
212 212 """
213 213 An instance of this class is emitted as an :term:`event` whenever a repo is
214 214 created.
215 215 """
216 216 name = 'repo-delete'
217 217 display_name = lazy_ugettext('repository deleted')
218 218
219 219
220 220 class RepoVCSEvent(RepoEvent):
221 221 """
222 222 Base class for events triggered by the VCS
223 223 """
224 224 def __init__(self, repo_name, extras):
225 225 self.repo = Repository.get_by_repo_name(repo_name)
226 226 if not self.repo:
227 227 raise Exception('repo by this name %s does not exist' % repo_name)
228 228 self.extras = extras
229 229 super(RepoVCSEvent, self).__init__(self.repo)
230 230
231 231 @property
232 232 def actor(self):
233 233 if self.extras.get('username'):
234 234 return User.get_by_username(self.extras['username'])
235 235
236 236 @property
237 237 def actor_ip(self):
238 238 if self.extras.get('ip'):
239 239 return self.extras['ip']
240 240
241 241 @property
242 242 def server_url(self):
243 243 if self.extras.get('server_url'):
244 244 return self.extras['server_url']
245 245
246 246 @property
247 247 def request(self):
248 248 return self.extras.get('request') or self.get_request()
249 249
250 250
251 251 class RepoPrePullEvent(RepoVCSEvent):
252 252 """
253 253 An instance of this class is emitted as an :term:`event` before commits
254 254 are pulled from a repo.
255 255 """
256 256 name = 'repo-pre-pull'
257 257 display_name = lazy_ugettext('repository pre pull')
258 258
259 259
260 260 class RepoPullEvent(RepoVCSEvent):
261 261 """
262 262 An instance of this class is emitted as an :term:`event` after commits
263 263 are pulled from a repo.
264 264 """
265 265 name = 'repo-pull'
266 266 display_name = lazy_ugettext('repository pull')
267 267
268 268
269 269 class RepoPrePushEvent(RepoVCSEvent):
270 270 """
271 271 An instance of this class is emitted as an :term:`event` before commits
272 272 are pushed to a repo.
273 273 """
274 274 name = 'repo-pre-push'
275 275 display_name = lazy_ugettext('repository pre push')
276 276
277 277
278 278 class RepoPushEvent(RepoVCSEvent):
279 279 """
280 280 An instance of this class is emitted as an :term:`event` after commits
281 281 are pushed to a repo.
282 282
283 283 :param extras: (optional) dict of data from proxied VCS actions
284 284 """
285 285 name = 'repo-push'
286 286 display_name = lazy_ugettext('repository push')
287 287
288 288 def __init__(self, repo_name, pushed_commit_ids, extras):
289 289 super(RepoPushEvent, self).__init__(repo_name, extras)
290 290 self.pushed_commit_ids = pushed_commit_ids
291 291 self.new_refs = extras.new_refs
292 292
293 293 def as_dict(self):
294 294 data = super(RepoPushEvent, self).as_dict()
295 295
296 296 def branch_url(branch_name):
297 297 return '{}/changelog?branch={}'.format(
298 298 data['repo']['url'], branch_name)
299 299
300 300 def tag_url(tag_name):
301 301 return '{}/files/{}/'.format(
302 302 data['repo']['url'], tag_name)
303 303
304 304 commits = _commits_as_dict(
305 305 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
306 306
307 307 last_branch = None
308 308 for commit in reversed(commits):
309 309 commit['branch'] = commit['branch'] or last_branch
310 310 last_branch = commit['branch']
311 311 issues = _issues_as_dict(commits)
312 312
313 313 branches = set()
314 314 tags = set()
315 315 for commit in commits:
316 316 if commit['refs']['tags']:
317 317 for tag in commit['refs']['tags']:
318 318 tags.add(tag)
319 319 if commit['branch']:
320 320 branches.add(commit['branch'])
321 321
322 322 # maybe we have branches in new_refs ?
323 323 try:
324 324 branches = branches.union(set(self.new_refs['branches']))
325 325 except Exception:
326 326 pass
327 327
328 328 branches = [
329 329 {
330 330 'name': branch,
331 331 'url': branch_url(branch)
332 332 }
333 333 for branch in branches
334 334 ]
335 335
336 336 # maybe we have branches in new_refs ?
337 337 try:
338 338 tags = tags.union(set(self.new_refs['tags']))
339 339 except Exception:
340 340 pass
341 341
342 342 tags = [
343 343 {
344 344 'name': tag,
345 345 'url': tag_url(tag)
346 346 }
347 347 for tag in tags
348 348 ]
349 349
350 350 data['push'] = {
351 351 'commits': commits,
352 352 'issues': issues,
353 353 'branches': branches,
354 354 'tags': tags,
355 355 }
356 356 return data
@@ -1,80 +1,80 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.base import RhodecodeEvent
22 from rhodecode.events.base import RhodeCodeIntegrationEvent
23 23
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27
28 class RepoGroupEvent(RhodecodeEvent):
28 class RepoGroupEvent(RhodeCodeIntegrationEvent):
29 29 """
30 30 Base class for events acting on a repository group.
31 31
32 32 :param repo: a :class:`RepositoryGroup` instance
33 33 """
34 34
35 35 def __init__(self, repo_group):
36 36 super(RepoGroupEvent, self).__init__()
37 37 self.repo_group = repo_group
38 38
39 39 def as_dict(self):
40 40 data = super(RepoGroupEvent, self).as_dict()
41 41 data.update({
42 42 'repo_group': {
43 43 'group_id': self.repo_group.group_id,
44 44 'group_name': self.repo_group.group_name,
45 45 'group_parent_id': self.repo_group.group_parent_id,
46 46 'group_description': self.repo_group.group_description,
47 47 'user_id': self.repo_group.user_id,
48 48 'created_by': self.repo_group.user.username,
49 49 'created_on': self.repo_group.created_on,
50 50 'enable_locking': self.repo_group.enable_locking,
51 51 }
52 52 })
53 53 return data
54 54
55 55
56 56 class RepoGroupCreateEvent(RepoGroupEvent):
57 57 """
58 58 An instance of this class is emitted as an :term:`event` whenever a
59 59 repository group is created.
60 60 """
61 61 name = 'repo-group-create'
62 62 display_name = lazy_ugettext('repository group created')
63 63
64 64
65 65 class RepoGroupDeleteEvent(RepoGroupEvent):
66 66 """
67 67 An instance of this class is emitted as an :term:`event` whenever a
68 68 repository group is deleted.
69 69 """
70 70 name = 'repo-group-delete'
71 71 display_name = lazy_ugettext('repository group deleted')
72 72
73 73
74 74 class RepoGroupUpdateEvent(RepoGroupEvent):
75 75 """
76 76 An instance of this class is emitted as an :term:`event` whenever a
77 77 repository group is updated.
78 78 """
79 79 name = 'repo-group-update'
80 80 display_name = lazy_ugettext('repository group update')
@@ -1,104 +1,104 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20 20 from zope.interface import implementer
21 21
22 22 from rhodecode.translation import lazy_ugettext
23 from rhodecode.events.base import RhodecodeEvent
23 from rhodecode.events.base import RhodecodeEvent, RhodeCodeIntegrationEvent
24 24 from rhodecode.events.interfaces import (
25 25 IUserRegistered, IUserPreCreate, IUserPreUpdate)
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 @implementer(IUserRegistered)
31 class UserRegistered(RhodecodeEvent):
31 class UserRegistered(RhodeCodeIntegrationEvent):
32 32 """
33 33 An instance of this class is emitted as an :term:`event` whenever a user
34 34 account is registered.
35 35 """
36 36 name = 'user-register'
37 37 display_name = lazy_ugettext('user registered')
38 38
39 39 def __init__(self, user, session):
40 40 super(UserRegistered, self).__init__()
41 41 self.user = user
42 42 self.session = session
43 43
44 44
45 45 @implementer(IUserPreCreate)
46 class UserPreCreate(RhodecodeEvent):
46 class UserPreCreate(RhodeCodeIntegrationEvent):
47 47 """
48 48 An instance of this class is emitted as an :term:`event` before a new user
49 49 object is created.
50 50 """
51 51 name = 'user-pre-create'
52 52 display_name = lazy_ugettext('user pre create')
53 53
54 54 def __init__(self, user_data):
55 55 super(UserPreCreate, self).__init__()
56 56 self.user_data = user_data
57 57
58 58
59 59 @implementer(IUserPreCreate)
60 class UserPostCreate(RhodecodeEvent):
60 class UserPostCreate(RhodeCodeIntegrationEvent):
61 61 """
62 62 An instance of this class is emitted as an :term:`event` after a new user
63 63 object is created.
64 64 """
65 65 name = 'user-post-create'
66 66 display_name = lazy_ugettext('user post create')
67 67
68 68 def __init__(self, user_data):
69 69 super(UserPostCreate, self).__init__()
70 70 self.user_data = user_data
71 71
72 72
73 73 @implementer(IUserPreUpdate)
74 class UserPreUpdate(RhodecodeEvent):
74 class UserPreUpdate(RhodeCodeIntegrationEvent):
75 75 """
76 76 An instance of this class is emitted as an :term:`event` before a user
77 77 object is updated.
78 78 """
79 79 name = 'user-pre-update'
80 80 display_name = lazy_ugettext('user pre update')
81 81
82 82 def __init__(self, user, user_data):
83 83 super(UserPreUpdate, self).__init__()
84 84 self.user = user
85 85 self.user_data = user_data
86 86
87 87
88 88 class UserPermissionsChange(RhodecodeEvent):
89 89 """
90 90 This event should be triggered on an event that permissions of user might changed.
91 91 Currently this should be triggered on:
92 92
93 93 - user added/removed from user group
94 94 - repo permissions changed
95 95 - repo group permissions changed
96 96 - user group permissions changed
97 97
98 98 """
99 99 name = 'user-permissions-change'
100 100 display_name = lazy_ugettext('user permissions change')
101 101
102 102 def __init__(self, user_ids):
103 103 super(UserPermissionsChange, self).__init__()
104 104 self.user_ids = user_ids
General Comments 0
You need to be logged in to leave comments. Login now