##// END OF EJS Templates
integrations: refactor/cleanup + features, fixes #4181...
dan -
r731:7a6d3636 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
9
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
13
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
18 log = logging.getLogger(__name__)
19
20
21 def upgrade(migrate_engine):
22 """
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_4_0_1
28
29 tbl = db_4_4_0_1.Integration.__table__
30 child_repos_only = db_4_4_0_1.Integration.child_repos_only
31 child_repos_only.create(table=tbl)
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,62 +1,62 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 """
22 22
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 56 # defines current db version for migrations
54 __dbversion__ = 57 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'http://rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
@@ -1,1159 +1,1160 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
45 46 # repo names can have a slash in them, but they must not end with a slash
46 47 'repo_name': r'.*?[^/]',
47 48 # file path eats up everything at the end
48 49 'f_path': r'.*',
49 50 # reference types
50 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
51 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
52 53 }
53 54
54 55
55 56 def add_route_requirements(route_path, requirements):
56 57 """
57 58 Adds regex requirements to pyramid routes using a mapping dict
58 59
59 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
60 61 '/{action}/{id:\d+}'
61 62
62 63 """
63 64 for key, regex in requirements.items():
64 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
65 66 return route_path
66 67
67 68
68 69 class JSRoutesMapper(Mapper):
69 70 """
70 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
71 72 """
72 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
73 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
74 75 def __init__(self, *args, **kw):
75 76 super(JSRoutesMapper, self).__init__(*args, **kw)
76 77 self._jsroutes = []
77 78
78 79 def connect(self, *args, **kw):
79 80 """
80 81 Wrapper for connect to take an extra argument jsroute=True
81 82
82 83 :param jsroute: boolean, if True will add the route to the pyroutes list
83 84 """
84 85 if kw.pop('jsroute', False):
85 86 if not self._named_route_regex.match(args[0]):
86 87 raise Exception('only named routes can be added to pyroutes')
87 88 self._jsroutes.append(args[0])
88 89
89 90 super(JSRoutesMapper, self).connect(*args, **kw)
90 91
91 92 def _extract_route_information(self, route):
92 93 """
93 94 Convert a route into tuple(name, path, args), eg:
94 95 ('user_profile', '/profile/%(username)s', ['username'])
95 96 """
96 97 routepath = route.routepath
97 98 def replace(matchobj):
98 99 if matchobj.group(1):
99 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
100 101 else:
101 102 return "%%(%s)s" % matchobj.group(2)
102 103
103 104 routepath = self._argument_prog.sub(replace, routepath)
104 105 return (
105 106 route.name,
106 107 routepath,
107 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
108 109 for arg in self._argument_prog.findall(route.routepath)]
109 110 )
110 111
111 112 def jsroutes(self):
112 113 """
113 114 Return a list of pyroutes.js compatible routes
114 115 """
115 116 for route_name in self._jsroutes:
116 117 yield self._extract_route_information(self._routenames[route_name])
117 118
118 119
119 120 def make_map(config):
120 121 """Create, configure and return the routes Mapper"""
121 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
122 123 always_scan=config['debug'])
123 124 rmap.minimization = False
124 125 rmap.explicit = False
125 126
126 127 from rhodecode.lib.utils2 import str2bool
127 128 from rhodecode.model import repo, repo_group
128 129
129 130 def check_repo(environ, match_dict):
130 131 """
131 132 check for valid repository for proper 404 handling
132 133
133 134 :param environ:
134 135 :param match_dict:
135 136 """
136 137 repo_name = match_dict.get('repo_name')
137 138
138 139 if match_dict.get('f_path'):
139 140 # fix for multiple initial slashes that causes errors
140 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 142 repo_model = repo.RepoModel()
142 143 by_name_match = repo_model.get_by_repo_name(repo_name)
143 144 # if we match quickly from database, short circuit the operation,
144 145 # and validate repo based on the type.
145 146 if by_name_match:
146 147 return True
147 148
148 149 by_id_match = repo_model.get_repo_by_id(repo_name)
149 150 if by_id_match:
150 151 repo_name = by_id_match.repo_name
151 152 match_dict['repo_name'] = repo_name
152 153 return True
153 154
154 155 return False
155 156
156 157 def check_group(environ, match_dict):
157 158 """
158 159 check for valid repository group path for proper 404 handling
159 160
160 161 :param environ:
161 162 :param match_dict:
162 163 """
163 164 repo_group_name = match_dict.get('group_name')
164 165 repo_group_model = repo_group.RepoGroupModel()
165 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 167 if by_name_match:
167 168 return True
168 169
169 170 return False
170 171
171 172 def check_user_group(environ, match_dict):
172 173 """
173 174 check for valid user group for proper 404 handling
174 175
175 176 :param environ:
176 177 :param match_dict:
177 178 """
178 179 return True
179 180
180 181 def check_int(environ, match_dict):
181 182 return match_dict.get('id').isdigit()
182 183
183 184
184 185 #==========================================================================
185 186 # CUSTOM ROUTES HERE
186 187 #==========================================================================
187 188
188 189 # MAIN PAGE
189 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
190 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
191 192 action='goto_switcher_data')
192 193 rmap.connect('repo_list_data', '/_repos', controller='home',
193 194 action='repo_list_data')
194 195
195 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
196 197 action='user_autocomplete_data', jsroute=True)
197 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
198 199 action='user_group_autocomplete_data')
199 200
200 201 rmap.connect(
201 202 'user_profile', '/_profiles/{username}', controller='users',
202 203 action='user_profile')
203 204
204 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
205 206 rmap.connect('rst_help',
206 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
207 208 _static=True)
208 209 rmap.connect('markdown_help',
209 210 'http://daringfireball.net/projects/markdown/syntax',
210 211 _static=True)
211 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
212 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
213 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
214 215 # TODO: anderson - making this a static link since redirect won't play
215 216 # nice with POST requests
216 217 rmap.connect('enterprise_license_convert_from_old',
217 218 'https://rhodecode.com/u/license-upgrade',
218 219 _static=True)
219 220
220 221 routing_links.connect_redirection_links(rmap)
221 222
222 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
223 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
224 225
225 226 # ADMIN REPOSITORY ROUTES
226 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
227 228 controller='admin/repos') as m:
228 229 m.connect('repos', '/repos',
229 230 action='create', conditions={'method': ['POST']})
230 231 m.connect('repos', '/repos',
231 232 action='index', conditions={'method': ['GET']})
232 233 m.connect('new_repo', '/create_repository', jsroute=True,
233 234 action='create_repository', conditions={'method': ['GET']})
234 235 m.connect('/repos/{repo_name}',
235 236 action='update', conditions={'method': ['PUT'],
236 237 'function': check_repo},
237 238 requirements=URL_NAME_REQUIREMENTS)
238 239 m.connect('delete_repo', '/repos/{repo_name}',
239 240 action='delete', conditions={'method': ['DELETE']},
240 241 requirements=URL_NAME_REQUIREMENTS)
241 242 m.connect('repo', '/repos/{repo_name}',
242 243 action='show', conditions={'method': ['GET'],
243 244 'function': check_repo},
244 245 requirements=URL_NAME_REQUIREMENTS)
245 246
246 247 # ADMIN REPOSITORY GROUPS ROUTES
247 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
248 249 controller='admin/repo_groups') as m:
249 250 m.connect('repo_groups', '/repo_groups',
250 251 action='create', conditions={'method': ['POST']})
251 252 m.connect('repo_groups', '/repo_groups',
252 253 action='index', conditions={'method': ['GET']})
253 254 m.connect('new_repo_group', '/repo_groups/new',
254 255 action='new', conditions={'method': ['GET']})
255 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
256 257 action='update', conditions={'method': ['PUT'],
257 258 'function': check_group},
258 259 requirements=URL_NAME_REQUIREMENTS)
259 260
260 261 # EXTRAS REPO GROUP ROUTES
261 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
262 263 action='edit',
263 264 conditions={'method': ['GET'], 'function': check_group},
264 265 requirements=URL_NAME_REQUIREMENTS)
265 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
266 267 action='edit',
267 268 conditions={'method': ['PUT'], 'function': check_group},
268 269 requirements=URL_NAME_REQUIREMENTS)
269 270
270 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
271 272 action='edit_repo_group_advanced',
272 273 conditions={'method': ['GET'], 'function': check_group},
273 274 requirements=URL_NAME_REQUIREMENTS)
274 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
275 276 action='edit_repo_group_advanced',
276 277 conditions={'method': ['PUT'], 'function': check_group},
277 278 requirements=URL_NAME_REQUIREMENTS)
278 279
279 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
280 281 action='edit_repo_group_perms',
281 282 conditions={'method': ['GET'], 'function': check_group},
282 283 requirements=URL_NAME_REQUIREMENTS)
283 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
284 285 action='update_perms',
285 286 conditions={'method': ['PUT'], 'function': check_group},
286 287 requirements=URL_NAME_REQUIREMENTS)
287 288
288 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
289 290 action='delete', conditions={'method': ['DELETE'],
290 291 'function': check_group},
291 292 requirements=URL_NAME_REQUIREMENTS)
292 293
293 294 # ADMIN USER ROUTES
294 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
295 296 controller='admin/users') as m:
296 297 m.connect('users', '/users',
297 298 action='create', conditions={'method': ['POST']})
298 299 m.connect('users', '/users',
299 300 action='index', conditions={'method': ['GET']})
300 301 m.connect('new_user', '/users/new',
301 302 action='new', conditions={'method': ['GET']})
302 303 m.connect('update_user', '/users/{user_id}',
303 304 action='update', conditions={'method': ['PUT']})
304 305 m.connect('delete_user', '/users/{user_id}',
305 306 action='delete', conditions={'method': ['DELETE']})
306 307 m.connect('edit_user', '/users/{user_id}/edit',
307 308 action='edit', conditions={'method': ['GET']})
308 309 m.connect('user', '/users/{user_id}',
309 310 action='show', conditions={'method': ['GET']})
310 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
311 312 action='reset_password', conditions={'method': ['POST']})
312 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
313 314 action='create_personal_repo_group', conditions={'method': ['POST']})
314 315
315 316 # EXTRAS USER ROUTES
316 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
317 318 action='edit_advanced', conditions={'method': ['GET']})
318 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
319 320 action='update_advanced', conditions={'method': ['PUT']})
320 321
321 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
322 323 action='edit_auth_tokens', conditions={'method': ['GET']})
323 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
324 325 action='add_auth_token', conditions={'method': ['PUT']})
325 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
326 327 action='delete_auth_token', conditions={'method': ['DELETE']})
327 328
328 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
329 330 action='edit_global_perms', conditions={'method': ['GET']})
330 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
331 332 action='update_global_perms', conditions={'method': ['PUT']})
332 333
333 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
334 335 action='edit_perms_summary', conditions={'method': ['GET']})
335 336
336 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
337 338 action='edit_emails', conditions={'method': ['GET']})
338 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
339 340 action='add_email', conditions={'method': ['PUT']})
340 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
341 342 action='delete_email', conditions={'method': ['DELETE']})
342 343
343 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
344 345 action='edit_ips', conditions={'method': ['GET']})
345 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
346 347 action='add_ip', conditions={'method': ['PUT']})
347 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
348 349 action='delete_ip', conditions={'method': ['DELETE']})
349 350
350 351 # ADMIN USER GROUPS REST ROUTES
351 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
352 353 controller='admin/user_groups') as m:
353 354 m.connect('users_groups', '/user_groups',
354 355 action='create', conditions={'method': ['POST']})
355 356 m.connect('users_groups', '/user_groups',
356 357 action='index', conditions={'method': ['GET']})
357 358 m.connect('new_users_group', '/user_groups/new',
358 359 action='new', conditions={'method': ['GET']})
359 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
360 361 action='update', conditions={'method': ['PUT']})
361 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
362 363 action='delete', conditions={'method': ['DELETE']})
363 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
364 365 action='edit', conditions={'method': ['GET']},
365 366 function=check_user_group)
366 367
367 368 # EXTRAS USER GROUP ROUTES
368 369 m.connect('edit_user_group_global_perms',
369 370 '/user_groups/{user_group_id}/edit/global_permissions',
370 371 action='edit_global_perms', conditions={'method': ['GET']})
371 372 m.connect('edit_user_group_global_perms',
372 373 '/user_groups/{user_group_id}/edit/global_permissions',
373 374 action='update_global_perms', conditions={'method': ['PUT']})
374 375 m.connect('edit_user_group_perms_summary',
375 376 '/user_groups/{user_group_id}/edit/permissions_summary',
376 377 action='edit_perms_summary', conditions={'method': ['GET']})
377 378
378 379 m.connect('edit_user_group_perms',
379 380 '/user_groups/{user_group_id}/edit/permissions',
380 381 action='edit_perms', conditions={'method': ['GET']})
381 382 m.connect('edit_user_group_perms',
382 383 '/user_groups/{user_group_id}/edit/permissions',
383 384 action='update_perms', conditions={'method': ['PUT']})
384 385
385 386 m.connect('edit_user_group_advanced',
386 387 '/user_groups/{user_group_id}/edit/advanced',
387 388 action='edit_advanced', conditions={'method': ['GET']})
388 389
389 390 m.connect('edit_user_group_members',
390 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
391 392 action='edit_members', conditions={'method': ['GET']})
392 393
393 394 # ADMIN PERMISSIONS ROUTES
394 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
395 396 controller='admin/permissions') as m:
396 397 m.connect('admin_permissions_application', '/permissions/application',
397 398 action='permission_application_update', conditions={'method': ['POST']})
398 399 m.connect('admin_permissions_application', '/permissions/application',
399 400 action='permission_application', conditions={'method': ['GET']})
400 401
401 402 m.connect('admin_permissions_global', '/permissions/global',
402 403 action='permission_global_update', conditions={'method': ['POST']})
403 404 m.connect('admin_permissions_global', '/permissions/global',
404 405 action='permission_global', conditions={'method': ['GET']})
405 406
406 407 m.connect('admin_permissions_object', '/permissions/object',
407 408 action='permission_objects_update', conditions={'method': ['POST']})
408 409 m.connect('admin_permissions_object', '/permissions/object',
409 410 action='permission_objects', conditions={'method': ['GET']})
410 411
411 412 m.connect('admin_permissions_ips', '/permissions/ips',
412 413 action='permission_ips', conditions={'method': ['POST']})
413 414 m.connect('admin_permissions_ips', '/permissions/ips',
414 415 action='permission_ips', conditions={'method': ['GET']})
415 416
416 417 m.connect('admin_permissions_overview', '/permissions/overview',
417 418 action='permission_perms', conditions={'method': ['GET']})
418 419
419 420 # ADMIN DEFAULTS REST ROUTES
420 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
421 422 controller='admin/defaults') as m:
422 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
423 424 action='update_repository_defaults', conditions={'method': ['POST']})
424 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
425 426 action='index', conditions={'method': ['GET']})
426 427
427 428 # ADMIN DEBUG STYLE ROUTES
428 429 if str2bool(config.get('debug_style')):
429 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
430 431 controller='debug_style') as m:
431 432 m.connect('debug_style_home', '',
432 433 action='index', conditions={'method': ['GET']})
433 434 m.connect('debug_style_template', '/t/{t_path}',
434 435 action='template', conditions={'method': ['GET']})
435 436
436 437 # ADMIN SETTINGS ROUTES
437 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
438 439 controller='admin/settings') as m:
439 440
440 441 # default
441 442 m.connect('admin_settings', '/settings',
442 443 action='settings_global_update',
443 444 conditions={'method': ['POST']})
444 445 m.connect('admin_settings', '/settings',
445 446 action='settings_global', conditions={'method': ['GET']})
446 447
447 448 m.connect('admin_settings_vcs', '/settings/vcs',
448 449 action='settings_vcs_update',
449 450 conditions={'method': ['POST']})
450 451 m.connect('admin_settings_vcs', '/settings/vcs',
451 452 action='settings_vcs',
452 453 conditions={'method': ['GET']})
453 454 m.connect('admin_settings_vcs', '/settings/vcs',
454 455 action='delete_svn_pattern',
455 456 conditions={'method': ['DELETE']})
456 457
457 458 m.connect('admin_settings_mapping', '/settings/mapping',
458 459 action='settings_mapping_update',
459 460 conditions={'method': ['POST']})
460 461 m.connect('admin_settings_mapping', '/settings/mapping',
461 462 action='settings_mapping', conditions={'method': ['GET']})
462 463
463 464 m.connect('admin_settings_global', '/settings/global',
464 465 action='settings_global_update',
465 466 conditions={'method': ['POST']})
466 467 m.connect('admin_settings_global', '/settings/global',
467 468 action='settings_global', conditions={'method': ['GET']})
468 469
469 470 m.connect('admin_settings_visual', '/settings/visual',
470 471 action='settings_visual_update',
471 472 conditions={'method': ['POST']})
472 473 m.connect('admin_settings_visual', '/settings/visual',
473 474 action='settings_visual', conditions={'method': ['GET']})
474 475
475 476 m.connect('admin_settings_issuetracker',
476 477 '/settings/issue-tracker', action='settings_issuetracker',
477 478 conditions={'method': ['GET']})
478 479 m.connect('admin_settings_issuetracker_save',
479 480 '/settings/issue-tracker/save',
480 481 action='settings_issuetracker_save',
481 482 conditions={'method': ['POST']})
482 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
483 484 action='settings_issuetracker_test',
484 485 conditions={'method': ['POST']})
485 486 m.connect('admin_issuetracker_delete',
486 487 '/settings/issue-tracker/delete',
487 488 action='settings_issuetracker_delete',
488 489 conditions={'method': ['DELETE']})
489 490
490 491 m.connect('admin_settings_email', '/settings/email',
491 492 action='settings_email_update',
492 493 conditions={'method': ['POST']})
493 494 m.connect('admin_settings_email', '/settings/email',
494 495 action='settings_email', conditions={'method': ['GET']})
495 496
496 497 m.connect('admin_settings_hooks', '/settings/hooks',
497 498 action='settings_hooks_update',
498 499 conditions={'method': ['POST', 'DELETE']})
499 500 m.connect('admin_settings_hooks', '/settings/hooks',
500 501 action='settings_hooks', conditions={'method': ['GET']})
501 502
502 503 m.connect('admin_settings_search', '/settings/search',
503 504 action='settings_search', conditions={'method': ['GET']})
504 505
505 506 m.connect('admin_settings_system', '/settings/system',
506 507 action='settings_system', conditions={'method': ['GET']})
507 508
508 509 m.connect('admin_settings_system_update', '/settings/system/updates',
509 510 action='settings_system_update', conditions={'method': ['GET']})
510 511
511 512 m.connect('admin_settings_supervisor', '/settings/supervisor',
512 513 action='settings_supervisor', conditions={'method': ['GET']})
513 514 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
514 515 action='settings_supervisor_log', conditions={'method': ['GET']})
515 516
516 517 m.connect('admin_settings_labs', '/settings/labs',
517 518 action='settings_labs_update',
518 519 conditions={'method': ['POST']})
519 520 m.connect('admin_settings_labs', '/settings/labs',
520 521 action='settings_labs', conditions={'method': ['GET']})
521 522
522 523 # ADMIN MY ACCOUNT
523 524 with rmap.submapper(path_prefix=ADMIN_PREFIX,
524 525 controller='admin/my_account') as m:
525 526
526 527 m.connect('my_account', '/my_account',
527 528 action='my_account', conditions={'method': ['GET']})
528 529 m.connect('my_account_edit', '/my_account/edit',
529 530 action='my_account_edit', conditions={'method': ['GET']})
530 531 m.connect('my_account', '/my_account',
531 532 action='my_account_update', conditions={'method': ['POST']})
532 533
533 534 m.connect('my_account_password', '/my_account/password',
534 535 action='my_account_password', conditions={'method': ['GET', 'POST']})
535 536
536 537 m.connect('my_account_repos', '/my_account/repos',
537 538 action='my_account_repos', conditions={'method': ['GET']})
538 539
539 540 m.connect('my_account_watched', '/my_account/watched',
540 541 action='my_account_watched', conditions={'method': ['GET']})
541 542
542 543 m.connect('my_account_pullrequests', '/my_account/pull_requests',
543 544 action='my_account_pullrequests', conditions={'method': ['GET']})
544 545
545 546 m.connect('my_account_perms', '/my_account/perms',
546 547 action='my_account_perms', conditions={'method': ['GET']})
547 548
548 549 m.connect('my_account_emails', '/my_account/emails',
549 550 action='my_account_emails', conditions={'method': ['GET']})
550 551 m.connect('my_account_emails', '/my_account/emails',
551 552 action='my_account_emails_add', conditions={'method': ['POST']})
552 553 m.connect('my_account_emails', '/my_account/emails',
553 554 action='my_account_emails_delete', conditions={'method': ['DELETE']})
554 555
555 556 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
556 557 action='my_account_auth_tokens', conditions={'method': ['GET']})
557 558 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
558 559 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
559 560 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
560 561 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
561 562 m.connect('my_account_notifications', '/my_account/notifications',
562 563 action='my_notifications',
563 564 conditions={'method': ['GET']})
564 565 m.connect('my_account_notifications_toggle_visibility',
565 566 '/my_account/toggle_visibility',
566 567 action='my_notifications_toggle_visibility',
567 568 conditions={'method': ['POST']})
568 569
569 570 # NOTIFICATION REST ROUTES
570 571 with rmap.submapper(path_prefix=ADMIN_PREFIX,
571 572 controller='admin/notifications') as m:
572 573 m.connect('notifications', '/notifications',
573 574 action='index', conditions={'method': ['GET']})
574 575 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
575 576 action='mark_all_read', conditions={'method': ['POST']})
576 577 m.connect('/notifications/{notification_id}',
577 578 action='update', conditions={'method': ['PUT']})
578 579 m.connect('/notifications/{notification_id}',
579 580 action='delete', conditions={'method': ['DELETE']})
580 581 m.connect('notification', '/notifications/{notification_id}',
581 582 action='show', conditions={'method': ['GET']})
582 583
583 584 # ADMIN GIST
584 585 with rmap.submapper(path_prefix=ADMIN_PREFIX,
585 586 controller='admin/gists') as m:
586 587 m.connect('gists', '/gists',
587 588 action='create', conditions={'method': ['POST']})
588 589 m.connect('gists', '/gists', jsroute=True,
589 590 action='index', conditions={'method': ['GET']})
590 591 m.connect('new_gist', '/gists/new', jsroute=True,
591 592 action='new', conditions={'method': ['GET']})
592 593
593 594 m.connect('/gists/{gist_id}',
594 595 action='delete', conditions={'method': ['DELETE']})
595 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
596 597 action='edit_form', conditions={'method': ['GET']})
597 598 m.connect('edit_gist', '/gists/{gist_id}/edit',
598 599 action='edit', conditions={'method': ['POST']})
599 600 m.connect(
600 601 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
601 602 action='check_revision', conditions={'method': ['GET']})
602 603
603 604 m.connect('gist', '/gists/{gist_id}',
604 605 action='show', conditions={'method': ['GET']})
605 606 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
606 607 revision='tip',
607 608 action='show', conditions={'method': ['GET']})
608 609 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
609 610 revision='tip',
610 611 action='show', conditions={'method': ['GET']})
611 612 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
612 613 revision='tip',
613 614 action='show', conditions={'method': ['GET']},
614 615 requirements=URL_NAME_REQUIREMENTS)
615 616
616 617 # ADMIN MAIN PAGES
617 618 with rmap.submapper(path_prefix=ADMIN_PREFIX,
618 619 controller='admin/admin') as m:
619 620 m.connect('admin_home', '', action='index')
620 621 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
621 622 action='add_repo')
622 623 m.connect(
623 624 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
624 625 action='pull_requests')
625 626 m.connect(
626 627 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
627 628 action='pull_requests')
628 629
629 630
630 631 # USER JOURNAL
631 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
632 633 controller='journal', action='index')
633 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
634 635 controller='journal', action='journal_rss')
635 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
636 637 controller='journal', action='journal_atom')
637 638
638 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
639 640 controller='journal', action='public_journal')
640 641
641 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
642 643 controller='journal', action='public_journal_rss')
643 644
644 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
645 646 controller='journal', action='public_journal_rss')
646 647
647 648 rmap.connect('public_journal_atom',
648 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
649 650 action='public_journal_atom')
650 651
651 652 rmap.connect('public_journal_atom_old',
652 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
653 654 action='public_journal_atom')
654 655
655 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
656 657 controller='journal', action='toggle_following', jsroute=True,
657 658 conditions={'method': ['POST']})
658 659
659 660 # FULL TEXT SEARCH
660 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
661 662 controller='search')
662 663 rmap.connect('search_repo_home', '/{repo_name}/search',
663 664 controller='search',
664 665 action='index',
665 666 conditions={'function': check_repo},
666 667 requirements=URL_NAME_REQUIREMENTS)
667 668
668 669 # FEEDS
669 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
670 671 controller='feed', action='rss',
671 672 conditions={'function': check_repo},
672 673 requirements=URL_NAME_REQUIREMENTS)
673 674
674 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
675 676 controller='feed', action='atom',
676 677 conditions={'function': check_repo},
677 678 requirements=URL_NAME_REQUIREMENTS)
678 679
679 680 #==========================================================================
680 681 # REPOSITORY ROUTES
681 682 #==========================================================================
682 683
683 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
684 685 controller='admin/repos', action='repo_creating',
685 686 requirements=URL_NAME_REQUIREMENTS)
686 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
687 688 controller='admin/repos', action='repo_check',
688 689 requirements=URL_NAME_REQUIREMENTS)
689 690
690 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
691 692 controller='summary', action='repo_stats',
692 693 conditions={'function': check_repo},
693 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
694 695
695 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
696 697 controller='summary', action='repo_refs_data', jsroute=True,
697 698 requirements=URL_NAME_REQUIREMENTS)
698 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
699 700 controller='summary', action='repo_refs_changelog_data',
700 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
701 702
702 703 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
703 704 controller='changeset', revision='tip', jsroute=True,
704 705 conditions={'function': check_repo},
705 706 requirements=URL_NAME_REQUIREMENTS)
706 707 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
707 708 controller='changeset', revision='tip', action='changeset_children',
708 709 conditions={'function': check_repo},
709 710 requirements=URL_NAME_REQUIREMENTS)
710 711 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
711 712 controller='changeset', revision='tip', action='changeset_parents',
712 713 conditions={'function': check_repo},
713 714 requirements=URL_NAME_REQUIREMENTS)
714 715
715 716 # repo edit options
716 717 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
717 718 controller='admin/repos', action='edit',
718 719 conditions={'method': ['GET'], 'function': check_repo},
719 720 requirements=URL_NAME_REQUIREMENTS)
720 721
721 722 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
722 723 jsroute=True,
723 724 controller='admin/repos', action='edit_permissions',
724 725 conditions={'method': ['GET'], 'function': check_repo},
725 726 requirements=URL_NAME_REQUIREMENTS)
726 727 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
727 728 controller='admin/repos', action='edit_permissions_update',
728 729 conditions={'method': ['PUT'], 'function': check_repo},
729 730 requirements=URL_NAME_REQUIREMENTS)
730 731
731 732 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
732 733 controller='admin/repos', action='edit_fields',
733 734 conditions={'method': ['GET'], 'function': check_repo},
734 735 requirements=URL_NAME_REQUIREMENTS)
735 736 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
736 737 controller='admin/repos', action='create_repo_field',
737 738 conditions={'method': ['PUT'], 'function': check_repo},
738 739 requirements=URL_NAME_REQUIREMENTS)
739 740 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
740 741 controller='admin/repos', action='delete_repo_field',
741 742 conditions={'method': ['DELETE'], 'function': check_repo},
742 743 requirements=URL_NAME_REQUIREMENTS)
743 744
744 745 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
745 746 controller='admin/repos', action='edit_advanced',
746 747 conditions={'method': ['GET'], 'function': check_repo},
747 748 requirements=URL_NAME_REQUIREMENTS)
748 749
749 750 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
750 751 controller='admin/repos', action='edit_advanced_locking',
751 752 conditions={'method': ['PUT'], 'function': check_repo},
752 753 requirements=URL_NAME_REQUIREMENTS)
753 754 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
754 755 controller='admin/repos', action='toggle_locking',
755 756 conditions={'method': ['GET'], 'function': check_repo},
756 757 requirements=URL_NAME_REQUIREMENTS)
757 758
758 759 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
759 760 controller='admin/repos', action='edit_advanced_journal',
760 761 conditions={'method': ['PUT'], 'function': check_repo},
761 762 requirements=URL_NAME_REQUIREMENTS)
762 763
763 764 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
764 765 controller='admin/repos', action='edit_advanced_fork',
765 766 conditions={'method': ['PUT'], 'function': check_repo},
766 767 requirements=URL_NAME_REQUIREMENTS)
767 768
768 769 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
769 770 controller='admin/repos', action='edit_caches_form',
770 771 conditions={'method': ['GET'], 'function': check_repo},
771 772 requirements=URL_NAME_REQUIREMENTS)
772 773 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 774 controller='admin/repos', action='edit_caches',
774 775 conditions={'method': ['PUT'], 'function': check_repo},
775 776 requirements=URL_NAME_REQUIREMENTS)
776 777
777 778 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
778 779 controller='admin/repos', action='edit_remote_form',
779 780 conditions={'method': ['GET'], 'function': check_repo},
780 781 requirements=URL_NAME_REQUIREMENTS)
781 782 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 783 controller='admin/repos', action='edit_remote',
783 784 conditions={'method': ['PUT'], 'function': check_repo},
784 785 requirements=URL_NAME_REQUIREMENTS)
785 786
786 787 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
787 788 controller='admin/repos', action='edit_statistics_form',
788 789 conditions={'method': ['GET'], 'function': check_repo},
789 790 requirements=URL_NAME_REQUIREMENTS)
790 791 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 792 controller='admin/repos', action='edit_statistics',
792 793 conditions={'method': ['PUT'], 'function': check_repo},
793 794 requirements=URL_NAME_REQUIREMENTS)
794 795 rmap.connect('repo_settings_issuetracker',
795 796 '/{repo_name}/settings/issue-tracker',
796 797 controller='admin/repos', action='repo_issuetracker',
797 798 conditions={'method': ['GET'], 'function': check_repo},
798 799 requirements=URL_NAME_REQUIREMENTS)
799 800 rmap.connect('repo_issuetracker_test',
800 801 '/{repo_name}/settings/issue-tracker/test',
801 802 controller='admin/repos', action='repo_issuetracker_test',
802 803 conditions={'method': ['POST'], 'function': check_repo},
803 804 requirements=URL_NAME_REQUIREMENTS)
804 805 rmap.connect('repo_issuetracker_delete',
805 806 '/{repo_name}/settings/issue-tracker/delete',
806 807 controller='admin/repos', action='repo_issuetracker_delete',
807 808 conditions={'method': ['DELETE'], 'function': check_repo},
808 809 requirements=URL_NAME_REQUIREMENTS)
809 810 rmap.connect('repo_issuetracker_save',
810 811 '/{repo_name}/settings/issue-tracker/save',
811 812 controller='admin/repos', action='repo_issuetracker_save',
812 813 conditions={'method': ['POST'], 'function': check_repo},
813 814 requirements=URL_NAME_REQUIREMENTS)
814 815 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
815 816 controller='admin/repos', action='repo_settings_vcs_update',
816 817 conditions={'method': ['POST'], 'function': check_repo},
817 818 requirements=URL_NAME_REQUIREMENTS)
818 819 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 820 controller='admin/repos', action='repo_settings_vcs',
820 821 conditions={'method': ['GET'], 'function': check_repo},
821 822 requirements=URL_NAME_REQUIREMENTS)
822 823 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 824 controller='admin/repos', action='repo_delete_svn_pattern',
824 825 conditions={'method': ['DELETE'], 'function': check_repo},
825 826 requirements=URL_NAME_REQUIREMENTS)
826 827
827 828 # still working url for backward compat.
828 829 rmap.connect('raw_changeset_home_depraced',
829 830 '/{repo_name}/raw-changeset/{revision}',
830 831 controller='changeset', action='changeset_raw',
831 832 revision='tip', conditions={'function': check_repo},
832 833 requirements=URL_NAME_REQUIREMENTS)
833 834
834 835 # new URLs
835 836 rmap.connect('changeset_raw_home',
836 837 '/{repo_name}/changeset-diff/{revision}',
837 838 controller='changeset', action='changeset_raw',
838 839 revision='tip', conditions={'function': check_repo},
839 840 requirements=URL_NAME_REQUIREMENTS)
840 841
841 842 rmap.connect('changeset_patch_home',
842 843 '/{repo_name}/changeset-patch/{revision}',
843 844 controller='changeset', action='changeset_patch',
844 845 revision='tip', conditions={'function': check_repo},
845 846 requirements=URL_NAME_REQUIREMENTS)
846 847
847 848 rmap.connect('changeset_download_home',
848 849 '/{repo_name}/changeset-download/{revision}',
849 850 controller='changeset', action='changeset_download',
850 851 revision='tip', conditions={'function': check_repo},
851 852 requirements=URL_NAME_REQUIREMENTS)
852 853
853 854 rmap.connect('changeset_comment',
854 855 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
855 856 controller='changeset', revision='tip', action='comment',
856 857 conditions={'function': check_repo},
857 858 requirements=URL_NAME_REQUIREMENTS)
858 859
859 860 rmap.connect('changeset_comment_preview',
860 861 '/{repo_name}/changeset/comment/preview', jsroute=True,
861 862 controller='changeset', action='preview_comment',
862 863 conditions={'function': check_repo, 'method': ['POST']},
863 864 requirements=URL_NAME_REQUIREMENTS)
864 865
865 866 rmap.connect('changeset_comment_delete',
866 867 '/{repo_name}/changeset/comment/{comment_id}/delete',
867 868 controller='changeset', action='delete_comment',
868 869 conditions={'function': check_repo, 'method': ['DELETE']},
869 870 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
870 871
871 872 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
872 873 controller='changeset', action='changeset_info',
873 874 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
874 875
875 876 rmap.connect('compare_home',
876 877 '/{repo_name}/compare',
877 878 controller='compare', action='index',
878 879 conditions={'function': check_repo},
879 880 requirements=URL_NAME_REQUIREMENTS)
880 881
881 882 rmap.connect('compare_url',
882 883 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
883 884 controller='compare', action='compare',
884 885 conditions={'function': check_repo},
885 886 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
886 887
887 888 rmap.connect('pullrequest_home',
888 889 '/{repo_name}/pull-request/new', controller='pullrequests',
889 890 action='index', conditions={'function': check_repo,
890 891 'method': ['GET']},
891 892 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
892 893
893 894 rmap.connect('pullrequest',
894 895 '/{repo_name}/pull-request/new', controller='pullrequests',
895 896 action='create', conditions={'function': check_repo,
896 897 'method': ['POST']},
897 898 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
898 899
899 900 rmap.connect('pullrequest_repo_refs',
900 901 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
901 902 controller='pullrequests',
902 903 action='get_repo_refs',
903 904 conditions={'function': check_repo, 'method': ['GET']},
904 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
905 906
906 907 rmap.connect('pullrequest_repo_destinations',
907 908 '/{repo_name}/pull-request/repo-destinations',
908 909 controller='pullrequests',
909 910 action='get_repo_destinations',
910 911 conditions={'function': check_repo, 'method': ['GET']},
911 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
912 913
913 914 rmap.connect('pullrequest_show',
914 915 '/{repo_name}/pull-request/{pull_request_id}',
915 916 controller='pullrequests',
916 917 action='show', conditions={'function': check_repo,
917 918 'method': ['GET']},
918 919 requirements=URL_NAME_REQUIREMENTS)
919 920
920 921 rmap.connect('pullrequest_update',
921 922 '/{repo_name}/pull-request/{pull_request_id}',
922 923 controller='pullrequests',
923 924 action='update', conditions={'function': check_repo,
924 925 'method': ['PUT']},
925 926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
926 927
927 928 rmap.connect('pullrequest_merge',
928 929 '/{repo_name}/pull-request/{pull_request_id}',
929 930 controller='pullrequests',
930 931 action='merge', conditions={'function': check_repo,
931 932 'method': ['POST']},
932 933 requirements=URL_NAME_REQUIREMENTS)
933 934
934 935 rmap.connect('pullrequest_delete',
935 936 '/{repo_name}/pull-request/{pull_request_id}',
936 937 controller='pullrequests',
937 938 action='delete', conditions={'function': check_repo,
938 939 'method': ['DELETE']},
939 940 requirements=URL_NAME_REQUIREMENTS)
940 941
941 942 rmap.connect('pullrequest_show_all',
942 943 '/{repo_name}/pull-request',
943 944 controller='pullrequests',
944 945 action='show_all', conditions={'function': check_repo,
945 946 'method': ['GET']},
946 947 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
947 948
948 949 rmap.connect('pullrequest_comment',
949 950 '/{repo_name}/pull-request-comment/{pull_request_id}',
950 951 controller='pullrequests',
951 952 action='comment', conditions={'function': check_repo,
952 953 'method': ['POST']},
953 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
954 955
955 956 rmap.connect('pullrequest_comment_delete',
956 957 '/{repo_name}/pull-request-comment/{comment_id}/delete',
957 958 controller='pullrequests', action='delete_comment',
958 959 conditions={'function': check_repo, 'method': ['DELETE']},
959 960 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
960 961
961 962 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
962 963 controller='summary', conditions={'function': check_repo},
963 964 requirements=URL_NAME_REQUIREMENTS)
964 965
965 966 rmap.connect('branches_home', '/{repo_name}/branches',
966 967 controller='branches', conditions={'function': check_repo},
967 968 requirements=URL_NAME_REQUIREMENTS)
968 969
969 970 rmap.connect('tags_home', '/{repo_name}/tags',
970 971 controller='tags', conditions={'function': check_repo},
971 972 requirements=URL_NAME_REQUIREMENTS)
972 973
973 974 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
974 975 controller='bookmarks', conditions={'function': check_repo},
975 976 requirements=URL_NAME_REQUIREMENTS)
976 977
977 978 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
978 979 controller='changelog', conditions={'function': check_repo},
979 980 requirements=URL_NAME_REQUIREMENTS)
980 981
981 982 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
982 983 controller='changelog', action='changelog_summary',
983 984 conditions={'function': check_repo},
984 985 requirements=URL_NAME_REQUIREMENTS)
985 986
986 987 rmap.connect('changelog_file_home',
987 988 '/{repo_name}/changelog/{revision}/{f_path}',
988 989 controller='changelog', f_path=None,
989 990 conditions={'function': check_repo},
990 991 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
991 992
992 993 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
993 994 controller='changelog', action='changelog_details',
994 995 conditions={'function': check_repo},
995 996 requirements=URL_NAME_REQUIREMENTS)
996 997
997 998 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
998 999 controller='files', revision='tip', f_path='',
999 1000 conditions={'function': check_repo},
1000 1001 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1001 1002
1002 1003 rmap.connect('files_home_simple_catchrev',
1003 1004 '/{repo_name}/files/{revision}',
1004 1005 controller='files', revision='tip', f_path='',
1005 1006 conditions={'function': check_repo},
1006 1007 requirements=URL_NAME_REQUIREMENTS)
1007 1008
1008 1009 rmap.connect('files_home_simple_catchall',
1009 1010 '/{repo_name}/files',
1010 1011 controller='files', revision='tip', f_path='',
1011 1012 conditions={'function': check_repo},
1012 1013 requirements=URL_NAME_REQUIREMENTS)
1013 1014
1014 1015 rmap.connect('files_history_home',
1015 1016 '/{repo_name}/history/{revision}/{f_path}',
1016 1017 controller='files', action='history', revision='tip', f_path='',
1017 1018 conditions={'function': check_repo},
1018 1019 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1019 1020
1020 1021 rmap.connect('files_authors_home',
1021 1022 '/{repo_name}/authors/{revision}/{f_path}',
1022 1023 controller='files', action='authors', revision='tip', f_path='',
1023 1024 conditions={'function': check_repo},
1024 1025 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1025 1026
1026 1027 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1027 1028 controller='files', action='diff', f_path='',
1028 1029 conditions={'function': check_repo},
1029 1030 requirements=URL_NAME_REQUIREMENTS)
1030 1031
1031 1032 rmap.connect('files_diff_2way_home',
1032 1033 '/{repo_name}/diff-2way/{f_path}',
1033 1034 controller='files', action='diff_2way', f_path='',
1034 1035 conditions={'function': check_repo},
1035 1036 requirements=URL_NAME_REQUIREMENTS)
1036 1037
1037 1038 rmap.connect('files_rawfile_home',
1038 1039 '/{repo_name}/rawfile/{revision}/{f_path}',
1039 1040 controller='files', action='rawfile', revision='tip',
1040 1041 f_path='', conditions={'function': check_repo},
1041 1042 requirements=URL_NAME_REQUIREMENTS)
1042 1043
1043 1044 rmap.connect('files_raw_home',
1044 1045 '/{repo_name}/raw/{revision}/{f_path}',
1045 1046 controller='files', action='raw', revision='tip', f_path='',
1046 1047 conditions={'function': check_repo},
1047 1048 requirements=URL_NAME_REQUIREMENTS)
1048 1049
1049 1050 rmap.connect('files_render_home',
1050 1051 '/{repo_name}/render/{revision}/{f_path}',
1051 1052 controller='files', action='index', revision='tip', f_path='',
1052 1053 rendered=True, conditions={'function': check_repo},
1053 1054 requirements=URL_NAME_REQUIREMENTS)
1054 1055
1055 1056 rmap.connect('files_annotate_home',
1056 1057 '/{repo_name}/annotate/{revision}/{f_path}',
1057 1058 controller='files', action='index', revision='tip',
1058 1059 f_path='', annotate=True, conditions={'function': check_repo},
1059 1060 requirements=URL_NAME_REQUIREMENTS)
1060 1061
1061 1062 rmap.connect('files_edit',
1062 1063 '/{repo_name}/edit/{revision}/{f_path}',
1063 1064 controller='files', action='edit', revision='tip',
1064 1065 f_path='',
1065 1066 conditions={'function': check_repo, 'method': ['POST']},
1066 1067 requirements=URL_NAME_REQUIREMENTS)
1067 1068
1068 1069 rmap.connect('files_edit_home',
1069 1070 '/{repo_name}/edit/{revision}/{f_path}',
1070 1071 controller='files', action='edit_home', revision='tip',
1071 1072 f_path='', conditions={'function': check_repo},
1072 1073 requirements=URL_NAME_REQUIREMENTS)
1073 1074
1074 1075 rmap.connect('files_add',
1075 1076 '/{repo_name}/add/{revision}/{f_path}',
1076 1077 controller='files', action='add', revision='tip',
1077 1078 f_path='',
1078 1079 conditions={'function': check_repo, 'method': ['POST']},
1079 1080 requirements=URL_NAME_REQUIREMENTS)
1080 1081
1081 1082 rmap.connect('files_add_home',
1082 1083 '/{repo_name}/add/{revision}/{f_path}',
1083 1084 controller='files', action='add_home', revision='tip',
1084 1085 f_path='', conditions={'function': check_repo},
1085 1086 requirements=URL_NAME_REQUIREMENTS)
1086 1087
1087 1088 rmap.connect('files_delete',
1088 1089 '/{repo_name}/delete/{revision}/{f_path}',
1089 1090 controller='files', action='delete', revision='tip',
1090 1091 f_path='',
1091 1092 conditions={'function': check_repo, 'method': ['POST']},
1092 1093 requirements=URL_NAME_REQUIREMENTS)
1093 1094
1094 1095 rmap.connect('files_delete_home',
1095 1096 '/{repo_name}/delete/{revision}/{f_path}',
1096 1097 controller='files', action='delete_home', revision='tip',
1097 1098 f_path='', conditions={'function': check_repo},
1098 1099 requirements=URL_NAME_REQUIREMENTS)
1099 1100
1100 1101 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1101 1102 controller='files', action='archivefile',
1102 1103 conditions={'function': check_repo},
1103 1104 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1104 1105
1105 1106 rmap.connect('files_nodelist_home',
1106 1107 '/{repo_name}/nodelist/{revision}/{f_path}',
1107 1108 controller='files', action='nodelist',
1108 1109 conditions={'function': check_repo},
1109 1110 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1110 1111
1111 1112 rmap.connect('files_nodetree_full',
1112 1113 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1113 1114 controller='files', action='nodetree_full',
1114 1115 conditions={'function': check_repo},
1115 1116 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1116 1117
1117 1118 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1118 1119 controller='forks', action='fork_create',
1119 1120 conditions={'function': check_repo, 'method': ['POST']},
1120 1121 requirements=URL_NAME_REQUIREMENTS)
1121 1122
1122 1123 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1123 1124 controller='forks', action='fork',
1124 1125 conditions={'function': check_repo},
1125 1126 requirements=URL_NAME_REQUIREMENTS)
1126 1127
1127 1128 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1128 1129 controller='forks', action='forks',
1129 1130 conditions={'function': check_repo},
1130 1131 requirements=URL_NAME_REQUIREMENTS)
1131 1132
1132 1133 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1133 1134 controller='followers', action='followers',
1134 1135 conditions={'function': check_repo},
1135 1136 requirements=URL_NAME_REQUIREMENTS)
1136 1137
1137 1138 # must be here for proper group/repo catching pattern
1138 1139 _connect_with_slash(
1139 1140 rmap, 'repo_group_home', '/{group_name}',
1140 1141 controller='home', action='index_repo_group',
1141 1142 conditions={'function': check_group},
1142 1143 requirements=URL_NAME_REQUIREMENTS)
1143 1144
1144 1145 # catch all, at the end
1145 1146 _connect_with_slash(
1146 1147 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1147 1148 controller='summary', action='index',
1148 1149 conditions={'function': check_repo},
1149 1150 requirements=URL_NAME_REQUIREMENTS)
1150 1151
1151 1152 return rmap
1152 1153
1153 1154
1154 1155 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1155 1156 """
1156 1157 Connect a route with an optional trailing slash in `path`.
1157 1158 """
1158 1159 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1159 1160 mapper.connect(name, path, *args, **kwargs)
@@ -1,200 +1,238 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 logging
22 22
23 23 from rhodecode.model.db import Repository, Integration, RepoGroup
24 24 from rhodecode.config.routing import (
25 25 ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
26 26 from rhodecode.integrations import integration_type_registry
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def includeme(config):
32 32
33 33 # global integrations
34
35 config.add_route('global_integrations_new',
36 ADMIN_PREFIX + '/integrations/new')
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
38 attr='new_integration',
39 renderer='rhodecode:templates/admin/integrations/new.html',
40 request_method='GET',
41 route_name='global_integrations_new')
42
34 43 config.add_route('global_integrations_home',
35 44 ADMIN_PREFIX + '/integrations')
36 45 config.add_route('global_integrations_list',
37 46 ADMIN_PREFIX + '/integrations/{integration}')
38 47 for route_name in ['global_integrations_home', 'global_integrations_list']:
39 48 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
40 49 attr='index',
41 50 renderer='rhodecode:templates/admin/integrations/list.html',
42 51 request_method='GET',
43 52 route_name=route_name)
44 53
45 54 config.add_route('global_integrations_create',
46 55 ADMIN_PREFIX + '/integrations/{integration}/new',
47 56 custom_predicates=(valid_integration,))
48 57 config.add_route('global_integrations_edit',
49 58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
50 59 custom_predicates=(valid_integration,))
60
61
51 62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
52 63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
53 64 attr='settings_get',
54 renderer='rhodecode:templates/admin/integrations/edit.html',
65 renderer='rhodecode:templates/admin/integrations/form.html',
55 66 request_method='GET',
56 67 route_name=route_name)
57 68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
58 69 attr='settings_post',
59 renderer='rhodecode:templates/admin/integrations/edit.html',
70 renderer='rhodecode:templates/admin/integrations/form.html',
71 request_method='POST',
72 route_name=route_name)
73
74
75 # repo group integrations
76 config.add_route('repo_group_integrations_home',
77 add_route_requirements(
78 '{repo_group_name}/settings/integrations',
79 URL_NAME_REQUIREMENTS
80 ),
81 custom_predicates=(valid_repo_group,)
82 )
83 config.add_route('repo_group_integrations_list',
84 add_route_requirements(
85 '{repo_group_name}/settings/integrations/{integration}',
86 URL_NAME_REQUIREMENTS
87 ),
88 custom_predicates=(valid_repo_group, valid_integration))
89 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
90 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
91 attr='index',
92 renderer='rhodecode:templates/admin/integrations/list.html',
93 request_method='GET',
94 route_name=route_name)
95
96 config.add_route('repo_group_integrations_new',
97 add_route_requirements(
98 '{repo_group_name}/settings/integrations/new',
99 URL_NAME_REQUIREMENTS
100 ),
101 custom_predicates=(valid_repo_group,))
102 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
103 attr='new_integration',
104 renderer='rhodecode:templates/admin/integrations/new.html',
105 request_method='GET',
106 route_name='repo_group_integrations_new')
107
108 config.add_route('repo_group_integrations_create',
109 add_route_requirements(
110 '{repo_group_name}/settings/integrations/{integration}/new',
111 URL_NAME_REQUIREMENTS
112 ),
113 custom_predicates=(valid_repo_group, valid_integration))
114 config.add_route('repo_group_integrations_edit',
115 add_route_requirements(
116 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
117 URL_NAME_REQUIREMENTS
118 ),
119 custom_predicates=(valid_repo_group, valid_integration))
120 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
121 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
122 attr='settings_get',
123 renderer='rhodecode:templates/admin/integrations/form.html',
124 request_method='GET',
125 route_name=route_name)
126 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
127 attr='settings_post',
128 renderer='rhodecode:templates/admin/integrations/form.html',
60 129 request_method='POST',
61 130 route_name=route_name)
62 131
63 132
64 133 # repo integrations
65 134 config.add_route('repo_integrations_home',
66 135 add_route_requirements(
67 136 '{repo_name}/settings/integrations',
68 137 URL_NAME_REQUIREMENTS
69 138 ),
70 139 custom_predicates=(valid_repo,))
71 140 config.add_route('repo_integrations_list',
72 141 add_route_requirements(
73 142 '{repo_name}/settings/integrations/{integration}',
74 143 URL_NAME_REQUIREMENTS
75 144 ),
76 145 custom_predicates=(valid_repo, valid_integration))
77 146 for route_name in ['repo_integrations_home', 'repo_integrations_list']:
78 147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
79 148 attr='index',
80 149 request_method='GET',
150 renderer='rhodecode:templates/admin/integrations/list.html',
81 151 route_name=route_name)
82 152
153 config.add_route('repo_integrations_new',
154 add_route_requirements(
155 '{repo_name}/settings/integrations/new',
156 URL_NAME_REQUIREMENTS
157 ),
158 custom_predicates=(valid_repo,))
159 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
160 attr='new_integration',
161 renderer='rhodecode:templates/admin/integrations/new.html',
162 request_method='GET',
163 route_name='repo_integrations_new')
164
83 165 config.add_route('repo_integrations_create',
84 166 add_route_requirements(
85 167 '{repo_name}/settings/integrations/{integration}/new',
86 168 URL_NAME_REQUIREMENTS
87 169 ),
88 170 custom_predicates=(valid_repo, valid_integration))
89 171 config.add_route('repo_integrations_edit',
90 172 add_route_requirements(
91 173 '{repo_name}/settings/integrations/{integration}/{integration_id}',
92 174 URL_NAME_REQUIREMENTS
93 175 ),
94 176 custom_predicates=(valid_repo, valid_integration))
95 177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
96 178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
97 179 attr='settings_get',
98 renderer='rhodecode:templates/admin/integrations/edit.html',
180 renderer='rhodecode:templates/admin/integrations/form.html',
99 181 request_method='GET',
100 182 route_name=route_name)
101 183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
102 184 attr='settings_post',
103 renderer='rhodecode:templates/admin/integrations/edit.html',
104 request_method='POST',
105 route_name=route_name)
106
107
108 # repo group integrations
109 config.add_route('repo_group_integrations_home',
110 add_route_requirements(
111 '{repo_group_name}/settings/integrations',
112 URL_NAME_REQUIREMENTS
113 ),
114 custom_predicates=(valid_repo_group,))
115 config.add_route('repo_group_integrations_list',
116 add_route_requirements(
117 '{repo_group_name}/settings/integrations/{integration}',
118 URL_NAME_REQUIREMENTS
119 ),
120 custom_predicates=(valid_repo_group, valid_integration))
121 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
122 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
123 attr='index',
124 request_method='GET',
125 route_name=route_name)
126
127 config.add_route('repo_group_integrations_create',
128 add_route_requirements(
129 '{repo_group_name}/settings/integrations/{integration}/new',
130 URL_NAME_REQUIREMENTS
131 ),
132 custom_predicates=(valid_repo_group, valid_integration))
133 config.add_route('repo_group_integrations_edit',
134 add_route_requirements(
135 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
136 URL_NAME_REQUIREMENTS
137 ),
138 custom_predicates=(valid_repo_group, valid_integration))
139 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
140 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
141 attr='settings_get',
142 renderer='rhodecode:templates/admin/integrations/edit.html',
143 request_method='GET',
144 route_name=route_name)
145 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
146 attr='settings_post',
147 renderer='rhodecode:templates/admin/integrations/edit.html',
185 renderer='rhodecode:templates/admin/integrations/form.html',
148 186 request_method='POST',
149 187 route_name=route_name)
150 188
151 189
152 190 def valid_repo(info, request):
153 191 repo = Repository.get_by_repo_name(info['match']['repo_name'])
154 192 if repo:
155 193 return True
156 194
157 195
158 196 def valid_repo_group(info, request):
159 197 repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name'])
160 198 if repo_group:
161 199 return True
162 200 return False
163 201
164 202
165 203 def valid_integration(info, request):
166 204 integration_type = info['match']['integration']
167 205 integration_id = info['match'].get('integration_id')
168 206 repo_name = info['match'].get('repo_name')
169 207 repo_group_name = info['match'].get('repo_group_name')
170 208
171 209 if integration_type not in integration_type_registry:
172 210 return False
173 211
174 212 repo, repo_group = None, None
175 213 if repo_name:
176 214 repo = Repository.get_by_repo_name(repo_name)
177 215 if not repo:
178 216 return False
179 217
180 218 if repo_group_name:
181 219 repo_group = RepoGroup.get_by_group_name(repo_group_name)
182 220 if not repo_group:
183 221 return False
184 222
185 223 if repo_name and repo_group:
186 224 raise Exception('Either repo or repo_group can be set, not both')
187 225
188 226
189 227 if integration_id:
190 228 integration = Integration.get(integration_id)
191 229 if not integration:
192 230 return False
193 231 if integration.integration_type != integration_type:
194 232 return False
195 233 if repo and repo.repo_id != integration.repo_id:
196 234 return False
197 if repo_group and repo_group.repo_group_id != integration.repo_group_id:
235 if repo_group and repo_group.group_id != integration.repo_group_id:
198 236 return False
199 237
200 238 return True
@@ -1,45 +1,71 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 colander
22 22
23 from rhodecode.translation import lazy_ugettext
23 from rhodecode.translation import _
24 24
25 25
26 class IntegrationSettingsSchemaBase(colander.MappingSchema):
27 """
28 This base schema is intended for use in integrations.
29 It adds a few default settings (e.g., "enabled"), so that integration
30 authors don't have to maintain a bunch of boilerplate.
31 """
26 class IntegrationOptionsSchemaBase(colander.MappingSchema):
32 27 enabled = colander.SchemaNode(
33 28 colander.Bool(),
34 29 default=True,
35 description=lazy_ugettext('Enable or disable this integration.'),
30 description=_('Enable or disable this integration.'),
36 31 missing=False,
37 title=lazy_ugettext('Enabled'),
32 title=_('Enabled'),
38 33 )
39 34
40 35 name = colander.SchemaNode(
41 36 colander.String(),
42 description=lazy_ugettext('Short name for this integration.'),
37 description=_('Short name for this integration.'),
43 38 missing=colander.required,
44 title=lazy_ugettext('Integration name'),
39 title=_('Integration name'),
45 40 )
41
42
43 class RepoIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
44 pass
45
46
47 class RepoGroupIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
48 child_repos_only = colander.SchemaNode(
49 colander.Bool(),
50 default=True,
51 description=_(
52 'Limit integrations to to work only on the direct children '
53 'repositories of this repository group (no subgroups)'),
54 missing=False,
55 title=_('Limit to childen repos only'),
56 )
57
58
59 class GlobalIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
60 child_repos_only = colander.SchemaNode(
61 colander.Bool(),
62 default=False,
63 description=_(
64 'Limit integrations to to work only on root level repositories'),
65 missing=False,
66 title=_('Root repositories only'),
67 )
68
69
70 class IntegrationSettingsSchemaBase(colander.MappingSchema):
71 pass
@@ -1,42 +1,101 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
21 import colander
22 from rhodecode.translation import _
22 23
23 24
24 25 class IntegrationTypeBase(object):
25 26 """ Base class for IntegrationType plugins """
26 27
28 description = ''
29 icon = '''
30 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
31 <svg
32 xmlns:dc="http://purl.org/dc/elements/1.1/"
33 xmlns:cc="http://creativecommons.org/ns#"
34 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
35 xmlns:svg="http://www.w3.org/2000/svg"
36 xmlns="http://www.w3.org/2000/svg"
37 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
38 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
39 viewBox="0 -256 1792 1792"
40 id="svg3025"
41 version="1.1"
42 inkscape:version="0.48.3.1 r9886"
43 width="100%"
44 height="100%"
45 sodipodi:docname="cog_font_awesome.svg">
46 <metadata
47 id="metadata3035">
48 <rdf:RDF>
49 <cc:Work
50 rdf:about="">
51 <dc:format>image/svg+xml</dc:format>
52 <dc:type
53 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
54 </cc:Work>
55 </rdf:RDF>
56 </metadata>
57 <defs
58 id="defs3033" />
59 <sodipodi:namedview
60 pagecolor="#ffffff"
61 bordercolor="#666666"
62 borderopacity="1"
63 objecttolerance="10"
64 gridtolerance="10"
65 guidetolerance="10"
66 inkscape:pageopacity="0"
67 inkscape:pageshadow="2"
68 inkscape:window-width="640"
69 inkscape:window-height="480"
70 id="namedview3031"
71 showgrid="false"
72 inkscape:zoom="0.13169643"
73 inkscape:cx="896"
74 inkscape:cy="896"
75 inkscape:window-x="0"
76 inkscape:window-y="25"
77 inkscape:window-maximized="0"
78 inkscape:current-layer="svg3025" />
79 <g
80 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
81 id="g3027">
82 <path
83 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"
84 id="path3029"
85 inkscape:connector-curvature="0"
86 style="fill:currentColor" />
87 </g>
88 </svg>
89 '''
90
27 91 def __init__(self, settings):
28 92 """
29 93 :param settings: dict of settings to be used for the integration
30 94 """
31 95 self.settings = settings
32 96
33
34 97 def settings_schema(self):
35 98 """
36 99 A colander schema of settings for the integration type
37
38 Subclasses can return their own schema but should always
39 inherit from IntegrationSettingsSchemaBase
40 100 """
41 return IntegrationSettingsSchemaBase()
42
101 return colander.Schema()
@@ -1,222 +1,283 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import logging
24 24 import colander
25 25
26 26 from mako.template import Template
27 27
28 28 from rhodecode import events
29 from rhodecode.translation import _, lazy_ugettext
29 from rhodecode.translation import _
30 30 from rhodecode.lib.celerylib import run_task
31 31 from rhodecode.lib.celerylib import tasks
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37 repo_push_template_plaintext = Template('''
39 38 Commits:
40 39
41 40 % for commit in data['push']['commits']:
42 41 ${commit['url']} by ${commit['author']} at ${commit['date']}
43 42 ${commit['message']}
44 43 ----
45 44
46 45 % endfor
47 46 ''')
48 47
49 48 ## TODO (marcink): think about putting this into a file, or use base.mako email template
50 49
51 50 repo_push_template_html = Template('''
52 51 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
53 52 <html xmlns="http://www.w3.org/1999/xhtml">
54 53 <head>
55 54 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
56 55 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
57 56 <title>${subject}</title>
58 57 <style type="text/css">
59 58 /* Based on The MailChimp Reset INLINE: Yes. */
60 59 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
61 60 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
62 61 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
63 62 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
64 63 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
65 64 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
66 65 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
67 66 /* End reset */
68 67
69 68 /* defaults for images*/
70 69 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
71 70 a img {border:none;}
72 71 .image_fix {display:block;}
73 72
74 73 body {line-height:1.2em;}
75 74 p {margin: 0 0 20px;}
76 75 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
77 76 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
78 77 a:focus {outline:none;}
79 78 a:hover {color: #305b91;}
80 79 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
81 80 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
82 81 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
83 82 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
84 83 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
85 84 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
86 85 input:focus {outline: 1px solid #979797}
87 86 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
88 87 /* Put your iPhone 4g styles in here */
89 88 }
90 89
91 90 /* Android targeting */
92 91 @media only screen and (-webkit-device-pixel-ratio:.75){
93 92 /* Put CSS for low density (ldpi) Android layouts in here */
94 93 }
95 94 @media only screen and (-webkit-device-pixel-ratio:1){
96 95 /* Put CSS for medium density (mdpi) Android layouts in here */
97 96 }
98 97 @media only screen and (-webkit-device-pixel-ratio:1.5){
99 98 /* Put CSS for high density (hdpi) Android layouts in here */
100 99 }
101 100 /* end Android targeting */
102 101
103 102 </style>
104 103
105 104 <!-- Targeting Windows Mobile -->
106 105 <!--[if IEMobile 7]>
107 106 <style type="text/css">
108 107
109 108 </style>
110 109 <![endif]-->
111 110
112 111 <!--[if gte mso 9]>
113 112 <style>
114 113 /* Target Outlook 2007 and 2010 */
115 114 </style>
116 115 <![endif]-->
117 116 </head>
118 117 <body>
119 118 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
120 119 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
121 120 <tr>
122 121 <td valign="top" style="padding:0;">
123 122 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
124 123 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
125 124 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
126 125 ${'RhodeCode'}
127 126 </a>
128 127 </td></tr>
129 128 <tr>
130 129 <td style="padding:15px;" valign="top">
131 130 % for commit in data['push']['commits']:
132 131 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
133 132 ${commit['message_html']} <br/>
134 133 <br/>
135 134 % endfor
136 135 </td>
137 136 </tr>
138 137 </table>
139 138 </td>
140 139 </tr>
141 140 </table>
142 141 <!-- End of wrapper table -->
143 142 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
144 143 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
145 144 </a></p>
146 145 </body>
147 146 </html>
148 147 ''')
149 148
149 email_icon = '''
150 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
151 <svg
152 xmlns:dc="http://purl.org/dc/elements/1.1/"
153 xmlns:cc="http://creativecommons.org/ns#"
154 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
155 xmlns:svg="http://www.w3.org/2000/svg"
156 xmlns="http://www.w3.org/2000/svg"
157 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
158 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
159 viewBox="0 -256 1850 1850"
160 id="svg2989"
161 version="1.1"
162 inkscape:version="0.48.3.1 r9886"
163 width="100%"
164 height="100%"
165 sodipodi:docname="envelope_font_awesome.svg">
166 <metadata
167 id="metadata2999">
168 <rdf:RDF>
169 <cc:Work
170 rdf:about="">
171 <dc:format>image/svg+xml</dc:format>
172 <dc:type
173 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
174 </cc:Work>
175 </rdf:RDF>
176 </metadata>
177 <defs
178 id="defs2997" />
179 <sodipodi:namedview
180 pagecolor="#ffffff"
181 bordercolor="#666666"
182 borderopacity="1"
183 objecttolerance="10"
184 gridtolerance="10"
185 guidetolerance="10"
186 inkscape:pageopacity="0"
187 inkscape:pageshadow="2"
188 inkscape:window-width="640"
189 inkscape:window-height="480"
190 id="namedview2995"
191 showgrid="false"
192 inkscape:zoom="0.13169643"
193 inkscape:cx="896"
194 inkscape:cy="896"
195 inkscape:window-x="0"
196 inkscape:window-y="25"
197 inkscape:window-maximized="0"
198 inkscape:current-layer="svg2989" />
199 <g
200 transform="matrix(1,0,0,-1,37.966102,1282.678)"
201 id="g2991">
202 <path
203 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
204 id="path2993"
205 inkscape:connector-curvature="0"
206 style="fill:currentColor" />
207 </g>
208 </svg>
209 '''
150 210
151 class EmailSettingsSchema(IntegrationSettingsSchemaBase):
211 class EmailSettingsSchema(colander.Schema):
152 212 @colander.instantiate(validator=colander.Length(min=1))
153 213 class recipients(colander.SequenceSchema):
154 title = lazy_ugettext('Recipients')
155 description = lazy_ugettext('Email addresses to send push events to')
214 title = _('Recipients')
215 description = _('Email addresses to send push events to')
156 216 widget = deform.widget.SequenceWidget(min_len=1)
157 217
158 218 recipient = colander.SchemaNode(
159 219 colander.String(),
160 title=lazy_ugettext('Email address'),
161 description=lazy_ugettext('Email address'),
220 title=_('Email address'),
221 description=_('Email address'),
162 222 default='',
163 223 validator=colander.Email(),
164 224 widget=deform.widget.TextInputWidget(
165 225 placeholder='user@domain.com',
166 226 ),
167 227 )
168 228
169 229
170 230 class EmailIntegrationType(IntegrationTypeBase):
171 231 key = 'email'
172 display_name = lazy_ugettext('Email')
173 SettingsSchema = EmailSettingsSchema
232 display_name = _('Email')
233 description = _('Send repo push summaries to a list of recipients via email')
234 icon = email_icon
174 235
175 236 def settings_schema(self):
176 237 schema = EmailSettingsSchema()
177 238 return schema
178 239
179 240 def send_event(self, event):
180 241 data = event.as_dict()
181 242 log.debug('got event: %r', event)
182 243
183 244 if isinstance(event, events.RepoPushEvent):
184 245 repo_push_handler(data, self.settings)
185 246 else:
186 247 log.debug('ignoring event: %r', event)
187 248
188 249
189 250 def repo_push_handler(data, settings):
190 251 commit_num = len(data['push']['commits'])
191 252 server_url = data['server_url']
192 253
193 254 if commit_num == 0:
194 255 subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'.format(
195 256 author=data['actor']['username'],
196 257 repo_name=data['repo']['repo_name'],
197 258 commit_num=commit_num,
198 259 branches=', '.join(
199 260 branch['name'] for branch in data['push']['branches'])
200 261 )
201 262 else:
202 263 subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'.format(
203 264 author=data['actor']['username'],
204 265 repo_name=data['repo']['repo_name'],
205 266 commit_num=commit_num,
206 267 branches=', '.join(
207 268 branch['name'] for branch in data['push']['branches']))
208 269
209 270 email_body_plaintext = repo_push_template_plaintext.render(
210 271 data=data,
211 272 subject=subject,
212 273 instance_url=server_url)
213 274
214 275 email_body_html = repo_push_template_html.render(
215 276 data=data,
216 277 subject=subject,
217 278 instance_url=server_url)
218 279
219 280 for email_address in settings['recipients']:
220 281 run_task(
221 282 tasks.send_email, email_address, subject,
222 283 email_body_plaintext, email_body_html)
@@ -1,242 +1,243 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import re
24 24 import logging
25 25 import requests
26 26 import colander
27 27 import textwrap
28 28 from celery.task import task
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
41 class HipchatSettingsSchema(colander.Schema):
43 42 color_choices = [
44 ('yellow', lazy_ugettext('Yellow')),
45 ('red', lazy_ugettext('Red')),
46 ('green', lazy_ugettext('Green')),
47 ('purple', lazy_ugettext('Purple')),
48 ('gray', lazy_ugettext('Gray')),
43 ('yellow', _('Yellow')),
44 ('red', _('Red')),
45 ('green', _('Green')),
46 ('purple', _('Purple')),
47 ('gray', _('Gray')),
49 48 ]
50 49
51 50 server_url = colander.SchemaNode(
52 51 colander.String(),
53 title=lazy_ugettext('Hipchat server URL'),
54 description=lazy_ugettext('Hipchat integration url.'),
52 title=_('Hipchat server URL'),
53 description=_('Hipchat integration url.'),
55 54 default='',
56 55 preparer=strip_whitespace,
57 56 validator=colander.url,
58 57 widget=deform.widget.TextInputWidget(
59 58 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
60 59 ),
61 60 )
62 61 notify = colander.SchemaNode(
63 62 colander.Bool(),
64 title=lazy_ugettext('Notify'),
65 description=lazy_ugettext('Make a notification to the users in room.'),
63 title=_('Notify'),
64 description=_('Make a notification to the users in room.'),
66 65 missing=False,
67 66 default=False,
68 67 )
69 68 color = colander.SchemaNode(
70 69 colander.String(),
71 title=lazy_ugettext('Color'),
72 description=lazy_ugettext('Background color of message.'),
70 title=_('Color'),
71 description=_('Background color of message.'),
73 72 missing='',
74 73 validator=colander.OneOf([x[0] for x in color_choices]),
75 74 widget=deform.widget.Select2Widget(
76 75 values=color_choices,
77 76 ),
78 77 )
79 78
80 79
81 80 repo_push_template = Template('''
82 81 <b>${data['actor']['username']}</b> pushed to
83 82 %if data['push']['branches']:
84 83 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'}
85 84 ${', '.join('<a href="%s">%s</a>' % (branch['url'], branch['name']) for branch in data['push']['branches'])}
86 85 %else:
87 86 unknown branch
88 87 %endif
89 88 in <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>
90 89 <br>
91 90 <ul>
92 91 %for commit in data['push']['commits']:
93 92 <li>
94 93 <a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}
95 94 </li>
96 95 %endfor
97 96 </ul>
98 97 ''')
99 98
100 99
101
102 100 class HipchatIntegrationType(IntegrationTypeBase):
103 101 key = 'hipchat'
104 display_name = lazy_ugettext('Hipchat')
102 display_name = _('Hipchat')
103 description = _('Send events such as repo pushes and pull requests to '
104 'your hipchat channel.')
105 icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
105 106 valid_events = [
106 107 events.PullRequestCloseEvent,
107 108 events.PullRequestMergeEvent,
108 109 events.PullRequestUpdateEvent,
109 110 events.PullRequestCommentEvent,
110 111 events.PullRequestReviewEvent,
111 112 events.PullRequestCreateEvent,
112 113 events.RepoPushEvent,
113 114 events.RepoCreateEvent,
114 115 ]
115 116
116 117 def send_event(self, event):
117 118 if event.__class__ not in self.valid_events:
118 119 log.debug('event not valid: %r' % event)
119 120 return
120 121
121 122 if event.name not in self.settings['events']:
122 123 log.debug('event ignored: %r' % event)
123 124 return
124 125
125 126 data = event.as_dict()
126 127
127 128 text = '<b>%s<b> caused a <b>%s</b> event' % (
128 129 data['actor']['username'], event.name)
129 130
130 131 log.debug('handling hipchat event for %s' % event.name)
131 132
132 133 if isinstance(event, events.PullRequestCommentEvent):
133 134 text = self.format_pull_request_comment_event(event, data)
134 135 elif isinstance(event, events.PullRequestReviewEvent):
135 136 text = self.format_pull_request_review_event(event, data)
136 137 elif isinstance(event, events.PullRequestEvent):
137 138 text = self.format_pull_request_event(event, data)
138 139 elif isinstance(event, events.RepoPushEvent):
139 140 text = self.format_repo_push_event(data)
140 141 elif isinstance(event, events.RepoCreateEvent):
141 142 text = self.format_repo_create_event(data)
142 143 else:
143 144 log.error('unhandled event type: %r' % event)
144 145
145 146 run_task(post_text_to_hipchat, self.settings, text)
146 147
147 148 def settings_schema(self):
148 149 schema = HipchatSettingsSchema()
149 150 schema.add(colander.SchemaNode(
150 151 colander.Set(),
151 152 widget=deform.widget.CheckboxChoiceWidget(
152 153 values=sorted(
153 154 [(e.name, e.display_name) for e in self.valid_events]
154 155 )
155 156 ),
156 157 description="Events activated for this integration",
157 158 name='events'
158 159 ))
159 160
160 161 return schema
161 162
162 163 def format_pull_request_comment_event(self, event, data):
163 164 comment_text = data['comment']['text']
164 165 if len(comment_text) > 200:
165 166 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
166 167 comment_text=comment_text[:200],
167 168 comment_url=data['comment']['url'],
168 169 )
169 170
170 171 comment_status = ''
171 172 if data['comment']['status']:
172 173 comment_status = '[{}]: '.format(data['comment']['status'])
173 174
174 175 return (textwrap.dedent(
175 176 '''
176 177 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
177 178 >>> {comment_status}{comment_text}
178 179 ''').format(
179 180 comment_status=comment_status,
180 181 user=data['actor']['username'],
181 182 number=data['pullrequest']['pull_request_id'],
182 183 pr_url=data['pullrequest']['url'],
183 184 pr_status=data['pullrequest']['status'],
184 185 pr_title=data['pullrequest']['title'],
185 186 comment_text=comment_text
186 187 )
187 188 )
188 189
189 190 def format_pull_request_review_event(self, event, data):
190 191 return (textwrap.dedent(
191 192 '''
192 193 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
193 194 ''').format(
194 195 user=data['actor']['username'],
195 196 number=data['pullrequest']['pull_request_id'],
196 197 pr_url=data['pullrequest']['url'],
197 198 pr_status=data['pullrequest']['status'],
198 199 pr_title=data['pullrequest']['title'],
199 200 )
200 201 )
201 202
202 203 def format_pull_request_event(self, event, data):
203 204 action = {
204 205 events.PullRequestCloseEvent: 'closed',
205 206 events.PullRequestMergeEvent: 'merged',
206 207 events.PullRequestUpdateEvent: 'updated',
207 208 events.PullRequestCreateEvent: 'created',
208 209 }.get(event.__class__, str(event.__class__))
209 210
210 211 return ('Pull request <a href="{url}">#{number}</a> - {title} '
211 212 '{action} by {user}').format(
212 213 user=data['actor']['username'],
213 214 number=data['pullrequest']['pull_request_id'],
214 215 url=data['pullrequest']['url'],
215 216 title=data['pullrequest']['title'],
216 217 action=action
217 218 )
218 219
219 220 def format_repo_push_event(self, data):
220 221 result = repo_push_template.render(
221 222 data=data,
222 223 )
223 224 return result
224 225
225 226 def format_repo_create_event(self, data):
226 227 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
227 228 data['repo']['url'],
228 229 data['repo']['repo_name'],
229 230 data['repo']['repo_type'],
230 231 data['actor']['username'],
231 232 )
232 233
233 234
234 235 @task(ignore_result=True)
235 236 def post_text_to_hipchat(settings, text):
236 237 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
237 238 resp = requests.post(settings['server_url'], json={
238 239 "message": text,
239 240 "color": settings.get('color', 'yellow'),
240 241 "notify": settings.get('notify', False),
241 242 })
242 243 resp.raise_for_status() # raise exception on a failed request
@@ -1,253 +1,256 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import re
24 24 import logging
25 25 import requests
26 26 import colander
27 27 import textwrap
28 28 from celery.task import task
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(colander.Schema):
43 42 service = colander.SchemaNode(
44 43 colander.String(),
45 title=lazy_ugettext('Slack service URL'),
46 description=h.literal(lazy_ugettext(
44 title=_('Slack service URL'),
45 description=h.literal(_(
47 46 'This can be setup at the '
48 47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 48 'slack app manager</a>')),
50 49 default='',
51 50 preparer=strip_whitespace,
52 51 validator=colander.url,
53 52 widget=deform.widget.TextInputWidget(
54 53 placeholder='https://hooks.slack.com/services/...',
55 54 ),
56 55 )
57 56 username = colander.SchemaNode(
58 57 colander.String(),
59 title=lazy_ugettext('Username'),
60 description=lazy_ugettext('Username to show notifications coming from.'),
58 title=_('Username'),
59 description=_('Username to show notifications coming from.'),
61 60 missing='Rhodecode',
62 61 preparer=strip_whitespace,
63 62 widget=deform.widget.TextInputWidget(
64 63 placeholder='Rhodecode'
65 64 ),
66 65 )
67 66 channel = colander.SchemaNode(
68 67 colander.String(),
69 title=lazy_ugettext('Channel'),
70 description=lazy_ugettext('Channel to send notifications to.'),
68 title=_('Channel'),
69 description=_('Channel to send notifications to.'),
71 70 missing='',
72 71 preparer=strip_whitespace,
73 72 widget=deform.widget.TextInputWidget(
74 73 placeholder='#general'
75 74 ),
76 75 )
77 76 icon_emoji = colander.SchemaNode(
78 77 colander.String(),
79 title=lazy_ugettext('Emoji'),
80 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
78 title=_('Emoji'),
79 description=_('Emoji to use eg. :studio_microphone:'),
81 80 missing='',
82 81 preparer=strip_whitespace,
83 82 widget=deform.widget.TextInputWidget(
84 83 placeholder=':studio_microphone:'
85 84 ),
86 85 )
87 86
88 87
89 88 repo_push_template = Template(r'''
90 89 *${data['actor']['username']}* pushed to \
91 90 %if data['push']['branches']:
92 91 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
93 92 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
94 93 %else:
95 94 unknown branch \
96 95 %endif
97 96 in <${data['repo']['url']}|${data['repo']['repo_name']}>
98 97 >>>
99 98 %for commit in data['push']['commits']:
100 99 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
101 100 %endfor
102 101 ''')
103 102
104 103
104
105
105 106 class SlackIntegrationType(IntegrationTypeBase):
106 107 key = 'slack'
107 display_name = lazy_ugettext('Slack')
108 SettingsSchema = SlackSettingsSchema
108 display_name = _('Slack')
109 description = _('Send events such as repo pushes and pull requests to '
110 'your slack channel.')
111 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
109 112 valid_events = [
110 113 events.PullRequestCloseEvent,
111 114 events.PullRequestMergeEvent,
112 115 events.PullRequestUpdateEvent,
113 116 events.PullRequestCommentEvent,
114 117 events.PullRequestReviewEvent,
115 118 events.PullRequestCreateEvent,
116 119 events.RepoPushEvent,
117 120 events.RepoCreateEvent,
118 121 ]
119 122
120 123 def send_event(self, event):
121 124 if event.__class__ not in self.valid_events:
122 125 log.debug('event not valid: %r' % event)
123 126 return
124 127
125 128 if event.name not in self.settings['events']:
126 129 log.debug('event ignored: %r' % event)
127 130 return
128 131
129 132 data = event.as_dict()
130 133
131 134 text = '*%s* caused a *%s* event' % (
132 135 data['actor']['username'], event.name)
133 136
134 137 log.debug('handling slack event for %s' % event.name)
135 138
136 139 if isinstance(event, events.PullRequestCommentEvent):
137 140 text = self.format_pull_request_comment_event(event, data)
138 141 elif isinstance(event, events.PullRequestReviewEvent):
139 142 text = self.format_pull_request_review_event(event, data)
140 143 elif isinstance(event, events.PullRequestEvent):
141 144 text = self.format_pull_request_event(event, data)
142 145 elif isinstance(event, events.RepoPushEvent):
143 146 text = self.format_repo_push_event(data)
144 147 elif isinstance(event, events.RepoCreateEvent):
145 148 text = self.format_repo_create_event(data)
146 149 else:
147 150 log.error('unhandled event type: %r' % event)
148 151
149 152 run_task(post_text_to_slack, self.settings, text)
150 153
151 154 def settings_schema(self):
152 155 schema = SlackSettingsSchema()
153 156 schema.add(colander.SchemaNode(
154 157 colander.Set(),
155 158 widget=deform.widget.CheckboxChoiceWidget(
156 159 values=sorted(
157 160 [(e.name, e.display_name) for e in self.valid_events]
158 161 )
159 162 ),
160 163 description="Events activated for this integration",
161 164 name='events'
162 165 ))
163 166
164 167 return schema
165 168
166 169 def format_pull_request_comment_event(self, event, data):
167 170 comment_text = data['comment']['text']
168 171 if len(comment_text) > 200:
169 172 comment_text = '<{comment_url}|{comment_text}...>'.format(
170 173 comment_text=comment_text[:200],
171 174 comment_url=data['comment']['url'],
172 175 )
173 176
174 177 comment_status = ''
175 178 if data['comment']['status']:
176 179 comment_status = '[{}]: '.format(data['comment']['status'])
177 180
178 181 return (textwrap.dedent(
179 182 '''
180 183 {user} commented on pull request <{pr_url}|#{number}> - {pr_title}:
181 184 >>> {comment_status}{comment_text}
182 185 ''').format(
183 186 comment_status=comment_status,
184 187 user=data['actor']['username'],
185 188 number=data['pullrequest']['pull_request_id'],
186 189 pr_url=data['pullrequest']['url'],
187 190 pr_status=data['pullrequest']['status'],
188 191 pr_title=data['pullrequest']['title'],
189 192 comment_text=comment_text
190 193 )
191 194 )
192 195
193 196 def format_pull_request_review_event(self, event, data):
194 197 return (textwrap.dedent(
195 198 '''
196 199 Status changed to {pr_status} for pull request <{pr_url}|#{number}> - {pr_title}
197 200 ''').format(
198 201 user=data['actor']['username'],
199 202 number=data['pullrequest']['pull_request_id'],
200 203 pr_url=data['pullrequest']['url'],
201 204 pr_status=data['pullrequest']['status'],
202 205 pr_title=data['pullrequest']['title'],
203 206 )
204 207 )
205 208
206 209 def format_pull_request_event(self, event, data):
207 210 action = {
208 211 events.PullRequestCloseEvent: 'closed',
209 212 events.PullRequestMergeEvent: 'merged',
210 213 events.PullRequestUpdateEvent: 'updated',
211 214 events.PullRequestCreateEvent: 'created',
212 215 }.get(event.__class__, str(event.__class__))
213 216
214 217 return ('Pull request <{url}|#{number}> - {title} '
215 218 '{action} by {user}').format(
216 219 user=data['actor']['username'],
217 220 number=data['pullrequest']['pull_request_id'],
218 221 url=data['pullrequest']['url'],
219 222 title=data['pullrequest']['title'],
220 223 action=action
221 224 )
222 225
223 226 def format_repo_push_event(self, data):
224 227 result = repo_push_template.render(
225 228 data=data,
226 229 html_to_slack_links=html_to_slack_links,
227 230 )
228 231 return result
229 232
230 233 def format_repo_create_event(self, data):
231 234 return '<{}|{}> ({}) repository created by *{}*'.format(
232 235 data['repo']['url'],
233 236 data['repo']['repo_name'],
234 237 data['repo']['repo_type'],
235 238 data['actor']['username'],
236 239 )
237 240
238 241
239 242 def html_to_slack_links(message):
240 243 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
241 244 r'<\1|\2>', message)
242 245
243 246
244 247 @task(ignore_result=True)
245 248 def post_text_to_slack(settings, text):
246 249 log.debug('sending %s to slack %s' % (text, settings['service']))
247 250 resp = requests.post(settings['service'], json={
248 251 "channel": settings.get('channel', ''),
249 252 "username": settings.get('username', 'Rhodecode'),
250 253 "text": text,
251 254 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
252 255 })
253 256 resp.raise_for_status() # raise exception on a failed request
@@ -1,111 +1,117 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22
23 23 import deform
24 24 import logging
25 25 import requests
26 26 import colander
27 27 from celery.task import task
28 28 from mako.template import Template
29 29
30 30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import _
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34 log = logging.getLogger(__name__)
36 35
37 36
38 class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
37 class WebhookSettingsSchema(colander.Schema):
39 38 url = colander.SchemaNode(
40 39 colander.String(),
41 title=lazy_ugettext('Webhook URL'),
42 description=lazy_ugettext('URL of the webhook to receive POST event.'),
43 default='',
40 title=_('Webhook URL'),
41 description=_('URL of the webhook to receive POST event.'),
42 missing=colander.required,
43 required=True,
44 44 validator=colander.url,
45 45 widget=deform.widget.TextInputWidget(
46 46 placeholder='https://www.example.com/webhook'
47 47 ),
48 48 )
49 49 secret_token = colander.SchemaNode(
50 50 colander.String(),
51 title=lazy_ugettext('Secret Token'),
52 description=lazy_ugettext('String used to validate received payloads.'),
51 title=_('Secret Token'),
52 description=_('String used to validate received payloads.'),
53 53 default='',
54 missing='',
54 55 widget=deform.widget.TextInputWidget(
55 56 placeholder='secret_token'
56 57 ),
57 58 )
58 59
59 60
61
62
60 63 class WebhookIntegrationType(IntegrationTypeBase):
61 64 key = 'webhook'
62 display_name = lazy_ugettext('Webhook')
65 display_name = _('Webhook')
66 description = _('Post json events to a webhook endpoint')
67 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
68
63 69 valid_events = [
64 70 events.PullRequestCloseEvent,
65 71 events.PullRequestMergeEvent,
66 72 events.PullRequestUpdateEvent,
67 73 events.PullRequestCommentEvent,
68 74 events.PullRequestReviewEvent,
69 75 events.PullRequestCreateEvent,
70 76 events.RepoPushEvent,
71 77 events.RepoCreateEvent,
72 78 ]
73 79
74 80 def settings_schema(self):
75 81 schema = WebhookSettingsSchema()
76 82 schema.add(colander.SchemaNode(
77 83 colander.Set(),
78 84 widget=deform.widget.CheckboxChoiceWidget(
79 85 values=sorted(
80 86 [(e.name, e.display_name) for e in self.valid_events]
81 87 )
82 88 ),
83 89 description="Events activated for this integration",
84 90 name='events'
85 91 ))
86 92 return schema
87 93
88 94 def send_event(self, event):
89 95 log.debug('handling event %s with webhook integration %s',
90 96 event.name, self)
91 97
92 98 if event.__class__ not in self.valid_events:
93 99 log.debug('event not valid: %r' % event)
94 100 return
95 101
96 102 if event.name not in self.settings['events']:
97 103 log.debug('event ignored: %r' % event)
98 104 return
99 105
100 106 data = event.as_dict()
101 107 post_to_webhook(data, self.settings)
102 108
103 109
104 110 @task(ignore_result=True)
105 111 def post_to_webhook(data, settings):
106 112 log.debug('sending event:%s to webhook %s', data['name'], settings['url'])
107 113 resp = requests.post(settings['url'], json={
108 114 'token': settings['secret_token'],
109 115 'event': data
110 116 })
111 117 resp.raise_for_status() # raise exception on a failed request
@@ -1,299 +1,385 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 import colander
22 import logging
23 21 import pylons
24 22 import deform
23 import logging
24 import colander
25 import peppercorn
26 import webhelpers.paginate
25 27
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 29 from pyramid.renderers import render
28 30 from pyramid.response import Response
29 31
30 32 from rhodecode.lib import auth
31 33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.helpers import Page
32 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
33 37 from rhodecode.model.scm import ScmModel
34 38 from rhodecode.model.integration import IntegrationModel
35 39 from rhodecode.admin.navigation import navigation_list
36 40 from rhodecode.translation import _
37 41 from rhodecode.integrations import integration_type_registry
42 from rhodecode.model.validation_schema.schemas.integration_schema import (
43 make_integration_schema)
38 44
39 45 log = logging.getLogger(__name__)
40 46
41 47
42 48 class IntegrationSettingsViewBase(object):
43 49 """ Base Integration settings view used by both repo / global settings """
44 50
45 51 def __init__(self, context, request):
46 52 self.context = context
47 53 self.request = request
48 54 self._load_general_context()
49 55
50 56 if not self.perm_check(request.user):
51 57 raise HTTPForbidden()
52 58
53 59 def _load_general_context(self):
54 60 """
55 61 This avoids boilerplate for repo/global+list/edit+views/templates
56 62 by doing all possible contexts at the same time however it should
57 63 be split up into separate functions once more "contexts" exist
58 64 """
59 65
60 66 self.IntegrationType = None
61 67 self.repo = None
62 68 self.repo_group = None
63 69 self.integration = None
64 70 self.integrations = {}
65 71
66 72 request = self.request
67 73
68 if 'repo_name' in request.matchdict: # we're in a repo context
74 if 'repo_name' in request.matchdict: # in repo settings context
69 75 repo_name = request.matchdict['repo_name']
70 76 self.repo = Repository.get_by_repo_name(repo_name)
71 77
72 if 'repo_group_name' in request.matchdict: # we're in repo_group context
78 if 'repo_group_name' in request.matchdict: # in group settings context
73 79 repo_group_name = request.matchdict['repo_group_name']
74 80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
75 81
76 if 'integration' in request.matchdict: # we're in integration context
82
83 if 'integration' in request.matchdict: # integration type context
77 84 integration_type = request.matchdict['integration']
78 85 self.IntegrationType = integration_type_registry[integration_type]
79 86
80 87 if 'integration_id' in request.matchdict: # single integration context
81 88 integration_id = request.matchdict['integration_id']
82 89 self.integration = Integration.get(integration_id)
83 else: # list integrations context
84 integrations = IntegrationModel().get_integrations(
85 repo=self.repo, repo_group=self.repo_group)
86 90
87 for integration in integrations:
88 self.integrations.setdefault(integration.integration_type, []
89 ).append(integration)
91 # extra perms check just in case
92 if not self._has_perms_for_integration(self.integration):
93 raise HTTPForbidden()
90 94
91 95 self.settings = self.integration and self.integration.settings or {}
96 self.admin_view = not (self.repo or self.repo_group)
97
98 def _has_perms_for_integration(self, integration):
99 perms = self.request.user.permissions
100
101 if 'hg.admin' in perms['global']:
102 return True
103
104 if integration.repo:
105 return perms['repositories'].get(
106 integration.repo.repo_name) == 'repository.admin'
107
108 if integration.repo_group:
109 return perms['repositories_groups'].get(
110 integration.repo_group.group_name) == 'group.admin'
111
112 return False
92 113
93 114 def _template_c_context(self):
94 115 # TODO: dan: this is a stopgap in order to inherit from current pylons
95 116 # based admin/repo settings templates - this should be removed entirely
96 117 # after port to pyramid
97 118
98 119 c = pylons.tmpl_context
99 120 c.active = 'integrations'
100 121 c.rhodecode_user = self.request.user
101 122 c.repo = self.repo
102 123 c.repo_group = self.repo_group
103 124 c.repo_name = self.repo and self.repo.repo_name or None
104 125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126
105 127 if self.repo:
106 128 c.repo_info = self.repo
107 129 c.rhodecode_db_repo = self.repo
108 130 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
109 131 else:
110 132 c.navlist = navigation_list(self.request)
111 133
112 134 return c
113 135
114 136 def _form_schema(self):
115 if self.integration:
116 settings = self.integration.settings
117 else:
118 settings = {}
119 return self.IntegrationType(settings=settings).settings_schema()
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
138 settings=self.settings)
120 139
121 def settings_get(self, defaults=None, errors=None, form=None):
122 """
123 View that displays the plugin settings as a form.
124 """
125 defaults = defaults or {}
126 errors = errors or {}
140 # returns a clone, important if mutating the schema later
141 return schema.bind(
142 permissions=self.request.user.permissions,
143 no_scope=not self.admin_view)
144
145
146 def _form_defaults(self):
147 defaults = {}
127 148
128 149 if self.integration:
129 defaults = self.integration.settings or {}
130 defaults['name'] = self.integration.name
131 defaults['enabled'] = self.integration.enabled
150 defaults['settings'] = self.integration.settings or {}
151 defaults['options'] = {
152 'name': self.integration.name,
153 'enabled': self.integration.enabled,
154 'scope': self.integration.scope,
155 }
132 156 else:
133 157 if self.repo:
134 158 scope = _('{repo_name} repository').format(
135 159 repo_name=self.repo.repo_name)
136 160 elif self.repo_group:
137 161 scope = _('{repo_group_name} repo group').format(
138 162 repo_group_name=self.repo_group.group_name)
139 163 else:
140 164 scope = _('Global')
141 165
142 defaults['name'] = '{} {} integration'.format(scope,
143 self.IntegrationType.display_name)
144 defaults['enabled'] = True
166 defaults['options'] = {
167 'enabled': True,
168 'name': _('{name} integration').format(
169 name=self.IntegrationType.display_name),
170 }
171 if self.repo:
172 defaults['options']['scope'] = self.repo
173 elif self.repo_group:
174 defaults['options']['scope'] = self.repo_group
175
176 return defaults
145 177
146 schema = self._form_schema().bind(request=self.request)
178 def _delete_integration(self, integration):
179 Session().delete(self.integration)
180 Session().commit()
181 self.request.session.flash(
182 _('Integration {integration_name} deleted successfully.').format(
183 integration_name=self.integration.name),
184 queue='success')
185
186 if self.repo:
187 redirect_to = self.request.route_url(
188 'repo_integrations_home', repo_name=self.repo.repo_name)
189 elif self.repo_group:
190 redirect_to = self.request.route_url(
191 'repo_group_integrations_home',
192 repo_group_name=self.repo_group.group_name)
193 else:
194 redirect_to = self.request.route_url('global_integrations_home')
195 raise HTTPFound(redirect_to)
196
197 def settings_get(self, defaults=None, form=None):
198 """
199 View that displays the integration settings as a form.
200 """
201
202 defaults = defaults or self._form_defaults()
203 schema = self._form_schema()
147 204
148 205 if self.integration:
149 206 buttons = ('submit', 'delete')
150 207 else:
151 208 buttons = ('submit',)
152 209
153 210 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
154 211
155 for node in schema:
156 setting = self.settings.get(node.name)
157 if setting is not None:
158 defaults.setdefault(node.name, setting)
159 else:
160 if node.default:
161 defaults.setdefault(node.name, node.default)
162
163 212 template_context = {
164 213 'form': form,
165 'defaults': defaults,
166 'errors': errors,
167 'schema': schema,
168 214 'current_IntegrationType': self.IntegrationType,
169 215 'integration': self.integration,
170 'settings': self.settings,
171 'resource': self.context,
172 216 'c': self._template_c_context(),
173 217 }
174 218
175 219 return template_context
176 220
177 221 @auth.CSRFRequired()
178 222 def settings_post(self):
179 223 """
180 View that validates and stores the plugin settings.
224 View that validates and stores the integration settings.
181 225 """
182 if self.request.params.get('delete'):
183 Session().delete(self.integration)
184 Session().commit()
185 self.request.session.flash(
186 _('Integration {integration_name} deleted successfully.').format(
187 integration_name=self.integration.name),
188 queue='success')
189 if self.repo:
190 redirect_to = self.request.route_url(
191 'repo_integrations_home', repo_name=self.repo.repo_name)
192 else:
193 redirect_to = self.request.route_url('global_integrations_home')
194 raise HTTPFound(redirect_to)
226 controls = self.request.POST.items()
227 pstruct = peppercorn.parse(controls)
228
229 if self.integration and pstruct.get('delete'):
230 return self._delete_integration(self.integration)
231
232 schema = self._form_schema()
233
234 skip_settings_validation = False
235 if self.integration and 'enabled' not in pstruct.get('options', {}):
236 skip_settings_validation = True
237 schema['settings'].validator = None
238 for field in schema['settings'].children:
239 field.validator = None
240 field.missing = ''
195 241
196 schema = self._form_schema().bind(request=self.request)
242 if self.integration:
243 buttons = ('submit', 'delete')
244 else:
245 buttons = ('submit',)
197 246
198 form = deform.Form(schema, buttons=('submit', 'delete'))
247 form = deform.Form(schema, buttons=buttons)
199 248
200 params = {}
201 for node in schema.children:
202 if type(node.typ) in (colander.Set, colander.List):
203 val = self.request.params.getall(node.name)
204 else:
205 val = self.request.params.get(node.name)
206 if val:
207 params[node.name] = val
249 if not self.admin_view:
250 # scope is read only field in these cases, and has to be added
251 options = pstruct.setdefault('options', {})
252 if 'scope' not in options:
253 if self.repo:
254 options['scope'] = 'repo:{}'.format(self.repo.repo_name)
255 elif self.repo_group:
256 options['scope'] = 'repogroup:{}'.format(
257 self.repo_group.group_name)
208 258
209 controls = self.request.POST.items()
210 259 try:
211 valid_data = form.validate(controls)
260 valid_data = form.validate_pstruct(pstruct)
212 261 except deform.ValidationFailure as e:
213 262 self.request.session.flash(
214 263 _('Errors exist when saving integration settings. '
215 264 'Please check the form inputs.'),
216 265 queue='error')
217 return self.settings_get(errors={}, defaults=params, form=e)
266 return self.settings_get(form=e)
218 267
219 268 if not self.integration:
220 269 self.integration = Integration()
221 270 self.integration.integration_type = self.IntegrationType.key
222 if self.repo:
223 self.integration.repo = self.repo
224 elif self.repo_group:
225 self.integration.repo_group = self.repo_group
226 271 Session().add(self.integration)
227 272
228 self.integration.enabled = valid_data.pop('enabled', False)
229 self.integration.name = valid_data.pop('name')
230 self.integration.settings = valid_data
273 scope = valid_data['options']['scope']
231 274
275 IntegrationModel().update_integration(self.integration,
276 name=valid_data['options']['name'],
277 enabled=valid_data['options']['enabled'],
278 settings=valid_data['settings'],
279 scope=scope)
280
281 self.integration.settings = valid_data['settings']
232 282 Session().commit()
233
234 283 # Display success message and redirect.
235 284 self.request.session.flash(
236 285 _('Integration {integration_name} updated successfully.').format(
237 286 integration_name=self.IntegrationType.display_name),
238 287 queue='success')
239 288
240 if self.repo:
241 redirect_to = self.request.route_url(
242 'repo_integrations_edit', repo_name=self.repo.repo_name,
289
290 # if integration scope changes, we must redirect to the right place
291 # keeping in mind if the original view was for /repo/ or /_admin/
292 admin_view = not (self.repo or self.repo_group)
293
294 if isinstance(self.integration.scope, Repository) and not admin_view:
295 redirect_to = self.request.route_path(
296 'repo_integrations_edit',
297 repo_name=self.integration.scope.repo_name,
243 298 integration=self.integration.integration_type,
244 299 integration_id=self.integration.integration_id)
245 elif self.repo:
246 redirect_to = self.request.route_url(
300 elif isinstance(self.integration.scope, RepoGroup) and not admin_view:
301 redirect_to = self.request.route_path(
247 302 'repo_group_integrations_edit',
248 repo_group_name=self.repo_group.group_name,
303 repo_group_name=self.integration.scope.group_name,
249 304 integration=self.integration.integration_type,
250 305 integration_id=self.integration.integration_id)
251 306 else:
252 redirect_to = self.request.route_url(
307 redirect_to = self.request.route_path(
253 308 'global_integrations_edit',
254 309 integration=self.integration.integration_type,
255 310 integration_id=self.integration.integration_id)
256 311
257 312 return HTTPFound(redirect_to)
258 313
259 314 def index(self):
260 current_integrations = self.integrations
261 if self.IntegrationType:
262 current_integrations = {
263 self.IntegrationType.key: self.integrations.get(
264 self.IntegrationType.key, [])
265 }
315 """ List integrations """
316 if self.repo:
317 scope = self.repo
318 elif self.repo_group:
319 scope = self.repo_group
320 else:
321 scope = 'all'
322
323 integrations = []
324
325 for integration in IntegrationModel().get_integrations(
326 scope=scope, IntegrationType=self.IntegrationType):
327
328 # extra permissions check *just in case*
329 if not self._has_perms_for_integration(integration):
330 continue
331 integrations.append(integration)
332
333 sort_arg = self.request.GET.get('sort', 'name:asc')
334 if ':' in sort_arg:
335 sort_field, sort_dir = sort_arg.split(':')
336 else:
337 sort_field = sort_arg, 'asc'
338
339 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
340
341 integrations.sort(
342 key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
343
344
345 page_url = webhelpers.paginate.PageURL(
346 self.request.path, self.request.GET)
347 page = safe_int(self.request.GET.get('page', 1), 1)
348
349 integrations = Page(integrations, page=page, items_per_page=10,
350 url=page_url)
266 351
267 352 template_context = {
353 'sort_field': sort_field,
354 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
268 355 'current_IntegrationType': self.IntegrationType,
269 'current_integrations': current_integrations,
356 'integrations_list': integrations,
270 357 'available_integrations': integration_type_registry,
271 'c': self._template_c_context()
358 'c': self._template_c_context(),
359 'request': self.request,
272 360 }
361 return template_context
273 362
274 if self.repo:
275 html = render('rhodecode:templates/admin/integrations/list.html',
276 template_context,
277 request=self.request)
278 else:
279 html = render('rhodecode:templates/admin/integrations/list.html',
280 template_context,
281 request=self.request)
282
283 return Response(html)
284
363 def new_integration(self):
364 template_context = {
365 'available_integrations': integration_type_registry,
366 'c': self._template_c_context(),
367 }
368 return template_context
285 369
286 370 class GlobalIntegrationsView(IntegrationSettingsViewBase):
287 371 def perm_check(self, user):
288 372 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
289 373
290 374
291 375 class RepoIntegrationsView(IntegrationSettingsViewBase):
292 376 def perm_check(self, user):
293 377 return auth.HasRepoPermissionAll('repository.admin'
294 378 )(repo_name=self.repo.repo_name, user=user)
295 379
380
296 381 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
297 382 def perm_check(self, user):
298 383 return auth.HasRepoGroupPermissionAll('group.admin'
299 384 )(group_name=self.repo_group.group_name, user=user)
385
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/templates/admin/integrations/edit.html to rhodecode/templates/admin/integrations/form.html
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now