##// END OF EJS Templates
comments: allow submitting id of comment which submitted comment resolved....
marcink -
r1325:b4535cc7 default
parent child Browse files
Show More
@@ -1,1167 +1,1167 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 392 action='user_group_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_supervisor', '/settings/supervisor',
507 507 action='settings_supervisor', conditions={'method': ['GET']})
508 508 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
509 509 action='settings_supervisor_log', conditions={'method': ['GET']})
510 510
511 511 m.connect('admin_settings_labs', '/settings/labs',
512 512 action='settings_labs_update',
513 513 conditions={'method': ['POST']})
514 514 m.connect('admin_settings_labs', '/settings/labs',
515 515 action='settings_labs', conditions={'method': ['GET']})
516 516
517 517 # ADMIN MY ACCOUNT
518 518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 519 controller='admin/my_account') as m:
520 520
521 521 m.connect('my_account', '/my_account',
522 522 action='my_account', conditions={'method': ['GET']})
523 523 m.connect('my_account_edit', '/my_account/edit',
524 524 action='my_account_edit', conditions={'method': ['GET']})
525 525 m.connect('my_account', '/my_account',
526 526 action='my_account_update', conditions={'method': ['POST']})
527 527
528 528 m.connect('my_account_password', '/my_account/password',
529 529 action='my_account_password', conditions={'method': ['GET', 'POST']})
530 530
531 531 m.connect('my_account_repos', '/my_account/repos',
532 532 action='my_account_repos', conditions={'method': ['GET']})
533 533
534 534 m.connect('my_account_watched', '/my_account/watched',
535 535 action='my_account_watched', conditions={'method': ['GET']})
536 536
537 537 m.connect('my_account_pullrequests', '/my_account/pull_requests',
538 538 action='my_account_pullrequests', conditions={'method': ['GET']})
539 539
540 540 m.connect('my_account_perms', '/my_account/perms',
541 541 action='my_account_perms', conditions={'method': ['GET']})
542 542
543 543 m.connect('my_account_emails', '/my_account/emails',
544 544 action='my_account_emails', conditions={'method': ['GET']})
545 545 m.connect('my_account_emails', '/my_account/emails',
546 546 action='my_account_emails_add', conditions={'method': ['POST']})
547 547 m.connect('my_account_emails', '/my_account/emails',
548 548 action='my_account_emails_delete', conditions={'method': ['DELETE']})
549 549
550 550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
551 551 action='my_account_auth_tokens', conditions={'method': ['GET']})
552 552 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
553 553 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
554 554 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
555 555 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
556 556 m.connect('my_account_notifications', '/my_account/notifications',
557 557 action='my_notifications',
558 558 conditions={'method': ['GET']})
559 559 m.connect('my_account_notifications_toggle_visibility',
560 560 '/my_account/toggle_visibility',
561 561 action='my_notifications_toggle_visibility',
562 562 conditions={'method': ['POST']})
563 563 m.connect('my_account_notifications_test_channelstream',
564 564 '/my_account/test_channelstream',
565 565 action='my_account_notifications_test_channelstream',
566 566 conditions={'method': ['POST']})
567 567
568 568 # NOTIFICATION REST ROUTES
569 569 with rmap.submapper(path_prefix=ADMIN_PREFIX,
570 570 controller='admin/notifications') as m:
571 571 m.connect('notifications', '/notifications',
572 572 action='index', conditions={'method': ['GET']})
573 573 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
574 574 action='mark_all_read', conditions={'method': ['POST']})
575 575 m.connect('/notifications/{notification_id}',
576 576 action='update', conditions={'method': ['PUT']})
577 577 m.connect('/notifications/{notification_id}',
578 578 action='delete', conditions={'method': ['DELETE']})
579 579 m.connect('notification', '/notifications/{notification_id}',
580 580 action='show', conditions={'method': ['GET']})
581 581
582 582 # ADMIN GIST
583 583 with rmap.submapper(path_prefix=ADMIN_PREFIX,
584 584 controller='admin/gists') as m:
585 585 m.connect('gists', '/gists',
586 586 action='create', conditions={'method': ['POST']})
587 587 m.connect('gists', '/gists', jsroute=True,
588 588 action='index', conditions={'method': ['GET']})
589 589 m.connect('new_gist', '/gists/new', jsroute=True,
590 590 action='new', conditions={'method': ['GET']})
591 591
592 592 m.connect('/gists/{gist_id}',
593 593 action='delete', conditions={'method': ['DELETE']})
594 594 m.connect('edit_gist', '/gists/{gist_id}/edit',
595 595 action='edit_form', conditions={'method': ['GET']})
596 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 597 action='edit', conditions={'method': ['POST']})
598 598 m.connect(
599 599 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
600 600 action='check_revision', conditions={'method': ['GET']})
601 601
602 602 m.connect('gist', '/gists/{gist_id}',
603 603 action='show', conditions={'method': ['GET']})
604 604 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
605 605 revision='tip',
606 606 action='show', conditions={'method': ['GET']})
607 607 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
608 608 revision='tip',
609 609 action='show', conditions={'method': ['GET']})
610 610 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
611 611 revision='tip',
612 612 action='show', conditions={'method': ['GET']},
613 613 requirements=URL_NAME_REQUIREMENTS)
614 614
615 615 # ADMIN MAIN PAGES
616 616 with rmap.submapper(path_prefix=ADMIN_PREFIX,
617 617 controller='admin/admin') as m:
618 618 m.connect('admin_home', '', action='index')
619 619 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
620 620 action='add_repo')
621 621 m.connect(
622 622 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
623 623 action='pull_requests')
624 624 m.connect(
625 625 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
626 626 action='pull_requests')
627 627 m.connect(
628 628 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
629 629 action='pull_requests')
630 630
631 631 # USER JOURNAL
632 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 633 controller='journal', action='index')
634 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 635 controller='journal', action='journal_rss')
636 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 637 controller='journal', action='journal_atom')
638 638
639 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 640 controller='journal', action='public_journal')
641 641
642 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 643 controller='journal', action='public_journal_rss')
644 644
645 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 646 controller='journal', action='public_journal_rss')
647 647
648 648 rmap.connect('public_journal_atom',
649 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 650 action='public_journal_atom')
651 651
652 652 rmap.connect('public_journal_atom_old',
653 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 654 action='public_journal_atom')
655 655
656 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 657 controller='journal', action='toggle_following', jsroute=True,
658 658 conditions={'method': ['POST']})
659 659
660 660 # FULL TEXT SEARCH
661 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 662 controller='search')
663 663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 664 controller='search',
665 665 action='index',
666 666 conditions={'function': check_repo},
667 667 requirements=URL_NAME_REQUIREMENTS)
668 668
669 669 # FEEDS
670 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 671 controller='feed', action='rss',
672 672 conditions={'function': check_repo},
673 673 requirements=URL_NAME_REQUIREMENTS)
674 674
675 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 676 controller='feed', action='atom',
677 677 conditions={'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679
680 680 #==========================================================================
681 681 # REPOSITORY ROUTES
682 682 #==========================================================================
683 683
684 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 685 controller='admin/repos', action='repo_creating',
686 686 requirements=URL_NAME_REQUIREMENTS)
687 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 688 controller='admin/repos', action='repo_check',
689 689 requirements=URL_NAME_REQUIREMENTS)
690 690
691 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 692 controller='summary', action='repo_stats',
693 693 conditions={'function': check_repo},
694 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 695
696 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 controller='summary', action='repo_refs_data', jsroute=True,
698 requirements=URL_NAME_REQUIREMENTS)
697 controller='summary', action='repo_refs_data',
698 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 703 controller='summary', action='repo_default_reviewers_data',
704 704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
705 705
706 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
707 controller='changeset', revision='tip', jsroute=True,
707 controller='changeset', revision='tip',
708 708 conditions={'function': check_repo},
709 requirements=URL_NAME_REQUIREMENTS)
709 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
710 710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
711 711 controller='changeset', revision='tip', action='changeset_children',
712 712 conditions={'function': check_repo},
713 713 requirements=URL_NAME_REQUIREMENTS)
714 714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
715 715 controller='changeset', revision='tip', action='changeset_parents',
716 716 conditions={'function': check_repo},
717 717 requirements=URL_NAME_REQUIREMENTS)
718 718
719 719 # repo edit options
720 720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
721 721 controller='admin/repos', action='edit',
722 722 conditions={'method': ['GET'], 'function': check_repo},
723 723 requirements=URL_NAME_REQUIREMENTS)
724 724
725 725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
726 726 jsroute=True,
727 727 controller='admin/repos', action='edit_permissions',
728 728 conditions={'method': ['GET'], 'function': check_repo},
729 729 requirements=URL_NAME_REQUIREMENTS)
730 730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
731 731 controller='admin/repos', action='edit_permissions_update',
732 732 conditions={'method': ['PUT'], 'function': check_repo},
733 733 requirements=URL_NAME_REQUIREMENTS)
734 734
735 735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
736 736 controller='admin/repos', action='edit_fields',
737 737 conditions={'method': ['GET'], 'function': check_repo},
738 738 requirements=URL_NAME_REQUIREMENTS)
739 739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
740 740 controller='admin/repos', action='create_repo_field',
741 741 conditions={'method': ['PUT'], 'function': check_repo},
742 742 requirements=URL_NAME_REQUIREMENTS)
743 743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
744 744 controller='admin/repos', action='delete_repo_field',
745 745 conditions={'method': ['DELETE'], 'function': check_repo},
746 746 requirements=URL_NAME_REQUIREMENTS)
747 747
748 748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
749 749 controller='admin/repos', action='edit_advanced',
750 750 conditions={'method': ['GET'], 'function': check_repo},
751 751 requirements=URL_NAME_REQUIREMENTS)
752 752
753 753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
754 754 controller='admin/repos', action='edit_advanced_locking',
755 755 conditions={'method': ['PUT'], 'function': check_repo},
756 756 requirements=URL_NAME_REQUIREMENTS)
757 757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
758 758 controller='admin/repos', action='toggle_locking',
759 759 conditions={'method': ['GET'], 'function': check_repo},
760 760 requirements=URL_NAME_REQUIREMENTS)
761 761
762 762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
763 763 controller='admin/repos', action='edit_advanced_journal',
764 764 conditions={'method': ['PUT'], 'function': check_repo},
765 765 requirements=URL_NAME_REQUIREMENTS)
766 766
767 767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
768 768 controller='admin/repos', action='edit_advanced_fork',
769 769 conditions={'method': ['PUT'], 'function': check_repo},
770 770 requirements=URL_NAME_REQUIREMENTS)
771 771
772 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 773 controller='admin/repos', action='edit_caches_form',
774 774 conditions={'method': ['GET'], 'function': check_repo},
775 775 requirements=URL_NAME_REQUIREMENTS)
776 776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
777 777 controller='admin/repos', action='edit_caches',
778 778 conditions={'method': ['PUT'], 'function': check_repo},
779 779 requirements=URL_NAME_REQUIREMENTS)
780 780
781 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 782 controller='admin/repos', action='edit_remote_form',
783 783 conditions={'method': ['GET'], 'function': check_repo},
784 784 requirements=URL_NAME_REQUIREMENTS)
785 785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
786 786 controller='admin/repos', action='edit_remote',
787 787 conditions={'method': ['PUT'], 'function': check_repo},
788 788 requirements=URL_NAME_REQUIREMENTS)
789 789
790 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 791 controller='admin/repos', action='edit_statistics_form',
792 792 conditions={'method': ['GET'], 'function': check_repo},
793 793 requirements=URL_NAME_REQUIREMENTS)
794 794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
795 795 controller='admin/repos', action='edit_statistics',
796 796 conditions={'method': ['PUT'], 'function': check_repo},
797 797 requirements=URL_NAME_REQUIREMENTS)
798 798 rmap.connect('repo_settings_issuetracker',
799 799 '/{repo_name}/settings/issue-tracker',
800 800 controller='admin/repos', action='repo_issuetracker',
801 801 conditions={'method': ['GET'], 'function': check_repo},
802 802 requirements=URL_NAME_REQUIREMENTS)
803 803 rmap.connect('repo_issuetracker_test',
804 804 '/{repo_name}/settings/issue-tracker/test',
805 805 controller='admin/repos', action='repo_issuetracker_test',
806 806 conditions={'method': ['POST'], 'function': check_repo},
807 807 requirements=URL_NAME_REQUIREMENTS)
808 808 rmap.connect('repo_issuetracker_delete',
809 809 '/{repo_name}/settings/issue-tracker/delete',
810 810 controller='admin/repos', action='repo_issuetracker_delete',
811 811 conditions={'method': ['DELETE'], 'function': check_repo},
812 812 requirements=URL_NAME_REQUIREMENTS)
813 813 rmap.connect('repo_issuetracker_save',
814 814 '/{repo_name}/settings/issue-tracker/save',
815 815 controller='admin/repos', action='repo_issuetracker_save',
816 816 conditions={'method': ['POST'], 'function': check_repo},
817 817 requirements=URL_NAME_REQUIREMENTS)
818 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 819 controller='admin/repos', action='repo_settings_vcs_update',
820 820 conditions={'method': ['POST'], 'function': check_repo},
821 821 requirements=URL_NAME_REQUIREMENTS)
822 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 823 controller='admin/repos', action='repo_settings_vcs',
824 824 conditions={'method': ['GET'], 'function': check_repo},
825 825 requirements=URL_NAME_REQUIREMENTS)
826 826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
827 827 controller='admin/repos', action='repo_delete_svn_pattern',
828 828 conditions={'method': ['DELETE'], 'function': check_repo},
829 829 requirements=URL_NAME_REQUIREMENTS)
830 830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 831 controller='admin/repos', action='repo_settings_pullrequest',
832 832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 833 requirements=URL_NAME_REQUIREMENTS)
834 834
835 835 # still working url for backward compat.
836 836 rmap.connect('raw_changeset_home_depraced',
837 837 '/{repo_name}/raw-changeset/{revision}',
838 838 controller='changeset', action='changeset_raw',
839 839 revision='tip', conditions={'function': check_repo},
840 840 requirements=URL_NAME_REQUIREMENTS)
841 841
842 842 # new URLs
843 843 rmap.connect('changeset_raw_home',
844 844 '/{repo_name}/changeset-diff/{revision}',
845 845 controller='changeset', action='changeset_raw',
846 846 revision='tip', conditions={'function': check_repo},
847 847 requirements=URL_NAME_REQUIREMENTS)
848 848
849 849 rmap.connect('changeset_patch_home',
850 850 '/{repo_name}/changeset-patch/{revision}',
851 851 controller='changeset', action='changeset_patch',
852 852 revision='tip', conditions={'function': check_repo},
853 853 requirements=URL_NAME_REQUIREMENTS)
854 854
855 855 rmap.connect('changeset_download_home',
856 856 '/{repo_name}/changeset-download/{revision}',
857 857 controller='changeset', action='changeset_download',
858 858 revision='tip', conditions={'function': check_repo},
859 859 requirements=URL_NAME_REQUIREMENTS)
860 860
861 861 rmap.connect('changeset_comment',
862 862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
863 863 controller='changeset', revision='tip', action='comment',
864 864 conditions={'function': check_repo},
865 865 requirements=URL_NAME_REQUIREMENTS)
866 866
867 867 rmap.connect('changeset_comment_preview',
868 868 '/{repo_name}/changeset/comment/preview', jsroute=True,
869 869 controller='changeset', action='preview_comment',
870 870 conditions={'function': check_repo, 'method': ['POST']},
871 871 requirements=URL_NAME_REQUIREMENTS)
872 872
873 873 rmap.connect('changeset_comment_delete',
874 874 '/{repo_name}/changeset/comment/{comment_id}/delete',
875 875 controller='changeset', action='delete_comment',
876 876 conditions={'function': check_repo, 'method': ['DELETE']},
877 877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878 878
879 879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
880 880 controller='changeset', action='changeset_info',
881 881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882 882
883 883 rmap.connect('compare_home',
884 884 '/{repo_name}/compare',
885 885 controller='compare', action='index',
886 886 conditions={'function': check_repo},
887 887 requirements=URL_NAME_REQUIREMENTS)
888 888
889 889 rmap.connect('compare_url',
890 890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
891 891 controller='compare', action='compare',
892 892 conditions={'function': check_repo},
893 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894 894
895 895 rmap.connect('pullrequest_home',
896 896 '/{repo_name}/pull-request/new', controller='pullrequests',
897 897 action='index', conditions={'function': check_repo,
898 898 'method': ['GET']},
899 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900 900
901 901 rmap.connect('pullrequest',
902 902 '/{repo_name}/pull-request/new', controller='pullrequests',
903 903 action='create', conditions={'function': check_repo,
904 904 'method': ['POST']},
905 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 906
907 907 rmap.connect('pullrequest_repo_refs',
908 908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
909 909 controller='pullrequests',
910 910 action='get_repo_refs',
911 911 conditions={'function': check_repo, 'method': ['GET']},
912 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913 913
914 914 rmap.connect('pullrequest_repo_destinations',
915 915 '/{repo_name}/pull-request/repo-destinations',
916 916 controller='pullrequests',
917 917 action='get_repo_destinations',
918 918 conditions={'function': check_repo, 'method': ['GET']},
919 919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
920 920
921 921 rmap.connect('pullrequest_show',
922 922 '/{repo_name}/pull-request/{pull_request_id}',
923 923 controller='pullrequests',
924 924 action='show', conditions={'function': check_repo,
925 925 'method': ['GET']},
926 requirements=URL_NAME_REQUIREMENTS)
926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927 927
928 928 rmap.connect('pullrequest_update',
929 929 '/{repo_name}/pull-request/{pull_request_id}',
930 930 controller='pullrequests',
931 931 action='update', conditions={'function': check_repo,
932 932 'method': ['PUT']},
933 933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
934 934
935 935 rmap.connect('pullrequest_merge',
936 936 '/{repo_name}/pull-request/{pull_request_id}',
937 937 controller='pullrequests',
938 938 action='merge', conditions={'function': check_repo,
939 939 'method': ['POST']},
940 940 requirements=URL_NAME_REQUIREMENTS)
941 941
942 942 rmap.connect('pullrequest_delete',
943 943 '/{repo_name}/pull-request/{pull_request_id}',
944 944 controller='pullrequests',
945 945 action='delete', conditions={'function': check_repo,
946 946 'method': ['DELETE']},
947 947 requirements=URL_NAME_REQUIREMENTS)
948 948
949 949 rmap.connect('pullrequest_show_all',
950 950 '/{repo_name}/pull-request',
951 951 controller='pullrequests',
952 952 action='show_all', conditions={'function': check_repo,
953 953 'method': ['GET']},
954 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955 955
956 956 rmap.connect('pullrequest_comment',
957 957 '/{repo_name}/pull-request-comment/{pull_request_id}',
958 958 controller='pullrequests',
959 959 action='comment', conditions={'function': check_repo,
960 960 'method': ['POST']},
961 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
962 962
963 963 rmap.connect('pullrequest_comment_delete',
964 964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
965 965 controller='pullrequests', action='delete_comment',
966 966 conditions={'function': check_repo, 'method': ['DELETE']},
967 967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
968 968
969 969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
970 970 controller='summary', conditions={'function': check_repo},
971 971 requirements=URL_NAME_REQUIREMENTS)
972 972
973 973 rmap.connect('branches_home', '/{repo_name}/branches',
974 974 controller='branches', conditions={'function': check_repo},
975 975 requirements=URL_NAME_REQUIREMENTS)
976 976
977 977 rmap.connect('tags_home', '/{repo_name}/tags',
978 978 controller='tags', conditions={'function': check_repo},
979 979 requirements=URL_NAME_REQUIREMENTS)
980 980
981 981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
982 982 controller='bookmarks', conditions={'function': check_repo},
983 983 requirements=URL_NAME_REQUIREMENTS)
984 984
985 985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
986 986 controller='changelog', conditions={'function': check_repo},
987 987 requirements=URL_NAME_REQUIREMENTS)
988 988
989 989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
990 990 controller='changelog', action='changelog_summary',
991 991 conditions={'function': check_repo},
992 992 requirements=URL_NAME_REQUIREMENTS)
993 993
994 994 rmap.connect('changelog_file_home',
995 995 '/{repo_name}/changelog/{revision}/{f_path}',
996 996 controller='changelog', f_path=None,
997 997 conditions={'function': check_repo},
998 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
999 999
1000 1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
1001 1001 controller='changelog', action='changelog_details',
1002 1002 conditions={'function': check_repo},
1003 1003 requirements=URL_NAME_REQUIREMENTS)
1004 1004
1005 1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1006 1006 controller='files', revision='tip', f_path='',
1007 1007 conditions={'function': check_repo},
1008 1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1009 1009
1010 1010 rmap.connect('files_home_simple_catchrev',
1011 1011 '/{repo_name}/files/{revision}',
1012 1012 controller='files', revision='tip', f_path='',
1013 1013 conditions={'function': check_repo},
1014 1014 requirements=URL_NAME_REQUIREMENTS)
1015 1015
1016 1016 rmap.connect('files_home_simple_catchall',
1017 1017 '/{repo_name}/files',
1018 1018 controller='files', revision='tip', f_path='',
1019 1019 conditions={'function': check_repo},
1020 1020 requirements=URL_NAME_REQUIREMENTS)
1021 1021
1022 1022 rmap.connect('files_history_home',
1023 1023 '/{repo_name}/history/{revision}/{f_path}',
1024 1024 controller='files', action='history', revision='tip', f_path='',
1025 1025 conditions={'function': check_repo},
1026 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1027 1027
1028 1028 rmap.connect('files_authors_home',
1029 1029 '/{repo_name}/authors/{revision}/{f_path}',
1030 1030 controller='files', action='authors', revision='tip', f_path='',
1031 1031 conditions={'function': check_repo},
1032 1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1033 1033
1034 1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1035 1035 controller='files', action='diff', f_path='',
1036 1036 conditions={'function': check_repo},
1037 1037 requirements=URL_NAME_REQUIREMENTS)
1038 1038
1039 1039 rmap.connect('files_diff_2way_home',
1040 1040 '/{repo_name}/diff-2way/{f_path}',
1041 1041 controller='files', action='diff_2way', f_path='',
1042 1042 conditions={'function': check_repo},
1043 1043 requirements=URL_NAME_REQUIREMENTS)
1044 1044
1045 1045 rmap.connect('files_rawfile_home',
1046 1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1047 1047 controller='files', action='rawfile', revision='tip',
1048 1048 f_path='', conditions={'function': check_repo},
1049 1049 requirements=URL_NAME_REQUIREMENTS)
1050 1050
1051 1051 rmap.connect('files_raw_home',
1052 1052 '/{repo_name}/raw/{revision}/{f_path}',
1053 1053 controller='files', action='raw', revision='tip', f_path='',
1054 1054 conditions={'function': check_repo},
1055 1055 requirements=URL_NAME_REQUIREMENTS)
1056 1056
1057 1057 rmap.connect('files_render_home',
1058 1058 '/{repo_name}/render/{revision}/{f_path}',
1059 1059 controller='files', action='index', revision='tip', f_path='',
1060 1060 rendered=True, conditions={'function': check_repo},
1061 1061 requirements=URL_NAME_REQUIREMENTS)
1062 1062
1063 1063 rmap.connect('files_annotate_home',
1064 1064 '/{repo_name}/annotate/{revision}/{f_path}',
1065 1065 controller='files', action='index', revision='tip',
1066 1066 f_path='', annotate=True, conditions={'function': check_repo},
1067 1067 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1068 1068
1069 1069 rmap.connect('files_edit',
1070 1070 '/{repo_name}/edit/{revision}/{f_path}',
1071 1071 controller='files', action='edit', revision='tip',
1072 1072 f_path='',
1073 1073 conditions={'function': check_repo, 'method': ['POST']},
1074 1074 requirements=URL_NAME_REQUIREMENTS)
1075 1075
1076 1076 rmap.connect('files_edit_home',
1077 1077 '/{repo_name}/edit/{revision}/{f_path}',
1078 1078 controller='files', action='edit_home', revision='tip',
1079 1079 f_path='', conditions={'function': check_repo},
1080 1080 requirements=URL_NAME_REQUIREMENTS)
1081 1081
1082 1082 rmap.connect('files_add',
1083 1083 '/{repo_name}/add/{revision}/{f_path}',
1084 1084 controller='files', action='add', revision='tip',
1085 1085 f_path='',
1086 1086 conditions={'function': check_repo, 'method': ['POST']},
1087 1087 requirements=URL_NAME_REQUIREMENTS)
1088 1088
1089 1089 rmap.connect('files_add_home',
1090 1090 '/{repo_name}/add/{revision}/{f_path}',
1091 1091 controller='files', action='add_home', revision='tip',
1092 1092 f_path='', conditions={'function': check_repo},
1093 1093 requirements=URL_NAME_REQUIREMENTS)
1094 1094
1095 1095 rmap.connect('files_delete',
1096 1096 '/{repo_name}/delete/{revision}/{f_path}',
1097 1097 controller='files', action='delete', revision='tip',
1098 1098 f_path='',
1099 1099 conditions={'function': check_repo, 'method': ['POST']},
1100 1100 requirements=URL_NAME_REQUIREMENTS)
1101 1101
1102 1102 rmap.connect('files_delete_home',
1103 1103 '/{repo_name}/delete/{revision}/{f_path}',
1104 1104 controller='files', action='delete_home', revision='tip',
1105 1105 f_path='', conditions={'function': check_repo},
1106 1106 requirements=URL_NAME_REQUIREMENTS)
1107 1107
1108 1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1109 1109 controller='files', action='archivefile',
1110 1110 conditions={'function': check_repo},
1111 1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112 1112
1113 1113 rmap.connect('files_nodelist_home',
1114 1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1115 1115 controller='files', action='nodelist',
1116 1116 conditions={'function': check_repo},
1117 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118 1118
1119 1119 rmap.connect('files_nodetree_full',
1120 1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1121 1121 controller='files', action='nodetree_full',
1122 1122 conditions={'function': check_repo},
1123 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1124 1124
1125 1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1126 1126 controller='forks', action='fork_create',
1127 1127 conditions={'function': check_repo, 'method': ['POST']},
1128 1128 requirements=URL_NAME_REQUIREMENTS)
1129 1129
1130 1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1131 1131 controller='forks', action='fork',
1132 1132 conditions={'function': check_repo},
1133 1133 requirements=URL_NAME_REQUIREMENTS)
1134 1134
1135 1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1136 1136 controller='forks', action='forks',
1137 1137 conditions={'function': check_repo},
1138 1138 requirements=URL_NAME_REQUIREMENTS)
1139 1139
1140 1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1141 1141 controller='followers', action='followers',
1142 1142 conditions={'function': check_repo},
1143 1143 requirements=URL_NAME_REQUIREMENTS)
1144 1144
1145 1145 # must be here for proper group/repo catching pattern
1146 1146 _connect_with_slash(
1147 1147 rmap, 'repo_group_home', '/{group_name}',
1148 1148 controller='home', action='index_repo_group',
1149 1149 conditions={'function': check_group},
1150 1150 requirements=URL_NAME_REQUIREMENTS)
1151 1151
1152 1152 # catch all, at the end
1153 1153 _connect_with_slash(
1154 1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1155 1155 controller='summary', action='index',
1156 1156 conditions={'function': check_repo},
1157 1157 requirements=URL_NAME_REQUIREMENTS)
1158 1158
1159 1159 return rmap
1160 1160
1161 1161
1162 1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1163 1163 """
1164 1164 Connect a route with an optional trailing slash in `path`.
1165 1165 """
1166 1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1167 1167 mapper.connect(name, path, *args, **kwargs)
@@ -1,1026 +1,1029 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = CommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636 636 return True
637 637
638 638 def _merge_pull_request(self, pull_request, user, extras):
639 639 merge_resp = PullRequestModel().merge(
640 640 pull_request, user, extras=extras)
641 641
642 642 if merge_resp.executed:
643 643 log.debug("The merge was successful, closing the pull request.")
644 644 PullRequestModel().close_pull_request(
645 645 pull_request.pull_request_id, user)
646 646 Session().commit()
647 647 msg = _('Pull request was successfully merged and closed.')
648 648 h.flash(msg, category='success')
649 649 else:
650 650 log.debug(
651 651 "The merge was not successful. Merge response: %s",
652 652 merge_resp)
653 653 msg = PullRequestModel().merge_status_message(
654 654 merge_resp.failure_reason)
655 655 h.flash(msg, category='error')
656 656
657 657 def _update_reviewers(self, pull_request_id, review_members):
658 658 reviewers = [
659 659 (int(r['user_id']), r['reasons']) for r in review_members]
660 660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 661 Session().commit()
662 662
663 663 def _reject_close(self, pull_request):
664 664 if pull_request.is_closed():
665 665 raise HTTPForbidden()
666 666
667 667 PullRequestModel().close_pull_request_with_comment(
668 668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 669 Session().commit()
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 674 'repository.admin')
675 675 @auth.CSRFRequired()
676 676 @jsonify
677 677 def delete(self, repo_name, pull_request_id):
678 678 pull_request_id = safe_int(pull_request_id)
679 679 pull_request = PullRequest.get_or_404(pull_request_id)
680 680 # only owner can delete it !
681 681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 682 PullRequestModel().delete(pull_request)
683 683 Session().commit()
684 684 h.flash(_('Successfully deleted pull request'),
685 685 category='success')
686 686 return redirect(url('my_account_pullrequests'))
687 687 raise HTTPForbidden()
688 688
689 689 def _get_pr_version(self, pull_request_id, version=None):
690 690 pull_request_id = safe_int(pull_request_id)
691 691 at_version = None
692 692
693 693 if version and version == 'latest':
694 694 pull_request_ver = PullRequest.get(pull_request_id)
695 695 pull_request_obj = pull_request_ver
696 696 _org_pull_request_obj = pull_request_obj
697 697 at_version = 'latest'
698 698 elif version:
699 699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 700 pull_request_obj = pull_request_ver
701 701 _org_pull_request_obj = pull_request_ver.pull_request
702 702 at_version = pull_request_ver.pull_request_version_id
703 703 else:
704 704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 705
706 706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 707 pull_request_obj, _org_pull_request_obj)
708 708 return _org_pull_request_obj, pull_request_obj, \
709 709 pull_request_display_obj, at_version
710 710
711 711 def _get_pr_version_changes(self, version, pull_request_latest):
712 712 """
713 713 Generate changes commits, and diff data based on the current pr version
714 714 """
715 715
716 716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 717
718 718 # fake the version to add the "initial" state object
719 719 pull_request_initial = PullRequest.get_pr_display_object(
720 720 pull_request_latest, pull_request_latest,
721 721 internal_methods=['get_commit', 'versions'])
722 722 pull_request_initial.revisions = []
723 723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 727
728 728 _changes_versions = [pull_request_latest] + \
729 729 list(reversed(c.versions)) + \
730 730 [pull_request_initial]
731 731
732 732 if version == 'latest':
733 733 index = 0
734 734 else:
735 735 for pos, prver in enumerate(_changes_versions):
736 736 ver = getattr(prver, 'pull_request_version_id', -1)
737 737 if ver == safe_int(version):
738 738 index = pos
739 739 break
740 740 else:
741 741 index = 0
742 742
743 743 cur_obj = _changes_versions[index]
744 744 prev_obj = _changes_versions[index + 1]
745 745
746 746 old_commit_ids = set(prev_obj.revisions)
747 747 new_commit_ids = set(cur_obj.revisions)
748 748
749 749 changes = PullRequestModel()._calculate_commit_id_changes(
750 750 old_commit_ids, new_commit_ids)
751 751
752 752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 753 cur_obj, prev_obj)
754 754 file_changes = PullRequestModel()._calculate_file_changes(
755 755 old_diff_data, new_diff_data)
756 756 return changes, file_changes
757 757
758 758 @LoginRequired()
759 759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 760 'repository.admin')
761 761 def show(self, repo_name, pull_request_id):
762 762 pull_request_id = safe_int(pull_request_id)
763 763 version = request.GET.get('version')
764 764
765 765 (pull_request_latest,
766 766 pull_request_at_ver,
767 767 pull_request_display_obj,
768 768 at_version) = self._get_pr_version(pull_request_id, version=version)
769 769
770 770 c.template_context['pull_request_data']['pull_request_id'] = \
771 771 pull_request_id
772 772
773 773 # pull_requests repo_name we opened it against
774 774 # ie. target_repo must match
775 775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 776 raise HTTPNotFound
777 777
778 778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 779 pull_request_at_ver)
780 780
781 781 pr_closed = pull_request_latest.is_closed()
782 782 if at_version and not at_version == 'latest':
783 783 c.allowed_to_change_status = False
784 784 c.allowed_to_update = False
785 785 c.allowed_to_merge = False
786 786 c.allowed_to_delete = False
787 787 c.allowed_to_comment = False
788 788 else:
789 789 c.allowed_to_change_status = PullRequestModel(). \
790 790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 791 c.allowed_to_update = PullRequestModel().check_user_update(
792 792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 797 c.allowed_to_comment = not pr_closed
798 798
799 799 cc_model = CommentsModel()
800 800
801 801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 804 pull_request_at_ver)
805 805 c.approval_msg = None
806 806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 807 c.approval_msg = _('Reviewer approval is pending.')
808 808 c.pr_merge_status = False
809 809
810 810 # inline comments
811 811 inline_comments = cc_model.get_inline_comments(
812 812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813 813
814 814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 815 inline_comments, version=at_version, include_aggregates=True)
816 816
817 817 c.versions = pull_request_display_obj.versions()
818 818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 820 c.at_version_num, c.versions)
821 821
822 822 is_outdated = lambda co: \
823 823 not c.at_version_num \
824 824 or co.pull_request_version_id <= c.at_version_num
825 825
826 826 # inline_comments_until_version
827 827 if c.at_version_num:
828 828 # if we use version, then do not show later comments
829 829 # than current version
830 830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 831 for fname, per_line_comments in inline_comments.iteritems():
832 832 for lno, comments in per_line_comments.iteritems():
833 833 for co in comments:
834 834 if co.pull_request_version_id and is_outdated(co):
835 835 paths[co.f_path][co.line_no].append(co)
836 836 inline_comments = paths
837 837
838 838 # outdated comments
839 839 c.outdated_cnt = 0
840 840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 841 outdated_comments = cc_model.get_outdated_comments(
842 842 c.rhodecode_db_repo.repo_id,
843 843 pull_request=pull_request_at_ver)
844 844
845 845 # Count outdated comments and check for deleted files
846 846 is_outdated = lambda co: \
847 847 not c.at_version_num \
848 848 or co.pull_request_version_id < c.at_version_num
849 849 for file_name, lines in outdated_comments.iteritems():
850 850 for comments in lines.values():
851 851 comments = [comm for comm in comments if is_outdated(comm)]
852 852 c.outdated_cnt += len(comments)
853 853
854 854 # load compare data into template context
855 855 self._load_compare_data(pull_request_at_ver, inline_comments)
856 856
857 857 # this is a hack to properly display links, when creating PR, the
858 858 # compare view and others uses different notation, and
859 859 # compare_commits.mako renders links based on the target_repo.
860 860 # We need to swap that here to generate it properly on the html side
861 861 c.target_repo = c.source_repo
862 862
863 863 # general comments
864 864 c.comments = cc_model.get_comments(
865 865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866 866
867 867 if c.allowed_to_update:
868 868 force_close = ('forced_closed', _('Close Pull Request'))
869 869 statuses = ChangesetStatus.STATUSES + [force_close]
870 870 else:
871 871 statuses = ChangesetStatus.STATUSES
872 872 c.commit_statuses = statuses
873 873
874 874 c.ancestor = None # TODO: add ancestor here
875 875 c.pull_request = pull_request_display_obj
876 876 c.pull_request_latest = pull_request_latest
877 877 c.at_version = at_version
878 878
879 879 c.changes = None
880 880 c.file_changes = None
881 881
882 882 c.show_version_changes = 1 # control flag, not used yet
883 883
884 884 if at_version and c.show_version_changes:
885 885 c.changes, c.file_changes = self._get_pr_version_changes(
886 886 version, pull_request_latest)
887 887
888 888 return render('/pullrequests/pullrequest_show.mako')
889 889
890 890 @LoginRequired()
891 891 @NotAnonymous()
892 892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 893 'repository.admin')
894 894 @auth.CSRFRequired()
895 895 @jsonify
896 896 def comment(self, repo_name, pull_request_id):
897 897 pull_request_id = safe_int(pull_request_id)
898 898 pull_request = PullRequest.get_or_404(pull_request_id)
899 899 if pull_request.is_closed():
900 900 raise HTTPForbidden()
901 901
902 902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 903 # as a changeset status, still we want to send it in one value.
904 904 status = request.POST.get('changeset_status', None)
905 905 text = request.POST.get('text')
906 906 comment_type = request.POST.get('comment_type')
907 resolves_comment_id = request.POST.get('resolves_comment_id')
908
907 909 if status and '_closed' in status:
908 910 close_pr = True
909 911 status = status.replace('_closed', '')
910 912 else:
911 913 close_pr = False
912 914
913 915 forced = (status == 'forced')
914 916 if forced:
915 917 status = 'rejected'
916 918
917 919 allowed_to_change_status = PullRequestModel().check_user_change_status(
918 920 pull_request, c.rhodecode_user)
919 921
920 922 if status and allowed_to_change_status:
921 923 message = (_('Status change %(transition_icon)s %(status)s')
922 924 % {'transition_icon': '>',
923 925 'status': ChangesetStatus.get_status_lbl(status)})
924 926 if close_pr:
925 927 message = _('Closing with') + ' ' + message
926 928 text = text or message
927 929 comm = CommentsModel().create(
928 930 text=text,
929 931 repo=c.rhodecode_db_repo.repo_id,
930 932 user=c.rhodecode_user.user_id,
931 933 pull_request=pull_request_id,
932 934 f_path=request.POST.get('f_path'),
933 935 line_no=request.POST.get('line'),
934 936 status_change=(ChangesetStatus.get_status_lbl(status)
935 937 if status and allowed_to_change_status else None),
936 938 status_change_type=(status
937 939 if status and allowed_to_change_status else None),
938 940 closing_pr=close_pr,
939 comment_type=comment_type
941 comment_type=comment_type,
942 resolves_comment_id=resolves_comment_id
940 943 )
941 944
942 945 if allowed_to_change_status:
943 946 old_calculated_status = pull_request.calculated_review_status()
944 947 # get status if set !
945 948 if status:
946 949 ChangesetStatusModel().set_status(
947 950 c.rhodecode_db_repo.repo_id,
948 951 status,
949 952 c.rhodecode_user.user_id,
950 953 comm,
951 954 pull_request=pull_request_id
952 955 )
953 956
954 957 Session().flush()
955 958 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
956 959 # we now calculate the status of pull request, and based on that
957 960 # calculation we set the commits status
958 961 calculated_status = pull_request.calculated_review_status()
959 962 if old_calculated_status != calculated_status:
960 963 PullRequestModel()._trigger_pull_request_hook(
961 964 pull_request, c.rhodecode_user, 'review_status_change')
962 965
963 966 calculated_status_lbl = ChangesetStatus.get_status_lbl(
964 967 calculated_status)
965 968
966 969 if close_pr:
967 970 status_completed = (
968 971 calculated_status in [ChangesetStatus.STATUS_APPROVED,
969 972 ChangesetStatus.STATUS_REJECTED])
970 973 if forced or status_completed:
971 974 PullRequestModel().close_pull_request(
972 975 pull_request_id, c.rhodecode_user)
973 976 else:
974 977 h.flash(_('Closing pull request on other statuses than '
975 978 'rejected or approved is forbidden. '
976 979 'Calculated status from all reviewers '
977 980 'is currently: %s') % calculated_status_lbl,
978 981 category='warning')
979 982
980 983 Session().commit()
981 984
982 985 if not request.is_xhr:
983 986 return redirect(h.url('pullrequest_show', repo_name=repo_name,
984 987 pull_request_id=pull_request_id))
985 988
986 989 data = {
987 990 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
988 991 }
989 992 if comm:
990 993 c.co = comm
991 994 c.inline_comment = True if comm.line_no else False
992 995 data.update(comm.get_dict())
993 996 data.update({'rendered_text':
994 997 render('changeset/changeset_comment_block.mako')})
995 998
996 999 return data
997 1000
998 1001 @LoginRequired()
999 1002 @NotAnonymous()
1000 1003 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1001 1004 'repository.admin')
1002 1005 @auth.CSRFRequired()
1003 1006 @jsonify
1004 1007 def delete_comment(self, repo_name, comment_id):
1005 1008 return self._delete_comment(comment_id)
1006 1009
1007 1010 def _delete_comment(self, comment_id):
1008 1011 comment_id = safe_int(comment_id)
1009 1012 co = ChangesetComment.get_or_404(comment_id)
1010 1013 if co.pull_request.is_closed():
1011 1014 # don't allow deleting comments on closed pull request
1012 1015 raise HTTPForbidden()
1013 1016
1014 1017 is_owner = co.author.user_id == c.rhodecode_user.user_id
1015 1018 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1016 1019 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1017 1020 old_calculated_status = co.pull_request.calculated_review_status()
1018 1021 CommentsModel().delete(comment=co)
1019 1022 Session().commit()
1020 1023 calculated_status = co.pull_request.calculated_review_status()
1021 1024 if old_calculated_status != calculated_status:
1022 1025 PullRequestModel()._trigger_pull_request_hook(
1023 1026 co.pull_request, c.rhodecode_user, 'review_status_change')
1024 1027 return True
1025 1028 else:
1026 1029 raise HTTPForbidden()
@@ -1,549 +1,551 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47 from rhodecode.model.validation_schema.schemas import comment_schema
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class CommentsModel(BaseModel):
54 54
55 55 cls = ChangesetComment
56 56
57 57 DIFF_CONTEXT_BEFORE = 3
58 58 DIFF_CONTEXT_AFTER = 3
59 59
60 60 def __get_commit_comment(self, changeset_comment):
61 61 return self._get_instance(ChangesetComment, changeset_comment)
62 62
63 63 def __get_pull_request(self, pull_request):
64 64 return self._get_instance(PullRequest, pull_request)
65 65
66 66 def _extract_mentions(self, s):
67 67 user_objects = []
68 68 for username in extract_mentioned_users(s):
69 69 user_obj = User.get_by_username(username, case_insensitive=True)
70 70 if user_obj:
71 71 user_objects.append(user_obj)
72 72 return user_objects
73 73
74 74 def _get_renderer(self, global_renderer='rst'):
75 75 try:
76 76 # try reading from visual context
77 77 from pylons import tmpl_context
78 78 global_renderer = tmpl_context.visual.default_renderer
79 79 except AttributeError:
80 80 log.debug("Renderer not set, falling back "
81 81 "to default renderer '%s'", global_renderer)
82 82 except Exception:
83 83 log.error(traceback.format_exc())
84 84 return global_renderer
85 85
86 86 def create(self, text, repo, user, commit_id=None, pull_request=None,
87 f_path=None, line_no=None, status_change=None, comment_type=None,
88 status_change_type=None, closing_pr=False,
89 send_email=True, renderer=None):
87 f_path=None, line_no=None, status_change=None,
88 status_change_type=None, comment_type=None,
89 resolves_comment_id=None, closing_pr=False, send_email=True,
90 renderer=None):
90 91 """
91 92 Creates new comment for commit or pull request.
92 93 IF status_change is not none this comment is associated with a
93 94 status change of commit or commit associated with pull request
94 95
95 96 :param text:
96 97 :param repo:
97 98 :param user:
98 99 :param commit_id:
99 100 :param pull_request:
100 101 :param f_path:
101 102 :param line_no:
102 103 :param status_change: Label for status change
103 104 :param comment_type: Type of comment
104 105 :param status_change_type: type of status change
105 106 :param closing_pr:
106 107 :param send_email:
107 108 :param renderer: pick renderer for this comment
108 109 """
109 110 if not text:
110 111 log.warning('Missing text for comment, skipping...')
111 112 return
112 113
113 114 if not renderer:
114 115 renderer = self._get_renderer()
115 116
117 repo = self._get_repo(repo)
118 user = self._get_user(user)
116 119
117 120 schema = comment_schema.CommentSchema()
118 121 validated_kwargs = schema.deserialize(dict(
119 122 comment_body=text,
120 123 comment_type=comment_type,
121 124 comment_file=f_path,
122 125 comment_line=line_no,
123 126 renderer_type=renderer,
124 status_change=status_change,
125
126 repo=repo,
127 user=user,
127 status_change=status_change_type,
128 resolves_comment_id=resolves_comment_id,
129 repo=repo.repo_id,
130 user=user.user_id,
128 131 ))
129 132
130 repo = self._get_repo(validated_kwargs['repo'])
131 user = self._get_user(validated_kwargs['user'])
132
133 133 comment = ChangesetComment()
134 134 comment.renderer = validated_kwargs['renderer_type']
135 135 comment.text = validated_kwargs['comment_body']
136 136 comment.f_path = validated_kwargs['comment_file']
137 137 comment.line_no = validated_kwargs['comment_line']
138 138 comment.comment_type = validated_kwargs['comment_type']
139 139
140 140 comment.repo = repo
141 141 comment.author = user
142 comment.resolved_comment = self.__get_commit_comment(
143 validated_kwargs['resolves_comment_id'])
142 144
143 145 pull_request_id = pull_request
144 146
145 147 commit_obj = None
146 148 pull_request_obj = None
147 149
148 150 if commit_id:
149 151 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
150 152 # do a lookup, so we don't pass something bad here
151 153 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
152 154 comment.revision = commit_obj.raw_id
153 155
154 156 elif pull_request_id:
155 157 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
156 158 pull_request_obj = self.__get_pull_request(pull_request_id)
157 159 comment.pull_request = pull_request_obj
158 160 else:
159 161 raise Exception('Please specify commit or pull_request_id')
160 162
161 163 Session().add(comment)
162 164 Session().flush()
163 165 kwargs = {
164 166 'user': user,
165 167 'renderer_type': renderer,
166 168 'repo_name': repo.repo_name,
167 169 'status_change': status_change,
168 170 'status_change_type': status_change_type,
169 171 'comment_body': text,
170 172 'comment_file': f_path,
171 173 'comment_line': line_no,
172 174 }
173 175
174 176 if commit_obj:
175 177 recipients = ChangesetComment.get_users(
176 178 revision=commit_obj.raw_id)
177 179 # add commit author if it's in RhodeCode system
178 180 cs_author = User.get_from_cs_author(commit_obj.author)
179 181 if not cs_author:
180 182 # use repo owner if we cannot extract the author correctly
181 183 cs_author = repo.user
182 184 recipients += [cs_author]
183 185
184 186 commit_comment_url = self.get_url(comment)
185 187
186 188 target_repo_url = h.link_to(
187 189 repo.repo_name,
188 190 h.url('summary_home',
189 191 repo_name=repo.repo_name, qualified=True))
190 192
191 193 # commit specifics
192 194 kwargs.update({
193 195 'commit': commit_obj,
194 196 'commit_message': commit_obj.message,
195 197 'commit_target_repo': target_repo_url,
196 198 'commit_comment_url': commit_comment_url,
197 199 })
198 200
199 201 elif pull_request_obj:
200 202 # get the current participants of this pull request
201 203 recipients = ChangesetComment.get_users(
202 204 pull_request_id=pull_request_obj.pull_request_id)
203 205 # add pull request author
204 206 recipients += [pull_request_obj.author]
205 207
206 208 # add the reviewers to notification
207 209 recipients += [x.user for x in pull_request_obj.reviewers]
208 210
209 211 pr_target_repo = pull_request_obj.target_repo
210 212 pr_source_repo = pull_request_obj.source_repo
211 213
212 214 pr_comment_url = h.url(
213 215 'pullrequest_show',
214 216 repo_name=pr_target_repo.repo_name,
215 217 pull_request_id=pull_request_obj.pull_request_id,
216 218 anchor='comment-%s' % comment.comment_id,
217 219 qualified=True,)
218 220
219 221 # set some variables for email notification
220 222 pr_target_repo_url = h.url(
221 223 'summary_home', repo_name=pr_target_repo.repo_name,
222 224 qualified=True)
223 225
224 226 pr_source_repo_url = h.url(
225 227 'summary_home', repo_name=pr_source_repo.repo_name,
226 228 qualified=True)
227 229
228 230 # pull request specifics
229 231 kwargs.update({
230 232 'pull_request': pull_request_obj,
231 233 'pr_id': pull_request_obj.pull_request_id,
232 234 'pr_target_repo': pr_target_repo,
233 235 'pr_target_repo_url': pr_target_repo_url,
234 236 'pr_source_repo': pr_source_repo,
235 237 'pr_source_repo_url': pr_source_repo_url,
236 238 'pr_comment_url': pr_comment_url,
237 239 'pr_closing': closing_pr,
238 240 })
239 241 if send_email:
240 242 # pre-generate the subject for notification itself
241 243 (subject,
242 244 _h, _e, # we don't care about those
243 245 body_plaintext) = EmailNotificationModel().render_email(
244 246 notification_type, **kwargs)
245 247
246 248 mention_recipients = set(
247 249 self._extract_mentions(text)).difference(recipients)
248 250
249 251 # create notification objects, and emails
250 252 NotificationModel().create(
251 253 created_by=user,
252 254 notification_subject=subject,
253 255 notification_body=body_plaintext,
254 256 notification_type=notification_type,
255 257 recipients=recipients,
256 258 mention_recipients=mention_recipients,
257 259 email_kwargs=kwargs,
258 260 )
259 261
260 262 action = (
261 263 'user_commented_pull_request:{}'.format(
262 264 comment.pull_request.pull_request_id)
263 265 if comment.pull_request
264 266 else 'user_commented_revision:{}'.format(comment.revision)
265 267 )
266 268 action_logger(user, action, comment.repo)
267 269
268 270 registry = get_current_registry()
269 271 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
270 272 channelstream_config = rhodecode_plugins.get('channelstream', {})
271 273 msg_url = ''
272 274 if commit_obj:
273 275 msg_url = commit_comment_url
274 276 repo_name = repo.repo_name
275 277 elif pull_request_obj:
276 278 msg_url = pr_comment_url
277 279 repo_name = pr_target_repo.repo_name
278 280
279 281 if channelstream_config.get('enabled'):
280 282 message = '<strong>{}</strong> {} - ' \
281 283 '<a onclick="window.location=\'{}\';' \
282 284 'window.location.reload()">' \
283 285 '<strong>{}</strong></a>'
284 286 message = message.format(
285 287 user.username, _('made a comment'), msg_url,
286 288 _('Show it now'))
287 289 channel = '/repo${}$/pr/{}'.format(
288 290 repo_name,
289 291 pull_request_id
290 292 )
291 293 payload = {
292 294 'type': 'message',
293 295 'timestamp': datetime.utcnow(),
294 296 'user': 'system',
295 297 'exclude_users': [user.username],
296 298 'channel': channel,
297 299 'message': {
298 300 'message': message,
299 301 'level': 'info',
300 302 'topic': '/notifications'
301 303 }
302 304 }
303 305 channelstream_request(channelstream_config, [payload],
304 306 '/message', raise_exc=False)
305 307
306 308 return comment
307 309
308 310 def delete(self, comment):
309 311 """
310 312 Deletes given comment
311 313
312 314 :param comment_id:
313 315 """
314 316 comment = self.__get_commit_comment(comment)
315 317 Session().delete(comment)
316 318
317 319 return comment
318 320
319 321 def get_all_comments(self, repo_id, revision=None, pull_request=None):
320 322 q = ChangesetComment.query()\
321 323 .filter(ChangesetComment.repo_id == repo_id)
322 324 if revision:
323 325 q = q.filter(ChangesetComment.revision == revision)
324 326 elif pull_request:
325 327 pull_request = self.__get_pull_request(pull_request)
326 328 q = q.filter(ChangesetComment.pull_request == pull_request)
327 329 else:
328 330 raise Exception('Please specify commit or pull_request')
329 331 q = q.order_by(ChangesetComment.created_on)
330 332 return q.all()
331 333
332 334 def get_url(self, comment):
333 335 comment = self.__get_commit_comment(comment)
334 336 if comment.pull_request:
335 337 return h.url(
336 338 'pullrequest_show',
337 339 repo_name=comment.pull_request.target_repo.repo_name,
338 340 pull_request_id=comment.pull_request.pull_request_id,
339 341 anchor='comment-%s' % comment.comment_id,
340 342 qualified=True,)
341 343 else:
342 344 return h.url(
343 345 'changeset_home',
344 346 repo_name=comment.repo.repo_name,
345 347 revision=comment.revision,
346 348 anchor='comment-%s' % comment.comment_id,
347 349 qualified=True,)
348 350
349 351 def get_comments(self, repo_id, revision=None, pull_request=None):
350 352 """
351 353 Gets main comments based on revision or pull_request_id
352 354
353 355 :param repo_id:
354 356 :param revision:
355 357 :param pull_request:
356 358 """
357 359
358 360 q = ChangesetComment.query()\
359 361 .filter(ChangesetComment.repo_id == repo_id)\
360 362 .filter(ChangesetComment.line_no == None)\
361 363 .filter(ChangesetComment.f_path == None)
362 364 if revision:
363 365 q = q.filter(ChangesetComment.revision == revision)
364 366 elif pull_request:
365 367 pull_request = self.__get_pull_request(pull_request)
366 368 q = q.filter(ChangesetComment.pull_request == pull_request)
367 369 else:
368 370 raise Exception('Please specify commit or pull_request')
369 371 q = q.order_by(ChangesetComment.created_on)
370 372 return q.all()
371 373
372 374 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
373 375 q = self._get_inline_comments_query(repo_id, revision, pull_request)
374 376 return self._group_comments_by_path_and_line_number(q)
375 377
376 378 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
377 379 version=None, include_aggregates=False):
378 380 version_aggregates = collections.defaultdict(list)
379 381 inline_cnt = 0
380 382 for fname, per_line_comments in inline_comments.iteritems():
381 383 for lno, comments in per_line_comments.iteritems():
382 384 for comm in comments:
383 385 version_aggregates[comm.pull_request_version_id].append(comm)
384 386 if not comm.outdated_at_version(version) and skip_outdated:
385 387 inline_cnt += 1
386 388
387 389 if include_aggregates:
388 390 return inline_cnt, version_aggregates
389 391 return inline_cnt
390 392
391 393 def get_outdated_comments(self, repo_id, pull_request):
392 394 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
393 395 # of a pull request.
394 396 q = self._all_inline_comments_of_pull_request(pull_request)
395 397 q = q.filter(
396 398 ChangesetComment.display_state ==
397 399 ChangesetComment.COMMENT_OUTDATED
398 400 ).order_by(ChangesetComment.comment_id.asc())
399 401
400 402 return self._group_comments_by_path_and_line_number(q)
401 403
402 404 def _get_inline_comments_query(self, repo_id, revision, pull_request):
403 405 # TODO: johbo: Split this into two methods: One for PR and one for
404 406 # commit.
405 407 if revision:
406 408 q = Session().query(ChangesetComment).filter(
407 409 ChangesetComment.repo_id == repo_id,
408 410 ChangesetComment.line_no != null(),
409 411 ChangesetComment.f_path != null(),
410 412 ChangesetComment.revision == revision)
411 413
412 414 elif pull_request:
413 415 pull_request = self.__get_pull_request(pull_request)
414 416 if not CommentsModel.use_outdated_comments(pull_request):
415 417 q = self._visible_inline_comments_of_pull_request(pull_request)
416 418 else:
417 419 q = self._all_inline_comments_of_pull_request(pull_request)
418 420
419 421 else:
420 422 raise Exception('Please specify commit or pull_request_id')
421 423 q = q.order_by(ChangesetComment.comment_id.asc())
422 424 return q
423 425
424 426 def _group_comments_by_path_and_line_number(self, q):
425 427 comments = q.all()
426 428 paths = collections.defaultdict(lambda: collections.defaultdict(list))
427 429 for co in comments:
428 430 paths[co.f_path][co.line_no].append(co)
429 431 return paths
430 432
431 433 @classmethod
432 434 def needed_extra_diff_context(cls):
433 435 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
434 436
435 437 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
436 438 if not CommentsModel.use_outdated_comments(pull_request):
437 439 return
438 440
439 441 comments = self._visible_inline_comments_of_pull_request(pull_request)
440 442 comments_to_outdate = comments.all()
441 443
442 444 for comment in comments_to_outdate:
443 445 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
444 446
445 447 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
446 448 diff_line = _parse_comment_line_number(comment.line_no)
447 449
448 450 try:
449 451 old_context = old_diff_proc.get_context_of_line(
450 452 path=comment.f_path, diff_line=diff_line)
451 453 new_context = new_diff_proc.get_context_of_line(
452 454 path=comment.f_path, diff_line=diff_line)
453 455 except (diffs.LineNotInDiffException,
454 456 diffs.FileNotInDiffException):
455 457 comment.display_state = ChangesetComment.COMMENT_OUTDATED
456 458 return
457 459
458 460 if old_context == new_context:
459 461 return
460 462
461 463 if self._should_relocate_diff_line(diff_line):
462 464 new_diff_lines = new_diff_proc.find_context(
463 465 path=comment.f_path, context=old_context,
464 466 offset=self.DIFF_CONTEXT_BEFORE)
465 467 if not new_diff_lines:
466 468 comment.display_state = ChangesetComment.COMMENT_OUTDATED
467 469 else:
468 470 new_diff_line = self._choose_closest_diff_line(
469 471 diff_line, new_diff_lines)
470 472 comment.line_no = _diff_to_comment_line_number(new_diff_line)
471 473 else:
472 474 comment.display_state = ChangesetComment.COMMENT_OUTDATED
473 475
474 476 def _should_relocate_diff_line(self, diff_line):
475 477 """
476 478 Checks if relocation shall be tried for the given `diff_line`.
477 479
478 480 If a comment points into the first lines, then we can have a situation
479 481 that after an update another line has been added on top. In this case
480 482 we would find the context still and move the comment around. This
481 483 would be wrong.
482 484 """
483 485 should_relocate = (
484 486 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
485 487 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
486 488 return should_relocate
487 489
488 490 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
489 491 candidate = new_diff_lines[0]
490 492 best_delta = _diff_line_delta(diff_line, candidate)
491 493 for new_diff_line in new_diff_lines[1:]:
492 494 delta = _diff_line_delta(diff_line, new_diff_line)
493 495 if delta < best_delta:
494 496 candidate = new_diff_line
495 497 best_delta = delta
496 498 return candidate
497 499
498 500 def _visible_inline_comments_of_pull_request(self, pull_request):
499 501 comments = self._all_inline_comments_of_pull_request(pull_request)
500 502 comments = comments.filter(
501 503 coalesce(ChangesetComment.display_state, '') !=
502 504 ChangesetComment.COMMENT_OUTDATED)
503 505 return comments
504 506
505 507 def _all_inline_comments_of_pull_request(self, pull_request):
506 508 comments = Session().query(ChangesetComment)\
507 509 .filter(ChangesetComment.line_no != None)\
508 510 .filter(ChangesetComment.f_path != None)\
509 511 .filter(ChangesetComment.pull_request == pull_request)
510 512 return comments
511 513
512 514 @staticmethod
513 515 def use_outdated_comments(pull_request):
514 516 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
515 517 settings = settings_model.get_general_settings()
516 518 return settings.get('rhodecode_use_outdated_comments', False)
517 519
518 520
519 521 def _parse_comment_line_number(line_no):
520 522 """
521 523 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
522 524 """
523 525 old_line = None
524 526 new_line = None
525 527 if line_no.startswith('o'):
526 528 old_line = int(line_no[1:])
527 529 elif line_no.startswith('n'):
528 530 new_line = int(line_no[1:])
529 531 else:
530 532 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
531 533 return diffs.DiffLineNumber(old_line, new_line)
532 534
533 535
534 536 def _diff_to_comment_line_number(diff_line):
535 537 if diff_line.new is not None:
536 538 return u'n{}'.format(diff_line.new)
537 539 elif diff_line.old is not None:
538 540 return u'o{}'.format(diff_line.old)
539 541 return u''
540 542
541 543
542 544 def _diff_line_delta(a, b):
543 545 if None not in (a.new, b.new):
544 546 return abs(a.new - b.new)
545 547 elif None not in (a.old, b.old):
546 548 return abs(a.old - b.old)
547 549 else:
548 550 raise ValueError(
549 551 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3829 +1,3833 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def builtin_token_roles(self):
606 606 return map(UserApiKeys._get_role_name, [
607 607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 608 ])
609 609
610 610 @property
611 611 def ip_addresses(self):
612 612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 613 return [x.ip_addr for x in ret]
614 614
615 615 @property
616 616 def username_and_name(self):
617 617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618 618
619 619 @property
620 620 def username_or_name_or_email(self):
621 621 full_name = self.full_name if self.full_name is not ' ' else None
622 622 return self.username or full_name or self.email
623 623
624 624 @property
625 625 def full_name(self):
626 626 return '%s %s' % (self.firstname, self.lastname)
627 627
628 628 @property
629 629 def full_name_or_username(self):
630 630 return ('%s %s' % (self.firstname, self.lastname)
631 631 if (self.firstname and self.lastname) else self.username)
632 632
633 633 @property
634 634 def full_contact(self):
635 635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636 636
637 637 @property
638 638 def short_contact(self):
639 639 return '%s %s' % (self.firstname, self.lastname)
640 640
641 641 @property
642 642 def is_admin(self):
643 643 return self.admin
644 644
645 645 @property
646 646 def AuthUser(self):
647 647 """
648 648 Returns instance of AuthUser for this user
649 649 """
650 650 from rhodecode.lib.auth import AuthUser
651 651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 652 username=self.username)
653 653
654 654 @hybrid_property
655 655 def user_data(self):
656 656 if not self._user_data:
657 657 return {}
658 658
659 659 try:
660 660 return json.loads(self._user_data)
661 661 except TypeError:
662 662 return {}
663 663
664 664 @user_data.setter
665 665 def user_data(self, val):
666 666 if not isinstance(val, dict):
667 667 raise Exception('user_data must be dict, got %s' % type(val))
668 668 try:
669 669 self._user_data = json.dumps(val)
670 670 except Exception:
671 671 log.error(traceback.format_exc())
672 672
673 673 @classmethod
674 674 def get_by_username(cls, username, case_insensitive=False,
675 675 cache=False, identity_cache=False):
676 676 session = Session()
677 677
678 678 if case_insensitive:
679 679 q = cls.query().filter(
680 680 func.lower(cls.username) == func.lower(username))
681 681 else:
682 682 q = cls.query().filter(cls.username == username)
683 683
684 684 if cache:
685 685 if identity_cache:
686 686 val = cls.identity_cache(session, 'username', username)
687 687 if val:
688 688 return val
689 689 else:
690 690 q = q.options(
691 691 FromCache("sql_cache_short",
692 692 "get_user_by_name_%s" % _hash_key(username)))
693 693
694 694 return q.scalar()
695 695
696 696 @classmethod
697 697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 698 q = cls.query().filter(cls.api_key == auth_token)
699 699
700 700 if cache:
701 701 q = q.options(FromCache("sql_cache_short",
702 702 "get_auth_token_%s" % auth_token))
703 703 res = q.scalar()
704 704
705 705 if fallback and not res:
706 706 #fallback to additional keys
707 707 _res = UserApiKeys.query()\
708 708 .filter(UserApiKeys.api_key == auth_token)\
709 709 .filter(or_(UserApiKeys.expires == -1,
710 710 UserApiKeys.expires >= time.time()))\
711 711 .first()
712 712 if _res:
713 713 res = _res.user
714 714 return res
715 715
716 716 @classmethod
717 717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718 718
719 719 if case_insensitive:
720 720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721 721
722 722 else:
723 723 q = cls.query().filter(cls.email == email)
724 724
725 725 if cache:
726 726 q = q.options(FromCache("sql_cache_short",
727 727 "get_email_key_%s" % _hash_key(email)))
728 728
729 729 ret = q.scalar()
730 730 if ret is None:
731 731 q = UserEmailMap.query()
732 732 # try fetching in alternate email map
733 733 if case_insensitive:
734 734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 735 else:
736 736 q = q.filter(UserEmailMap.email == email)
737 737 q = q.options(joinedload(UserEmailMap.user))
738 738 if cache:
739 739 q = q.options(FromCache("sql_cache_short",
740 740 "get_email_map_key_%s" % email))
741 741 ret = getattr(q.scalar(), 'user', None)
742 742
743 743 return ret
744 744
745 745 @classmethod
746 746 def get_from_cs_author(cls, author):
747 747 """
748 748 Tries to get User objects out of commit author string
749 749
750 750 :param author:
751 751 """
752 752 from rhodecode.lib.helpers import email, author_name
753 753 # Valid email in the attribute passed, see if they're in the system
754 754 _email = email(author)
755 755 if _email:
756 756 user = cls.get_by_email(_email, case_insensitive=True)
757 757 if user:
758 758 return user
759 759 # Maybe we can match by username?
760 760 _author = author_name(author)
761 761 user = cls.get_by_username(_author, case_insensitive=True)
762 762 if user:
763 763 return user
764 764
765 765 def update_userdata(self, **kwargs):
766 766 usr = self
767 767 old = usr.user_data
768 768 old.update(**kwargs)
769 769 usr.user_data = old
770 770 Session().add(usr)
771 771 log.debug('updated userdata with ', kwargs)
772 772
773 773 def update_lastlogin(self):
774 774 """Update user lastlogin"""
775 775 self.last_login = datetime.datetime.now()
776 776 Session().add(self)
777 777 log.debug('updated user %s lastlogin', self.username)
778 778
779 779 def update_lastactivity(self):
780 780 """Update user lastactivity"""
781 781 usr = self
782 782 old = usr.user_data
783 783 old.update({'last_activity': time.time()})
784 784 usr.user_data = old
785 785 Session().add(usr)
786 786 log.debug('updated user %s lastactivity', usr.username)
787 787
788 788 def update_password(self, new_password, change_api_key=False):
789 789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790 790
791 791 self.password = get_crypt_password(new_password)
792 792 if change_api_key:
793 793 self.api_key = generate_auth_token(self.username)
794 794 Session().add(self)
795 795
796 796 @classmethod
797 797 def get_first_super_admin(cls):
798 798 user = User.query().filter(User.admin == true()).first()
799 799 if user is None:
800 800 raise Exception('FATAL: Missing administrative account!')
801 801 return user
802 802
803 803 @classmethod
804 804 def get_all_super_admins(cls):
805 805 """
806 806 Returns all admin accounts sorted by username
807 807 """
808 808 return User.query().filter(User.admin == true())\
809 809 .order_by(User.username.asc()).all()
810 810
811 811 @classmethod
812 812 def get_default_user(cls, cache=False):
813 813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 814 if user is None:
815 815 raise Exception('FATAL: Missing default account!')
816 816 return user
817 817
818 818 def _get_default_perms(self, user, suffix=''):
819 819 from rhodecode.model.permission import PermissionModel
820 820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821 821
822 822 def get_default_perms(self, suffix=''):
823 823 return self._get_default_perms(self, suffix)
824 824
825 825 def get_api_data(self, include_secrets=False, details='full'):
826 826 """
827 827 Common function for generating user related data for API
828 828
829 829 :param include_secrets: By default secrets in the API data will be replaced
830 830 by a placeholder value to prevent exposing this data by accident. In case
831 831 this data shall be exposed, set this flag to ``True``.
832 832
833 833 :param details: details can be 'basic|full' basic gives only a subset of
834 834 the available user information that includes user_id, name and emails.
835 835 """
836 836 user = self
837 837 user_data = self.user_data
838 838 data = {
839 839 'user_id': user.user_id,
840 840 'username': user.username,
841 841 'firstname': user.name,
842 842 'lastname': user.lastname,
843 843 'email': user.email,
844 844 'emails': user.emails,
845 845 }
846 846 if details == 'basic':
847 847 return data
848 848
849 849 api_key_length = 40
850 850 api_key_replacement = '*' * api_key_length
851 851
852 852 extras = {
853 853 'api_key': api_key_replacement,
854 854 'api_keys': [api_key_replacement],
855 855 'active': user.active,
856 856 'admin': user.admin,
857 857 'extern_type': user.extern_type,
858 858 'extern_name': user.extern_name,
859 859 'last_login': user.last_login,
860 860 'ip_addresses': user.ip_addresses,
861 861 'language': user_data.get('language')
862 862 }
863 863 data.update(extras)
864 864
865 865 if include_secrets:
866 866 data['api_key'] = user.api_key
867 867 data['api_keys'] = user.auth_tokens
868 868 return data
869 869
870 870 def __json__(self):
871 871 data = {
872 872 'full_name': self.full_name,
873 873 'full_name_or_username': self.full_name_or_username,
874 874 'short_contact': self.short_contact,
875 875 'full_contact': self.full_contact,
876 876 }
877 877 data.update(self.get_api_data())
878 878 return data
879 879
880 880
881 881 class UserApiKeys(Base, BaseModel):
882 882 __tablename__ = 'user_api_keys'
883 883 __table_args__ = (
884 884 Index('uak_api_key_idx', 'api_key'),
885 885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 886 UniqueConstraint('api_key'),
887 887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 889 )
890 890 __mapper_args__ = {}
891 891
892 892 # ApiKey role
893 893 ROLE_ALL = 'token_role_all'
894 894 ROLE_HTTP = 'token_role_http'
895 895 ROLE_VCS = 'token_role_vcs'
896 896 ROLE_API = 'token_role_api'
897 897 ROLE_FEED = 'token_role_feed'
898 898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899 899
900 900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 904 expires = Column('expires', Float(53), nullable=False)
905 905 role = Column('role', String(255), nullable=True)
906 906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907 907
908 908 user = relationship('User', lazy='joined')
909 909
910 910 @classmethod
911 911 def _get_role_name(cls, role):
912 912 return {
913 913 cls.ROLE_ALL: _('all'),
914 914 cls.ROLE_HTTP: _('http/web interface'),
915 915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 916 cls.ROLE_API: _('api calls'),
917 917 cls.ROLE_FEED: _('feed access'),
918 918 }.get(role, role)
919 919
920 920 @property
921 921 def expired(self):
922 922 if self.expires == -1:
923 923 return False
924 924 return time.time() > self.expires
925 925
926 926 @property
927 927 def role_humanized(self):
928 928 return self._get_role_name(self.role)
929 929
930 930
931 931 class UserEmailMap(Base, BaseModel):
932 932 __tablename__ = 'user_email_map'
933 933 __table_args__ = (
934 934 Index('uem_email_idx', 'email'),
935 935 UniqueConstraint('email'),
936 936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 938 )
939 939 __mapper_args__ = {}
940 940
941 941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 944 user = relationship('User', lazy='joined')
945 945
946 946 @validates('_email')
947 947 def validate_email(self, key, email):
948 948 # check if this email is not main one
949 949 main_email = Session().query(User).filter(User.email == email).scalar()
950 950 if main_email is not None:
951 951 raise AttributeError('email %s is present is user table' % email)
952 952 return email
953 953
954 954 @hybrid_property
955 955 def email(self):
956 956 return self._email
957 957
958 958 @email.setter
959 959 def email(self, val):
960 960 self._email = val.lower() if val else None
961 961
962 962
963 963 class UserIpMap(Base, BaseModel):
964 964 __tablename__ = 'user_ip_map'
965 965 __table_args__ = (
966 966 UniqueConstraint('user_id', 'ip_addr'),
967 967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 969 )
970 970 __mapper_args__ = {}
971 971
972 972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 977 user = relationship('User', lazy='joined')
978 978
979 979 @classmethod
980 980 def _get_ip_range(cls, ip_addr):
981 981 net = ipaddress.ip_network(ip_addr, strict=False)
982 982 return [str(net.network_address), str(net.broadcast_address)]
983 983
984 984 def __json__(self):
985 985 return {
986 986 'ip_addr': self.ip_addr,
987 987 'ip_range': self._get_ip_range(self.ip_addr),
988 988 }
989 989
990 990 def __unicode__(self):
991 991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 992 self.user_id, self.ip_addr)
993 993
994 994 class UserLog(Base, BaseModel):
995 995 __tablename__ = 'user_logs'
996 996 __table_args__ = (
997 997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 999 )
1000 1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008 1008
1009 1009 def __unicode__(self):
1010 1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 1011 self.repository_name,
1012 1012 self.action)
1013 1013
1014 1014 @property
1015 1015 def action_as_day(self):
1016 1016 return datetime.date(*self.action_date.timetuple()[:3])
1017 1017
1018 1018 user = relationship('User')
1019 1019 repository = relationship('Repository', cascade='')
1020 1020
1021 1021
1022 1022 class UserGroup(Base, BaseModel):
1023 1023 __tablename__ = 'users_groups'
1024 1024 __table_args__ = (
1025 1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 1027 )
1028 1028
1029 1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037 1037
1038 1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044 1044
1045 1045 user = relationship('User')
1046 1046
1047 1047 @hybrid_property
1048 1048 def group_data(self):
1049 1049 if not self._group_data:
1050 1050 return {}
1051 1051
1052 1052 try:
1053 1053 return json.loads(self._group_data)
1054 1054 except TypeError:
1055 1055 return {}
1056 1056
1057 1057 @group_data.setter
1058 1058 def group_data(self, val):
1059 1059 try:
1060 1060 self._group_data = json.dumps(val)
1061 1061 except Exception:
1062 1062 log.error(traceback.format_exc())
1063 1063
1064 1064 def __unicode__(self):
1065 1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 1066 self.users_group_id,
1067 1067 self.users_group_name)
1068 1068
1069 1069 @classmethod
1070 1070 def get_by_group_name(cls, group_name, cache=False,
1071 1071 case_insensitive=False):
1072 1072 if case_insensitive:
1073 1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 1074 func.lower(group_name))
1075 1075
1076 1076 else:
1077 1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 1078 if cache:
1079 1079 q = q.options(FromCache(
1080 1080 "sql_cache_short",
1081 1081 "get_group_%s" % _hash_key(group_name)))
1082 1082 return q.scalar()
1083 1083
1084 1084 @classmethod
1085 1085 def get(cls, user_group_id, cache=False):
1086 1086 user_group = cls.query()
1087 1087 if cache:
1088 1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 1089 "get_users_group_%s" % user_group_id))
1090 1090 return user_group.get(user_group_id)
1091 1091
1092 1092 def permissions(self, with_admins=True, with_owner=True):
1093 1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 1095 joinedload(UserUserGroupToPerm.user),
1096 1096 joinedload(UserUserGroupToPerm.permission),)
1097 1097
1098 1098 # get owners and admins and permissions. We do a trick of re-writing
1099 1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 1100 # has a global reference and changing one object propagates to all
1101 1101 # others. This means if admin is also an owner admin_row that change
1102 1102 # would propagate to both objects
1103 1103 perm_rows = []
1104 1104 for _usr in q.all():
1105 1105 usr = AttributeDict(_usr.user.get_dict())
1106 1106 usr.permission = _usr.permission.permission_name
1107 1107 perm_rows.append(usr)
1108 1108
1109 1109 # filter the perm rows by 'default' first and then sort them by
1110 1110 # admin,write,read,none permissions sorted again alphabetically in
1111 1111 # each group
1112 1112 perm_rows = sorted(perm_rows, key=display_sort)
1113 1113
1114 1114 _admin_perm = 'usergroup.admin'
1115 1115 owner_row = []
1116 1116 if with_owner:
1117 1117 usr = AttributeDict(self.user.get_dict())
1118 1118 usr.owner_row = True
1119 1119 usr.permission = _admin_perm
1120 1120 owner_row.append(usr)
1121 1121
1122 1122 super_admin_rows = []
1123 1123 if with_admins:
1124 1124 for usr in User.get_all_super_admins():
1125 1125 # if this admin is also owner, don't double the record
1126 1126 if usr.user_id == owner_row[0].user_id:
1127 1127 owner_row[0].admin_row = True
1128 1128 else:
1129 1129 usr = AttributeDict(usr.get_dict())
1130 1130 usr.admin_row = True
1131 1131 usr.permission = _admin_perm
1132 1132 super_admin_rows.append(usr)
1133 1133
1134 1134 return super_admin_rows + owner_row + perm_rows
1135 1135
1136 1136 def permission_user_groups(self):
1137 1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141 1141
1142 1142 perm_rows = []
1143 1143 for _user_group in q.all():
1144 1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 1145 usr.permission = _user_group.permission.permission_name
1146 1146 perm_rows.append(usr)
1147 1147
1148 1148 return perm_rows
1149 1149
1150 1150 def _get_default_perms(self, user_group, suffix=''):
1151 1151 from rhodecode.model.permission import PermissionModel
1152 1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153 1153
1154 1154 def get_default_perms(self, suffix=''):
1155 1155 return self._get_default_perms(self, suffix)
1156 1156
1157 1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 1158 """
1159 1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 1160 basically forwarded.
1161 1161
1162 1162 """
1163 1163 user_group = self
1164 1164
1165 1165 data = {
1166 1166 'users_group_id': user_group.users_group_id,
1167 1167 'group_name': user_group.users_group_name,
1168 1168 'group_description': user_group.user_group_description,
1169 1169 'active': user_group.users_group_active,
1170 1170 'owner': user_group.user.username,
1171 1171 }
1172 1172 if with_group_members:
1173 1173 users = []
1174 1174 for user in user_group.members:
1175 1175 user = user.user
1176 1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 1177 data['users'] = users
1178 1178
1179 1179 return data
1180 1180
1181 1181
1182 1182 class UserGroupMember(Base, BaseModel):
1183 1183 __tablename__ = 'users_groups_members'
1184 1184 __table_args__ = (
1185 1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 1187 )
1188 1188
1189 1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192 1192
1193 1193 user = relationship('User', lazy='joined')
1194 1194 users_group = relationship('UserGroup')
1195 1195
1196 1196 def __init__(self, gr_id='', u_id=''):
1197 1197 self.users_group_id = gr_id
1198 1198 self.user_id = u_id
1199 1199
1200 1200
1201 1201 class RepositoryField(Base, BaseModel):
1202 1202 __tablename__ = 'repositories_fields'
1203 1203 __table_args__ = (
1204 1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 1207 )
1208 1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209 1209
1210 1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 1212 field_key = Column("field_key", String(250))
1213 1213 field_label = Column("field_label", String(1024), nullable=False)
1214 1214 field_value = Column("field_value", String(10000), nullable=False)
1215 1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218 1218
1219 1219 repository = relationship('Repository')
1220 1220
1221 1221 @property
1222 1222 def field_key_prefixed(self):
1223 1223 return 'ex_%s' % self.field_key
1224 1224
1225 1225 @classmethod
1226 1226 def un_prefix_key(cls, key):
1227 1227 if key.startswith(cls.PREFIX):
1228 1228 return key[len(cls.PREFIX):]
1229 1229 return key
1230 1230
1231 1231 @classmethod
1232 1232 def get_by_key_name(cls, key, repo):
1233 1233 row = cls.query()\
1234 1234 .filter(cls.repository == repo)\
1235 1235 .filter(cls.field_key == key).scalar()
1236 1236 return row
1237 1237
1238 1238
1239 1239 class Repository(Base, BaseModel):
1240 1240 __tablename__ = 'repositories'
1241 1241 __table_args__ = (
1242 1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 1245 )
1246 1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248 1248
1249 1249 STATE_CREATED = 'repo_state_created'
1250 1250 STATE_PENDING = 'repo_state_pending'
1251 1251 STATE_ERROR = 'repo_state_error'
1252 1252
1253 1253 LOCK_AUTOMATIC = 'lock_auto'
1254 1254 LOCK_API = 'lock_api'
1255 1255 LOCK_WEB = 'lock_web'
1256 1256 LOCK_PULL = 'lock_pull'
1257 1257
1258 1258 NAME_SEP = URL_SEP
1259 1259
1260 1260 repo_id = Column(
1261 1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 1262 primary_key=True)
1263 1263 _repo_name = Column(
1264 1264 "repo_name", Text(), nullable=False, default=None)
1265 1265 _repo_name_hash = Column(
1266 1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 1267 repo_state = Column("repo_state", String(255), nullable=True)
1268 1268
1269 1269 clone_uri = Column(
1270 1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 1271 default=None)
1272 1272 repo_type = Column(
1273 1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 1274 user_id = Column(
1275 1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 1276 unique=False, default=None)
1277 1277 private = Column(
1278 1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 1279 enable_statistics = Column(
1280 1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 1281 enable_downloads = Column(
1282 1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 1283 description = Column(
1284 1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 1285 created_on = Column(
1286 1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 updated_on = Column(
1289 1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 1290 default=datetime.datetime.now)
1291 1291 _landing_revision = Column(
1292 1292 "landing_revision", String(255), nullable=False, unique=False,
1293 1293 default=None)
1294 1294 enable_locking = Column(
1295 1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 1296 default=False)
1297 1297 _locked = Column(
1298 1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 1299 _changeset_cache = Column(
1300 1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301 1301
1302 1302 fork_id = Column(
1303 1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 1304 nullable=True, unique=False, default=None)
1305 1305 group_id = Column(
1306 1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 1307 unique=False, default=None)
1308 1308
1309 1309 user = relationship('User', lazy='joined')
1310 1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 1311 group = relationship('RepoGroup', lazy='joined')
1312 1312 repo_to_perm = relationship(
1313 1313 'UserRepoToPerm', cascade='all',
1314 1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317 1317
1318 1318 followers = relationship(
1319 1319 'UserFollowing',
1320 1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 1321 cascade='all')
1322 1322 extra_fields = relationship(
1323 1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 1324 logs = relationship('UserLog')
1325 1325 comments = relationship(
1326 1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 1327 pull_requests_source = relationship(
1328 1328 'PullRequest',
1329 1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 1330 cascade="all, delete, delete-orphan")
1331 1331 pull_requests_target = relationship(
1332 1332 'PullRequest',
1333 1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 1334 cascade="all, delete, delete-orphan")
1335 1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 1337 integrations = relationship('Integration',
1338 1338 cascade="all, delete, delete-orphan")
1339 1339
1340 1340 def __unicode__(self):
1341 1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 1342 safe_unicode(self.repo_name))
1343 1343
1344 1344 @hybrid_property
1345 1345 def landing_rev(self):
1346 1346 # always should return [rev_type, rev]
1347 1347 if self._landing_revision:
1348 1348 _rev_info = self._landing_revision.split(':')
1349 1349 if len(_rev_info) < 2:
1350 1350 _rev_info.insert(0, 'rev')
1351 1351 return [_rev_info[0], _rev_info[1]]
1352 1352 return [None, None]
1353 1353
1354 1354 @landing_rev.setter
1355 1355 def landing_rev(self, val):
1356 1356 if ':' not in val:
1357 1357 raise ValueError('value must be delimited with `:` and consist '
1358 1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 1359 self._landing_revision = val
1360 1360
1361 1361 @hybrid_property
1362 1362 def locked(self):
1363 1363 if self._locked:
1364 1364 user_id, timelocked, reason = self._locked.split(':')
1365 1365 lock_values = int(user_id), timelocked, reason
1366 1366 else:
1367 1367 lock_values = [None, None, None]
1368 1368 return lock_values
1369 1369
1370 1370 @locked.setter
1371 1371 def locked(self, val):
1372 1372 if val and isinstance(val, (list, tuple)):
1373 1373 self._locked = ':'.join(map(str, val))
1374 1374 else:
1375 1375 self._locked = None
1376 1376
1377 1377 @hybrid_property
1378 1378 def changeset_cache(self):
1379 1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 1380 dummy = EmptyCommit().__json__()
1381 1381 if not self._changeset_cache:
1382 1382 return dummy
1383 1383 try:
1384 1384 return json.loads(self._changeset_cache)
1385 1385 except TypeError:
1386 1386 return dummy
1387 1387 except Exception:
1388 1388 log.error(traceback.format_exc())
1389 1389 return dummy
1390 1390
1391 1391 @changeset_cache.setter
1392 1392 def changeset_cache(self, val):
1393 1393 try:
1394 1394 self._changeset_cache = json.dumps(val)
1395 1395 except Exception:
1396 1396 log.error(traceback.format_exc())
1397 1397
1398 1398 @hybrid_property
1399 1399 def repo_name(self):
1400 1400 return self._repo_name
1401 1401
1402 1402 @repo_name.setter
1403 1403 def repo_name(self, value):
1404 1404 self._repo_name = value
1405 1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406 1406
1407 1407 @classmethod
1408 1408 def normalize_repo_name(cls, repo_name):
1409 1409 """
1410 1410 Normalizes os specific repo_name to the format internally stored inside
1411 1411 database using URL_SEP
1412 1412
1413 1413 :param cls:
1414 1414 :param repo_name:
1415 1415 """
1416 1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417 1417
1418 1418 @classmethod
1419 1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 1420 session = Session()
1421 1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422 1422
1423 1423 if cache:
1424 1424 if identity_cache:
1425 1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 1426 if val:
1427 1427 return val
1428 1428 else:
1429 1429 q = q.options(
1430 1430 FromCache("sql_cache_short",
1431 1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432 1432
1433 1433 return q.scalar()
1434 1434
1435 1435 @classmethod
1436 1436 def get_by_full_path(cls, repo_full_path):
1437 1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 1438 repo_name = cls.normalize_repo_name(repo_name)
1439 1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440 1440
1441 1441 @classmethod
1442 1442 def get_repo_forks(cls, repo_id):
1443 1443 return cls.query().filter(Repository.fork_id == repo_id)
1444 1444
1445 1445 @classmethod
1446 1446 def base_path(cls):
1447 1447 """
1448 1448 Returns base path when all repos are stored
1449 1449
1450 1450 :param cls:
1451 1451 """
1452 1452 q = Session().query(RhodeCodeUi)\
1453 1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 1455 return q.one().ui_value
1456 1456
1457 1457 @classmethod
1458 1458 def is_valid(cls, repo_name):
1459 1459 """
1460 1460 returns True if given repo name is a valid filesystem repository
1461 1461
1462 1462 :param cls:
1463 1463 :param repo_name:
1464 1464 """
1465 1465 from rhodecode.lib.utils import is_valid_repo
1466 1466
1467 1467 return is_valid_repo(repo_name, cls.base_path())
1468 1468
1469 1469 @classmethod
1470 1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 1471 case_insensitive=True):
1472 1472 q = Repository.query()
1473 1473
1474 1474 if not isinstance(user_id, Optional):
1475 1475 q = q.filter(Repository.user_id == user_id)
1476 1476
1477 1477 if not isinstance(group_id, Optional):
1478 1478 q = q.filter(Repository.group_id == group_id)
1479 1479
1480 1480 if case_insensitive:
1481 1481 q = q.order_by(func.lower(Repository.repo_name))
1482 1482 else:
1483 1483 q = q.order_by(Repository.repo_name)
1484 1484 return q.all()
1485 1485
1486 1486 @property
1487 1487 def forks(self):
1488 1488 """
1489 1489 Return forks of this repo
1490 1490 """
1491 1491 return Repository.get_repo_forks(self.repo_id)
1492 1492
1493 1493 @property
1494 1494 def parent(self):
1495 1495 """
1496 1496 Returns fork parent
1497 1497 """
1498 1498 return self.fork
1499 1499
1500 1500 @property
1501 1501 def just_name(self):
1502 1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503 1503
1504 1504 @property
1505 1505 def groups_with_parents(self):
1506 1506 groups = []
1507 1507 if self.group is None:
1508 1508 return groups
1509 1509
1510 1510 cur_gr = self.group
1511 1511 groups.insert(0, cur_gr)
1512 1512 while 1:
1513 1513 gr = getattr(cur_gr, 'parent_group', None)
1514 1514 cur_gr = cur_gr.parent_group
1515 1515 if gr is None:
1516 1516 break
1517 1517 groups.insert(0, gr)
1518 1518
1519 1519 return groups
1520 1520
1521 1521 @property
1522 1522 def groups_and_repo(self):
1523 1523 return self.groups_with_parents, self
1524 1524
1525 1525 @LazyProperty
1526 1526 def repo_path(self):
1527 1527 """
1528 1528 Returns base full path for that repository means where it actually
1529 1529 exists on a filesystem
1530 1530 """
1531 1531 q = Session().query(RhodeCodeUi).filter(
1532 1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 1534 return q.one().ui_value
1535 1535
1536 1536 @property
1537 1537 def repo_full_path(self):
1538 1538 p = [self.repo_path]
1539 1539 # we need to split the name by / since this is how we store the
1540 1540 # names in the database, but that eventually needs to be converted
1541 1541 # into a valid system path
1542 1542 p += self.repo_name.split(self.NAME_SEP)
1543 1543 return os.path.join(*map(safe_unicode, p))
1544 1544
1545 1545 @property
1546 1546 def cache_keys(self):
1547 1547 """
1548 1548 Returns associated cache keys for that repo
1549 1549 """
1550 1550 return CacheKey.query()\
1551 1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 1552 .order_by(CacheKey.cache_key)\
1553 1553 .all()
1554 1554
1555 1555 def get_new_name(self, repo_name):
1556 1556 """
1557 1557 returns new full repository name based on assigned group and new new
1558 1558
1559 1559 :param group_name:
1560 1560 """
1561 1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563 1563
1564 1564 @property
1565 1565 def _config(self):
1566 1566 """
1567 1567 Returns db based config object.
1568 1568 """
1569 1569 from rhodecode.lib.utils import make_db_config
1570 1570 return make_db_config(clear_session=False, repo=self)
1571 1571
1572 1572 def permissions(self, with_admins=True, with_owner=True):
1573 1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 1575 joinedload(UserRepoToPerm.user),
1576 1576 joinedload(UserRepoToPerm.permission),)
1577 1577
1578 1578 # get owners and admins and permissions. We do a trick of re-writing
1579 1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 1580 # has a global reference and changing one object propagates to all
1581 1581 # others. This means if admin is also an owner admin_row that change
1582 1582 # would propagate to both objects
1583 1583 perm_rows = []
1584 1584 for _usr in q.all():
1585 1585 usr = AttributeDict(_usr.user.get_dict())
1586 1586 usr.permission = _usr.permission.permission_name
1587 1587 perm_rows.append(usr)
1588 1588
1589 1589 # filter the perm rows by 'default' first and then sort them by
1590 1590 # admin,write,read,none permissions sorted again alphabetically in
1591 1591 # each group
1592 1592 perm_rows = sorted(perm_rows, key=display_sort)
1593 1593
1594 1594 _admin_perm = 'repository.admin'
1595 1595 owner_row = []
1596 1596 if with_owner:
1597 1597 usr = AttributeDict(self.user.get_dict())
1598 1598 usr.owner_row = True
1599 1599 usr.permission = _admin_perm
1600 1600 owner_row.append(usr)
1601 1601
1602 1602 super_admin_rows = []
1603 1603 if with_admins:
1604 1604 for usr in User.get_all_super_admins():
1605 1605 # if this admin is also owner, don't double the record
1606 1606 if usr.user_id == owner_row[0].user_id:
1607 1607 owner_row[0].admin_row = True
1608 1608 else:
1609 1609 usr = AttributeDict(usr.get_dict())
1610 1610 usr.admin_row = True
1611 1611 usr.permission = _admin_perm
1612 1612 super_admin_rows.append(usr)
1613 1613
1614 1614 return super_admin_rows + owner_row + perm_rows
1615 1615
1616 1616 def permission_user_groups(self):
1617 1617 q = UserGroupRepoToPerm.query().filter(
1618 1618 UserGroupRepoToPerm.repository == self)
1619 1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 1620 joinedload(UserGroupRepoToPerm.users_group),
1621 1621 joinedload(UserGroupRepoToPerm.permission),)
1622 1622
1623 1623 perm_rows = []
1624 1624 for _user_group in q.all():
1625 1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 1626 usr.permission = _user_group.permission.permission_name
1627 1627 perm_rows.append(usr)
1628 1628
1629 1629 return perm_rows
1630 1630
1631 1631 def get_api_data(self, include_secrets=False):
1632 1632 """
1633 1633 Common function for generating repo api data
1634 1634
1635 1635 :param include_secrets: See :meth:`User.get_api_data`.
1636 1636
1637 1637 """
1638 1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 1639 # move this methods on models level.
1640 1640 from rhodecode.model.settings import SettingsModel
1641 1641
1642 1642 repo = self
1643 1643 _user_id, _time, _reason = self.locked
1644 1644
1645 1645 data = {
1646 1646 'repo_id': repo.repo_id,
1647 1647 'repo_name': repo.repo_name,
1648 1648 'repo_type': repo.repo_type,
1649 1649 'clone_uri': repo.clone_uri or '',
1650 1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 1651 'private': repo.private,
1652 1652 'created_on': repo.created_on,
1653 1653 'description': repo.description,
1654 1654 'landing_rev': repo.landing_rev,
1655 1655 'owner': repo.user.username,
1656 1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 1657 'enable_statistics': repo.enable_statistics,
1658 1658 'enable_locking': repo.enable_locking,
1659 1659 'enable_downloads': repo.enable_downloads,
1660 1660 'last_changeset': repo.changeset_cache,
1661 1661 'locked_by': User.get(_user_id).get_api_data(
1662 1662 include_secrets=include_secrets) if _user_id else None,
1663 1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 1664 'lock_reason': _reason if _reason else None,
1665 1665 }
1666 1666
1667 1667 # TODO: mikhail: should be per-repo settings here
1668 1668 rc_config = SettingsModel().get_all_settings()
1669 1669 repository_fields = str2bool(
1670 1670 rc_config.get('rhodecode_repository_fields'))
1671 1671 if repository_fields:
1672 1672 for f in self.extra_fields:
1673 1673 data[f.field_key_prefixed] = f.field_value
1674 1674
1675 1675 return data
1676 1676
1677 1677 @classmethod
1678 1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 1679 if not lock_time:
1680 1680 lock_time = time.time()
1681 1681 if not lock_reason:
1682 1682 lock_reason = cls.LOCK_AUTOMATIC
1683 1683 repo.locked = [user_id, lock_time, lock_reason]
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def unlock(cls, repo):
1689 1689 repo.locked = None
1690 1690 Session().add(repo)
1691 1691 Session().commit()
1692 1692
1693 1693 @classmethod
1694 1694 def getlock(cls, repo):
1695 1695 return repo.locked
1696 1696
1697 1697 def is_user_lock(self, user_id):
1698 1698 if self.lock[0]:
1699 1699 lock_user_id = safe_int(self.lock[0])
1700 1700 user_id = safe_int(user_id)
1701 1701 # both are ints, and they are equal
1702 1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703 1703
1704 1704 return False
1705 1705
1706 1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 1707 """
1708 1708 Checks locking on this repository, if locking is enabled and lock is
1709 1709 present returns a tuple of make_lock, locked, locked_by.
1710 1710 make_lock can have 3 states None (do nothing) True, make lock
1711 1711 False release lock, This value is later propagated to hooks, which
1712 1712 do the locking. Think about this as signals passed to hooks what to do.
1713 1713
1714 1714 """
1715 1715 # TODO: johbo: This is part of the business logic and should be moved
1716 1716 # into the RepositoryModel.
1717 1717
1718 1718 if action not in ('push', 'pull'):
1719 1719 raise ValueError("Invalid action value: %s" % repr(action))
1720 1720
1721 1721 # defines if locked error should be thrown to user
1722 1722 currently_locked = False
1723 1723 # defines if new lock should be made, tri-state
1724 1724 make_lock = None
1725 1725 repo = self
1726 1726 user = User.get(user_id)
1727 1727
1728 1728 lock_info = repo.locked
1729 1729
1730 1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 1731 if action == 'push':
1732 1732 # check if it's already locked !, if it is compare users
1733 1733 locked_by_user_id = lock_info[0]
1734 1734 if user.user_id == locked_by_user_id:
1735 1735 log.debug(
1736 1736 'Got `push` action from user %s, now unlocking', user)
1737 1737 # unlock if we have push from user who locked
1738 1738 make_lock = False
1739 1739 else:
1740 1740 # we're not the same user who locked, ban with
1741 1741 # code defined in settings (default is 423 HTTP Locked) !
1742 1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 1743 currently_locked = True
1744 1744 elif action == 'pull':
1745 1745 # [0] user [1] date
1746 1746 if lock_info[0] and lock_info[1]:
1747 1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 1748 currently_locked = True
1749 1749 else:
1750 1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 1751 make_lock = True
1752 1752
1753 1753 else:
1754 1754 log.debug('Repository %s do not have locking enabled', repo)
1755 1755
1756 1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 1757 make_lock, currently_locked, lock_info)
1758 1758
1759 1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 1762 # if we don't have at least write permission we cannot make a lock
1763 1763 log.debug('lock state reset back to FALSE due to lack '
1764 1764 'of at least read permission')
1765 1765 make_lock = False
1766 1766
1767 1767 return make_lock, currently_locked, lock_info
1768 1768
1769 1769 @property
1770 1770 def last_db_change(self):
1771 1771 return self.updated_on
1772 1772
1773 1773 @property
1774 1774 def clone_uri_hidden(self):
1775 1775 clone_uri = self.clone_uri
1776 1776 if clone_uri:
1777 1777 import urlobject
1778 1778 url_obj = urlobject.URLObject(clone_uri)
1779 1779 if url_obj.password:
1780 1780 clone_uri = url_obj.with_password('*****')
1781 1781 return clone_uri
1782 1782
1783 1783 def clone_url(self, **override):
1784 1784 qualified_home_url = url('home', qualified=True)
1785 1785
1786 1786 uri_tmpl = None
1787 1787 if 'with_id' in override:
1788 1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 1789 del override['with_id']
1790 1790
1791 1791 if 'uri_tmpl' in override:
1792 1792 uri_tmpl = override['uri_tmpl']
1793 1793 del override['uri_tmpl']
1794 1794
1795 1795 # we didn't override our tmpl from **overrides
1796 1796 if not uri_tmpl:
1797 1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 1798 try:
1799 1799 from pylons import tmpl_context as c
1800 1800 uri_tmpl = c.clone_uri_tmpl
1801 1801 except Exception:
1802 1802 # in any case if we call this outside of request context,
1803 1803 # ie, not having tmpl_context set up
1804 1804 pass
1805 1805
1806 1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 1807 qualifed_home_url=qualified_home_url,
1808 1808 repo_name=self.repo_name,
1809 1809 repo_id=self.repo_id, **override)
1810 1810
1811 1811 def set_state(self, state):
1812 1812 self.repo_state = state
1813 1813 Session().add(self)
1814 1814 #==========================================================================
1815 1815 # SCM PROPERTIES
1816 1816 #==========================================================================
1817 1817
1818 1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 1819 return get_commit_safe(
1820 1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821 1821
1822 1822 def get_changeset(self, rev=None, pre_load=None):
1823 1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 1824 commit_id = None
1825 1825 commit_idx = None
1826 1826 if isinstance(rev, basestring):
1827 1827 commit_id = rev
1828 1828 else:
1829 1829 commit_idx = rev
1830 1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 1831 pre_load=pre_load)
1832 1832
1833 1833 def get_landing_commit(self):
1834 1834 """
1835 1835 Returns landing commit, or if that doesn't exist returns the tip
1836 1836 """
1837 1837 _rev_type, _rev = self.landing_rev
1838 1838 commit = self.get_commit(_rev)
1839 1839 if isinstance(commit, EmptyCommit):
1840 1840 return self.get_commit()
1841 1841 return commit
1842 1842
1843 1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 1844 """
1845 1845 Update cache of last changeset for repository, keys should be::
1846 1846
1847 1847 short_id
1848 1848 raw_id
1849 1849 revision
1850 1850 parents
1851 1851 message
1852 1852 date
1853 1853 author
1854 1854
1855 1855 :param cs_cache:
1856 1856 """
1857 1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 1858 if cs_cache is None:
1859 1859 # use no-cache version here
1860 1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 1861 if scm_repo:
1862 1862 cs_cache = scm_repo.get_commit(
1863 1863 pre_load=["author", "date", "message", "parents"])
1864 1864 else:
1865 1865 cs_cache = EmptyCommit()
1866 1866
1867 1867 if isinstance(cs_cache, BaseChangeset):
1868 1868 cs_cache = cs_cache.__json__()
1869 1869
1870 1870 def is_outdated(new_cs_cache):
1871 1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 1873 return True
1874 1874 return False
1875 1875
1876 1876 # check if we have maybe already latest cached revision
1877 1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 1878 _default = datetime.datetime.fromtimestamp(0)
1879 1879 last_change = cs_cache.get('date') or _default
1880 1880 log.debug('updated repo %s with new cs cache %s',
1881 1881 self.repo_name, cs_cache)
1882 1882 self.updated_on = last_change
1883 1883 self.changeset_cache = cs_cache
1884 1884 Session().add(self)
1885 1885 Session().commit()
1886 1886 else:
1887 1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 1888 'commit already with latest changes', self.repo_name)
1889 1889
1890 1890 @property
1891 1891 def tip(self):
1892 1892 return self.get_commit('tip')
1893 1893
1894 1894 @property
1895 1895 def author(self):
1896 1896 return self.tip.author
1897 1897
1898 1898 @property
1899 1899 def last_change(self):
1900 1900 return self.scm_instance().last_change
1901 1901
1902 1902 def get_comments(self, revisions=None):
1903 1903 """
1904 1904 Returns comments for this repository grouped by revisions
1905 1905
1906 1906 :param revisions: filter query by revisions only
1907 1907 """
1908 1908 cmts = ChangesetComment.query()\
1909 1909 .filter(ChangesetComment.repo == self)
1910 1910 if revisions:
1911 1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 1912 grouped = collections.defaultdict(list)
1913 1913 for cmt in cmts.all():
1914 1914 grouped[cmt.revision].append(cmt)
1915 1915 return grouped
1916 1916
1917 1917 def statuses(self, revisions=None):
1918 1918 """
1919 1919 Returns statuses for this repository
1920 1920
1921 1921 :param revisions: list of revisions to get statuses for
1922 1922 """
1923 1923 statuses = ChangesetStatus.query()\
1924 1924 .filter(ChangesetStatus.repo == self)\
1925 1925 .filter(ChangesetStatus.version == 0)
1926 1926
1927 1927 if revisions:
1928 1928 # Try doing the filtering in chunks to avoid hitting limits
1929 1929 size = 500
1930 1930 status_results = []
1931 1931 for chunk in xrange(0, len(revisions), size):
1932 1932 status_results += statuses.filter(
1933 1933 ChangesetStatus.revision.in_(
1934 1934 revisions[chunk: chunk+size])
1935 1935 ).all()
1936 1936 else:
1937 1937 status_results = statuses.all()
1938 1938
1939 1939 grouped = {}
1940 1940
1941 1941 # maybe we have open new pullrequest without a status?
1942 1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 1945 for rev in pr.revisions:
1946 1946 pr_id = pr.pull_request_id
1947 1947 pr_repo = pr.target_repo.repo_name
1948 1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949 1949
1950 1950 for stat in status_results:
1951 1951 pr_id = pr_repo = None
1952 1952 if stat.pull_request:
1953 1953 pr_id = stat.pull_request.pull_request_id
1954 1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 1956 pr_id, pr_repo]
1957 1957 return grouped
1958 1958
1959 1959 # ==========================================================================
1960 1960 # SCM CACHE INSTANCE
1961 1961 # ==========================================================================
1962 1962
1963 1963 def scm_instance(self, **kwargs):
1964 1964 import rhodecode
1965 1965
1966 1966 # Passing a config will not hit the cache currently only used
1967 1967 # for repo2dbmapper
1968 1968 config = kwargs.pop('config', None)
1969 1969 cache = kwargs.pop('cache', None)
1970 1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 1971 # if cache is NOT defined use default global, else we have a full
1972 1972 # control over cache behaviour
1973 1973 if cache is None and full_cache and not config:
1974 1974 return self._get_instance_cached()
1975 1975 return self._get_instance(cache=bool(cache), config=config)
1976 1976
1977 1977 def _get_instance_cached(self):
1978 1978 @cache_region('long_term')
1979 1979 def _get_repo(cache_key):
1980 1980 return self._get_instance()
1981 1981
1982 1982 invalidator_context = CacheKey.repo_context_cache(
1983 1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984 1984
1985 1985 with invalidator_context as context:
1986 1986 context.invalidate()
1987 1987 repo = context.compute()
1988 1988
1989 1989 return repo
1990 1990
1991 1991 def _get_instance(self, cache=True, config=None):
1992 1992 config = config or self._config
1993 1993 custom_wire = {
1994 1994 'cache': cache # controls the vcs.remote cache
1995 1995 }
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False,
2001 2001 _vcs_alias=self.repo_type)
2002 2002
2003 2003 return repo
2004 2004
2005 2005 def __json__(self):
2006 2006 return {'landing_rev': self.landing_rev}
2007 2007
2008 2008 def get_dict(self):
2009 2009
2010 2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 2011 # keep compatibility with the code which uses `repo_name` field.
2012 2012
2013 2013 result = super(Repository, self).get_dict()
2014 2014 result['repo_name'] = result.pop('_repo_name', None)
2015 2015 return result
2016 2016
2017 2017
2018 2018 class RepoGroup(Base, BaseModel):
2019 2019 __tablename__ = 'groups'
2020 2020 __table_args__ = (
2021 2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 2022 CheckConstraint('group_id != group_parent_id'),
2023 2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2025 )
2026 2026 __mapper_args__ = {'order_by': 'group_name'}
2027 2027
2028 2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029 2029
2030 2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038 2038
2039 2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 2042 user = relationship('User')
2043 2043 integrations = relationship('Integration',
2044 2044 cascade="all, delete, delete-orphan")
2045 2045
2046 2046 def __init__(self, group_name='', parent_group=None):
2047 2047 self.group_name = group_name
2048 2048 self.parent_group = parent_group
2049 2049
2050 2050 def __unicode__(self):
2051 2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 2052 self.group_name)
2053 2053
2054 2054 @classmethod
2055 2055 def _generate_choice(cls, repo_group):
2056 2056 from webhelpers.html import literal as _literal
2057 2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059 2059
2060 2060 @classmethod
2061 2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 2062 if not groups:
2063 2063 groups = cls.query().all()
2064 2064
2065 2065 repo_groups = []
2066 2066 if show_empty_group:
2067 2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068 2068
2069 2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070 2070
2071 2071 repo_groups = sorted(
2072 2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 2073 return repo_groups
2074 2074
2075 2075 @classmethod
2076 2076 def url_sep(cls):
2077 2077 return URL_SEP
2078 2078
2079 2079 @classmethod
2080 2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 2081 if case_insensitive:
2082 2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 2083 == func.lower(group_name))
2084 2084 else:
2085 2085 gr = cls.query().filter(cls.group_name == group_name)
2086 2086 if cache:
2087 2087 gr = gr.options(FromCache(
2088 2088 "sql_cache_short",
2089 2089 "get_group_%s" % _hash_key(group_name)))
2090 2090 return gr.scalar()
2091 2091
2092 2092 @classmethod
2093 2093 def get_user_personal_repo_group(cls, user_id):
2094 2094 user = User.get(user_id)
2095 2095 return cls.query()\
2096 2096 .filter(cls.personal == true())\
2097 2097 .filter(cls.user == user).scalar()
2098 2098
2099 2099 @classmethod
2100 2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 2101 case_insensitive=True):
2102 2102 q = RepoGroup.query()
2103 2103
2104 2104 if not isinstance(user_id, Optional):
2105 2105 q = q.filter(RepoGroup.user_id == user_id)
2106 2106
2107 2107 if not isinstance(group_id, Optional):
2108 2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109 2109
2110 2110 if case_insensitive:
2111 2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 2112 else:
2113 2113 q = q.order_by(RepoGroup.group_name)
2114 2114 return q.all()
2115 2115
2116 2116 @property
2117 2117 def parents(self):
2118 2118 parents_recursion_limit = 10
2119 2119 groups = []
2120 2120 if self.parent_group is None:
2121 2121 return groups
2122 2122 cur_gr = self.parent_group
2123 2123 groups.insert(0, cur_gr)
2124 2124 cnt = 0
2125 2125 while 1:
2126 2126 cnt += 1
2127 2127 gr = getattr(cur_gr, 'parent_group', None)
2128 2128 cur_gr = cur_gr.parent_group
2129 2129 if gr is None:
2130 2130 break
2131 2131 if cnt == parents_recursion_limit:
2132 2132 # this will prevent accidental infinit loops
2133 2133 log.error(('more than %s parents found for group %s, stopping '
2134 2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 2135 break
2136 2136
2137 2137 groups.insert(0, gr)
2138 2138 return groups
2139 2139
2140 2140 @property
2141 2141 def children(self):
2142 2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143 2143
2144 2144 @property
2145 2145 def name(self):
2146 2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147 2147
2148 2148 @property
2149 2149 def full_path(self):
2150 2150 return self.group_name
2151 2151
2152 2152 @property
2153 2153 def full_path_splitted(self):
2154 2154 return self.group_name.split(RepoGroup.url_sep())
2155 2155
2156 2156 @property
2157 2157 def repositories(self):
2158 2158 return Repository.query()\
2159 2159 .filter(Repository.group == self)\
2160 2160 .order_by(Repository.repo_name)
2161 2161
2162 2162 @property
2163 2163 def repositories_recursive_count(self):
2164 2164 cnt = self.repositories.count()
2165 2165
2166 2166 def children_count(group):
2167 2167 cnt = 0
2168 2168 for child in group.children:
2169 2169 cnt += child.repositories.count()
2170 2170 cnt += children_count(child)
2171 2171 return cnt
2172 2172
2173 2173 return cnt + children_count(self)
2174 2174
2175 2175 def _recursive_objects(self, include_repos=True):
2176 2176 all_ = []
2177 2177
2178 2178 def _get_members(root_gr):
2179 2179 if include_repos:
2180 2180 for r in root_gr.repositories:
2181 2181 all_.append(r)
2182 2182 childs = root_gr.children.all()
2183 2183 if childs:
2184 2184 for gr in childs:
2185 2185 all_.append(gr)
2186 2186 _get_members(gr)
2187 2187
2188 2188 _get_members(self)
2189 2189 return [self] + all_
2190 2190
2191 2191 def recursive_groups_and_repos(self):
2192 2192 """
2193 2193 Recursive return all groups, with repositories in those groups
2194 2194 """
2195 2195 return self._recursive_objects()
2196 2196
2197 2197 def recursive_groups(self):
2198 2198 """
2199 2199 Returns all children groups for this group including children of children
2200 2200 """
2201 2201 return self._recursive_objects(include_repos=False)
2202 2202
2203 2203 def get_new_name(self, group_name):
2204 2204 """
2205 2205 returns new full group name based on parent and new name
2206 2206
2207 2207 :param group_name:
2208 2208 """
2209 2209 path_prefix = (self.parent_group.full_path_splitted if
2210 2210 self.parent_group else [])
2211 2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212 2212
2213 2213 def permissions(self, with_admins=True, with_owner=True):
2214 2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 2216 joinedload(UserRepoGroupToPerm.user),
2217 2217 joinedload(UserRepoGroupToPerm.permission),)
2218 2218
2219 2219 # get owners and admins and permissions. We do a trick of re-writing
2220 2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 2221 # has a global reference and changing one object propagates to all
2222 2222 # others. This means if admin is also an owner admin_row that change
2223 2223 # would propagate to both objects
2224 2224 perm_rows = []
2225 2225 for _usr in q.all():
2226 2226 usr = AttributeDict(_usr.user.get_dict())
2227 2227 usr.permission = _usr.permission.permission_name
2228 2228 perm_rows.append(usr)
2229 2229
2230 2230 # filter the perm rows by 'default' first and then sort them by
2231 2231 # admin,write,read,none permissions sorted again alphabetically in
2232 2232 # each group
2233 2233 perm_rows = sorted(perm_rows, key=display_sort)
2234 2234
2235 2235 _admin_perm = 'group.admin'
2236 2236 owner_row = []
2237 2237 if with_owner:
2238 2238 usr = AttributeDict(self.user.get_dict())
2239 2239 usr.owner_row = True
2240 2240 usr.permission = _admin_perm
2241 2241 owner_row.append(usr)
2242 2242
2243 2243 super_admin_rows = []
2244 2244 if with_admins:
2245 2245 for usr in User.get_all_super_admins():
2246 2246 # if this admin is also owner, don't double the record
2247 2247 if usr.user_id == owner_row[0].user_id:
2248 2248 owner_row[0].admin_row = True
2249 2249 else:
2250 2250 usr = AttributeDict(usr.get_dict())
2251 2251 usr.admin_row = True
2252 2252 usr.permission = _admin_perm
2253 2253 super_admin_rows.append(usr)
2254 2254
2255 2255 return super_admin_rows + owner_row + perm_rows
2256 2256
2257 2257 def permission_user_groups(self):
2258 2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262 2262
2263 2263 perm_rows = []
2264 2264 for _user_group in q.all():
2265 2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 2266 usr.permission = _user_group.permission.permission_name
2267 2267 perm_rows.append(usr)
2268 2268
2269 2269 return perm_rows
2270 2270
2271 2271 def get_api_data(self):
2272 2272 """
2273 2273 Common function for generating api data
2274 2274
2275 2275 """
2276 2276 group = self
2277 2277 data = {
2278 2278 'group_id': group.group_id,
2279 2279 'group_name': group.group_name,
2280 2280 'group_description': group.group_description,
2281 2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 2282 'repositories': [x.repo_name for x in group.repositories],
2283 2283 'owner': group.user.username,
2284 2284 }
2285 2285 return data
2286 2286
2287 2287
2288 2288 class Permission(Base, BaseModel):
2289 2289 __tablename__ = 'permissions'
2290 2290 __table_args__ = (
2291 2291 Index('p_perm_name_idx', 'permission_name'),
2292 2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 2294 )
2295 2295 PERMS = [
2296 2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297 2297
2298 2298 ('repository.none', _('Repository no access')),
2299 2299 ('repository.read', _('Repository read access')),
2300 2300 ('repository.write', _('Repository write access')),
2301 2301 ('repository.admin', _('Repository admin access')),
2302 2302
2303 2303 ('group.none', _('Repository group no access')),
2304 2304 ('group.read', _('Repository group read access')),
2305 2305 ('group.write', _('Repository group write access')),
2306 2306 ('group.admin', _('Repository group admin access')),
2307 2307
2308 2308 ('usergroup.none', _('User group no access')),
2309 2309 ('usergroup.read', _('User group read access')),
2310 2310 ('usergroup.write', _('User group write access')),
2311 2311 ('usergroup.admin', _('User group admin access')),
2312 2312
2313 2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315 2315
2316 2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318 2318
2319 2319 ('hg.create.none', _('Repository creation disabled')),
2320 2320 ('hg.create.repository', _('Repository creation enabled')),
2321 2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323 2323
2324 2324 ('hg.fork.none', _('Repository forking disabled')),
2325 2325 ('hg.fork.repository', _('Repository forking enabled')),
2326 2326
2327 2327 ('hg.register.none', _('Registration disabled')),
2328 2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330 2330
2331 2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334 2334
2335 2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337 2337
2338 2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 2340 ]
2341 2341
2342 2342 # definition of system default permissions for DEFAULT user
2343 2343 DEFAULT_USER_PERMISSIONS = [
2344 2344 'repository.read',
2345 2345 'group.read',
2346 2346 'usergroup.read',
2347 2347 'hg.create.repository',
2348 2348 'hg.repogroup.create.false',
2349 2349 'hg.usergroup.create.false',
2350 2350 'hg.create.write_on_repogroup.true',
2351 2351 'hg.fork.repository',
2352 2352 'hg.register.manual_activate',
2353 2353 'hg.password_reset.enabled',
2354 2354 'hg.extern_activate.auto',
2355 2355 'hg.inherit_default_perms.true',
2356 2356 ]
2357 2357
2358 2358 # defines which permissions are more important higher the more important
2359 2359 # Weight defines which permissions are more important.
2360 2360 # The higher number the more important.
2361 2361 PERM_WEIGHTS = {
2362 2362 'repository.none': 0,
2363 2363 'repository.read': 1,
2364 2364 'repository.write': 3,
2365 2365 'repository.admin': 4,
2366 2366
2367 2367 'group.none': 0,
2368 2368 'group.read': 1,
2369 2369 'group.write': 3,
2370 2370 'group.admin': 4,
2371 2371
2372 2372 'usergroup.none': 0,
2373 2373 'usergroup.read': 1,
2374 2374 'usergroup.write': 3,
2375 2375 'usergroup.admin': 4,
2376 2376
2377 2377 'hg.repogroup.create.false': 0,
2378 2378 'hg.repogroup.create.true': 1,
2379 2379
2380 2380 'hg.usergroup.create.false': 0,
2381 2381 'hg.usergroup.create.true': 1,
2382 2382
2383 2383 'hg.fork.none': 0,
2384 2384 'hg.fork.repository': 1,
2385 2385 'hg.create.none': 0,
2386 2386 'hg.create.repository': 1
2387 2387 }
2388 2388
2389 2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392 2392
2393 2393 def __unicode__(self):
2394 2394 return u"<%s('%s:%s')>" % (
2395 2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 2396 )
2397 2397
2398 2398 @classmethod
2399 2399 def get_by_key(cls, key):
2400 2400 return cls.query().filter(cls.permission_name == key).scalar()
2401 2401
2402 2402 @classmethod
2403 2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 2407 .filter(UserRepoToPerm.user_id == user_id)
2408 2408 if repo_id:
2409 2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 2410 return q.all()
2411 2411
2412 2412 @classmethod
2413 2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 2415 .join(
2416 2416 Permission,
2417 2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 2418 .join(
2419 2419 Repository,
2420 2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 2421 .join(
2422 2422 UserGroup,
2423 2423 UserGroupRepoToPerm.users_group_id ==
2424 2424 UserGroup.users_group_id)\
2425 2425 .join(
2426 2426 UserGroupMember,
2427 2427 UserGroupRepoToPerm.users_group_id ==
2428 2428 UserGroupMember.users_group_id)\
2429 2429 .filter(
2430 2430 UserGroupMember.user_id == user_id,
2431 2431 UserGroup.users_group_active == true())
2432 2432 if repo_id:
2433 2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 2434 return q.all()
2435 2435
2436 2436 @classmethod
2437 2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 2442 if repo_group_id:
2443 2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 2444 return q.all()
2445 2445
2446 2446 @classmethod
2447 2447 def get_default_group_perms_from_user_group(
2448 2448 cls, user_id, repo_group_id=None):
2449 2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 2450 .join(
2451 2451 Permission,
2452 2452 UserGroupRepoGroupToPerm.permission_id ==
2453 2453 Permission.permission_id)\
2454 2454 .join(
2455 2455 RepoGroup,
2456 2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 2457 .join(
2458 2458 UserGroup,
2459 2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 2460 UserGroup.users_group_id)\
2461 2461 .join(
2462 2462 UserGroupMember,
2463 2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 2464 UserGroupMember.users_group_id)\
2465 2465 .filter(
2466 2466 UserGroupMember.user_id == user_id,
2467 2467 UserGroup.users_group_active == true())
2468 2468 if repo_group_id:
2469 2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 2470 return q.all()
2471 2471
2472 2472 @classmethod
2473 2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 2478 if user_group_id:
2479 2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 2480 return q.all()
2481 2481
2482 2482 @classmethod
2483 2483 def get_default_user_group_perms_from_user_group(
2484 2484 cls, user_id, user_group_id=None):
2485 2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 2487 .join(
2488 2488 Permission,
2489 2489 UserGroupUserGroupToPerm.permission_id ==
2490 2490 Permission.permission_id)\
2491 2491 .join(
2492 2492 TargetUserGroup,
2493 2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 2494 TargetUserGroup.users_group_id)\
2495 2495 .join(
2496 2496 UserGroup,
2497 2497 UserGroupUserGroupToPerm.user_group_id ==
2498 2498 UserGroup.users_group_id)\
2499 2499 .join(
2500 2500 UserGroupMember,
2501 2501 UserGroupUserGroupToPerm.user_group_id ==
2502 2502 UserGroupMember.users_group_id)\
2503 2503 .filter(
2504 2504 UserGroupMember.user_id == user_id,
2505 2505 UserGroup.users_group_active == true())
2506 2506 if user_group_id:
2507 2507 q = q.filter(
2508 2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509 2509
2510 2510 return q.all()
2511 2511
2512 2512
2513 2513 class UserRepoToPerm(Base, BaseModel):
2514 2514 __tablename__ = 'repo_to_perm'
2515 2515 __table_args__ = (
2516 2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 2519 )
2520 2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524 2524
2525 2525 user = relationship('User')
2526 2526 repository = relationship('Repository')
2527 2527 permission = relationship('Permission')
2528 2528
2529 2529 @classmethod
2530 2530 def create(cls, user, repository, permission):
2531 2531 n = cls()
2532 2532 n.user = user
2533 2533 n.repository = repository
2534 2534 n.permission = permission
2535 2535 Session().add(n)
2536 2536 return n
2537 2537
2538 2538 def __unicode__(self):
2539 2539 return u'<%s => %s >' % (self.user, self.repository)
2540 2540
2541 2541
2542 2542 class UserUserGroupToPerm(Base, BaseModel):
2543 2543 __tablename__ = 'user_user_group_to_perm'
2544 2544 __table_args__ = (
2545 2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 2548 )
2549 2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553 2553
2554 2554 user = relationship('User')
2555 2555 user_group = relationship('UserGroup')
2556 2556 permission = relationship('Permission')
2557 2557
2558 2558 @classmethod
2559 2559 def create(cls, user, user_group, permission):
2560 2560 n = cls()
2561 2561 n.user = user
2562 2562 n.user_group = user_group
2563 2563 n.permission = permission
2564 2564 Session().add(n)
2565 2565 return n
2566 2566
2567 2567 def __unicode__(self):
2568 2568 return u'<%s => %s >' % (self.user, self.user_group)
2569 2569
2570 2570
2571 2571 class UserToPerm(Base, BaseModel):
2572 2572 __tablename__ = 'user_to_perm'
2573 2573 __table_args__ = (
2574 2574 UniqueConstraint('user_id', 'permission_id'),
2575 2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2577 )
2578 2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2581
2582 2582 user = relationship('User')
2583 2583 permission = relationship('Permission', lazy='joined')
2584 2584
2585 2585 def __unicode__(self):
2586 2586 return u'<%s => %s >' % (self.user, self.permission)
2587 2587
2588 2588
2589 2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 2590 __tablename__ = 'users_group_repo_to_perm'
2591 2591 __table_args__ = (
2592 2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 2595 )
2596 2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600 2600
2601 2601 users_group = relationship('UserGroup')
2602 2602 permission = relationship('Permission')
2603 2603 repository = relationship('Repository')
2604 2604
2605 2605 @classmethod
2606 2606 def create(cls, users_group, repository, permission):
2607 2607 n = cls()
2608 2608 n.users_group = users_group
2609 2609 n.repository = repository
2610 2610 n.permission = permission
2611 2611 Session().add(n)
2612 2612 return n
2613 2613
2614 2614 def __unicode__(self):
2615 2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616 2616
2617 2617
2618 2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 2619 __tablename__ = 'user_group_user_group_to_perm'
2620 2620 __table_args__ = (
2621 2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2625 )
2626 2626 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2627 2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630 2630
2631 2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 2633 permission = relationship('Permission')
2634 2634
2635 2635 @classmethod
2636 2636 def create(cls, target_user_group, user_group, permission):
2637 2637 n = cls()
2638 2638 n.target_user_group = target_user_group
2639 2639 n.user_group = user_group
2640 2640 n.permission = permission
2641 2641 Session().add(n)
2642 2642 return n
2643 2643
2644 2644 def __unicode__(self):
2645 2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646 2646
2647 2647
2648 2648 class UserGroupToPerm(Base, BaseModel):
2649 2649 __tablename__ = 'users_group_to_perm'
2650 2650 __table_args__ = (
2651 2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 2654 )
2655 2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 2658
2659 2659 users_group = relationship('UserGroup')
2660 2660 permission = relationship('Permission')
2661 2661
2662 2662
2663 2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 2664 __tablename__ = 'user_repo_group_to_perm'
2665 2665 __table_args__ = (
2666 2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 2669 )
2670 2670
2671 2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675 2675
2676 2676 user = relationship('User')
2677 2677 group = relationship('RepoGroup')
2678 2678 permission = relationship('Permission')
2679 2679
2680 2680 @classmethod
2681 2681 def create(cls, user, repository_group, permission):
2682 2682 n = cls()
2683 2683 n.user = user
2684 2684 n.group = repository_group
2685 2685 n.permission = permission
2686 2686 Session().add(n)
2687 2687 return n
2688 2688
2689 2689
2690 2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 2692 __table_args__ = (
2693 2693 UniqueConstraint('users_group_id', 'group_id'),
2694 2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 2696 )
2697 2697
2698 2698 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2699 2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 2702
2703 2703 users_group = relationship('UserGroup')
2704 2704 permission = relationship('Permission')
2705 2705 group = relationship('RepoGroup')
2706 2706
2707 2707 @classmethod
2708 2708 def create(cls, user_group, repository_group, permission):
2709 2709 n = cls()
2710 2710 n.users_group = user_group
2711 2711 n.group = repository_group
2712 2712 n.permission = permission
2713 2713 Session().add(n)
2714 2714 return n
2715 2715
2716 2716 def __unicode__(self):
2717 2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718 2718
2719 2719
2720 2720 class Statistics(Base, BaseModel):
2721 2721 __tablename__ = 'statistics'
2722 2722 __table_args__ = (
2723 2723 UniqueConstraint('repository_id'),
2724 2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2726 )
2727 2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733 2733
2734 2734 repository = relationship('Repository', single_parent=True)
2735 2735
2736 2736
2737 2737 class UserFollowing(Base, BaseModel):
2738 2738 __tablename__ = 'user_followings'
2739 2739 __table_args__ = (
2740 2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2744 )
2745 2745
2746 2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751 2751
2752 2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753 2753
2754 2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756 2756
2757 2757 @classmethod
2758 2758 def get_repo_followers(cls, repo_id):
2759 2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760 2760
2761 2761
2762 2762 class CacheKey(Base, BaseModel):
2763 2763 __tablename__ = 'cache_invalidation'
2764 2764 __table_args__ = (
2765 2765 UniqueConstraint('cache_key'),
2766 2766 Index('key_idx', 'cache_key'),
2767 2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 2769 )
2770 2770 CACHE_TYPE_ATOM = 'ATOM'
2771 2771 CACHE_TYPE_RSS = 'RSS'
2772 2772 CACHE_TYPE_README = 'README'
2773 2773
2774 2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778 2778
2779 2779 def __init__(self, cache_key, cache_args=''):
2780 2780 self.cache_key = cache_key
2781 2781 self.cache_args = cache_args
2782 2782 self.cache_active = False
2783 2783
2784 2784 def __unicode__(self):
2785 2785 return u"<%s('%s:%s[%s]')>" % (
2786 2786 self.__class__.__name__,
2787 2787 self.cache_id, self.cache_key, self.cache_active)
2788 2788
2789 2789 def _cache_key_partition(self):
2790 2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 2791 return prefix, repo_name, suffix
2792 2792
2793 2793 def get_prefix(self):
2794 2794 """
2795 2795 Try to extract prefix from existing cache key. The key could consist
2796 2796 of prefix, repo_name, suffix
2797 2797 """
2798 2798 # this returns prefix, repo_name, suffix
2799 2799 return self._cache_key_partition()[0]
2800 2800
2801 2801 def get_suffix(self):
2802 2802 """
2803 2803 get suffix that might have been used in _get_cache_key to
2804 2804 generate self.cache_key. Only used for informational purposes
2805 2805 in repo_edit.mako.
2806 2806 """
2807 2807 # prefix, repo_name, suffix
2808 2808 return self._cache_key_partition()[2]
2809 2809
2810 2810 @classmethod
2811 2811 def delete_all_cache(cls):
2812 2812 """
2813 2813 Delete all cache keys from database.
2814 2814 Should only be run when all instances are down and all entries
2815 2815 thus stale.
2816 2816 """
2817 2817 cls.query().delete()
2818 2818 Session().commit()
2819 2819
2820 2820 @classmethod
2821 2821 def get_cache_key(cls, repo_name, cache_type):
2822 2822 """
2823 2823
2824 2824 Generate a cache key for this process of RhodeCode instance.
2825 2825 Prefix most likely will be process id or maybe explicitly set
2826 2826 instance_id from .ini file.
2827 2827 """
2828 2828 import rhodecode
2829 2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830 2830
2831 2831 repo_as_unicode = safe_unicode(repo_name)
2832 2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 2833 if cache_type else repo_as_unicode
2834 2834
2835 2835 return u'{}{}'.format(prefix, key)
2836 2836
2837 2837 @classmethod
2838 2838 def set_invalidate(cls, repo_name, delete=False):
2839 2839 """
2840 2840 Mark all caches of a repo as invalid in the database.
2841 2841 """
2842 2842
2843 2843 try:
2844 2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 2845 if delete:
2846 2846 log.debug('cache objects deleted for repo %s',
2847 2847 safe_str(repo_name))
2848 2848 qry.delete()
2849 2849 else:
2850 2850 log.debug('cache objects marked as invalid for repo %s',
2851 2851 safe_str(repo_name))
2852 2852 qry.update({"cache_active": False})
2853 2853
2854 2854 Session().commit()
2855 2855 except Exception:
2856 2856 log.exception(
2857 2857 'Cache key invalidation failed for repository %s',
2858 2858 safe_str(repo_name))
2859 2859 Session().rollback()
2860 2860
2861 2861 @classmethod
2862 2862 def get_active_cache(cls, cache_key):
2863 2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 2864 if inv_obj:
2865 2865 return inv_obj
2866 2866 return None
2867 2867
2868 2868 @classmethod
2869 2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 2870 thread_scoped=False):
2871 2871 """
2872 2872 @cache_region('long_term')
2873 2873 def _heavy_calculation(cache_key):
2874 2874 return 'result'
2875 2875
2876 2876 cache_context = CacheKey.repo_context_cache(
2877 2877 _heavy_calculation, repo_name, cache_type)
2878 2878
2879 2879 with cache_context as context:
2880 2880 context.invalidate()
2881 2881 computed = context.compute()
2882 2882
2883 2883 assert computed == 'result'
2884 2884 """
2885 2885 from rhodecode.lib import caches
2886 2886 return caches.InvalidationContext(
2887 2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888 2888
2889 2889
2890 2890 class ChangesetComment(Base, BaseModel):
2891 2891 __tablename__ = 'changeset_comments'
2892 2892 __table_args__ = (
2893 2893 Index('cc_revision_idx', 'revision'),
2894 2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 2896 )
2897 2897
2898 2898 COMMENT_OUTDATED = u'comment_outdated'
2899 2899 COMMENT_TYPE_NOTE = u'note'
2900 2900 COMMENT_TYPE_TODO = u'todo'
2901 2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2902 2902
2903 2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2904 2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2905 2905 revision = Column('revision', String(40), nullable=True)
2906 2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2907 2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2908 2908 line_no = Column('line_no', Unicode(10), nullable=True)
2909 2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2910 2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2911 2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2912 2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2913 2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2915 2915 renderer = Column('renderer', Unicode(64), nullable=True)
2916 2916 display_state = Column('display_state', Unicode(128), nullable=True)
2917 2917
2918 2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2921 2921 author = relationship('User', lazy='joined')
2922 2922 repo = relationship('Repository')
2923 2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2924 2924 pull_request = relationship('PullRequest', lazy='joined')
2925 2925 pull_request_version = relationship('PullRequestVersion')
2926 2926
2927 2927 @classmethod
2928 2928 def get_users(cls, revision=None, pull_request_id=None):
2929 2929 """
2930 2930 Returns user associated with this ChangesetComment. ie those
2931 2931 who actually commented
2932 2932
2933 2933 :param cls:
2934 2934 :param revision:
2935 2935 """
2936 2936 q = Session().query(User)\
2937 2937 .join(ChangesetComment.author)
2938 2938 if revision:
2939 2939 q = q.filter(cls.revision == revision)
2940 2940 elif pull_request_id:
2941 2941 q = q.filter(cls.pull_request_id == pull_request_id)
2942 2942 return q.all()
2943 2943
2944 2944 @classmethod
2945 2945 def get_index_from_version(cls, pr_version, versions):
2946 2946 num_versions = [x.pull_request_version_id for x in versions]
2947 2947 try:
2948 2948 return num_versions.index(pr_version) +1
2949 2949 except (IndexError, ValueError):
2950 2950 return
2951 2951
2952 2952 @property
2953 2953 def outdated(self):
2954 2954 return self.display_state == self.COMMENT_OUTDATED
2955 2955
2956 2956 def outdated_at_version(self, version):
2957 2957 """
2958 2958 Checks if comment is outdated for given pull request version
2959 2959 """
2960 2960 return self.outdated and self.pull_request_version_id != version
2961 2961
2962 @property
2963 def resolved(self):
2964 return self.resolved_by[0] if self.resolved_by else None
2965
2962 2966 def get_index_version(self, versions):
2963 2967 return self.get_index_from_version(
2964 2968 self.pull_request_version_id, versions)
2965 2969
2966 2970 def render(self, mentions=False):
2967 2971 from rhodecode.lib import helpers as h
2968 2972 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2969 2973
2970 2974 def __repr__(self):
2971 2975 if self.comment_id:
2972 2976 return '<DB:ChangesetComment #%s>' % self.comment_id
2973 2977 else:
2974 2978 return '<DB:ChangesetComment at %#x>' % id(self)
2975 2979
2976 2980
2977 2981 class ChangesetStatus(Base, BaseModel):
2978 2982 __tablename__ = 'changeset_statuses'
2979 2983 __table_args__ = (
2980 2984 Index('cs_revision_idx', 'revision'),
2981 2985 Index('cs_version_idx', 'version'),
2982 2986 UniqueConstraint('repo_id', 'revision', 'version'),
2983 2987 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2984 2988 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2985 2989 )
2986 2990 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2987 2991 STATUS_APPROVED = 'approved'
2988 2992 STATUS_REJECTED = 'rejected'
2989 2993 STATUS_UNDER_REVIEW = 'under_review'
2990 2994
2991 2995 STATUSES = [
2992 2996 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2993 2997 (STATUS_APPROVED, _("Approved")),
2994 2998 (STATUS_REJECTED, _("Rejected")),
2995 2999 (STATUS_UNDER_REVIEW, _("Under Review")),
2996 3000 ]
2997 3001
2998 3002 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2999 3003 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3000 3004 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3001 3005 revision = Column('revision', String(40), nullable=False)
3002 3006 status = Column('status', String(128), nullable=False, default=DEFAULT)
3003 3007 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3004 3008 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3005 3009 version = Column('version', Integer(), nullable=False, default=0)
3006 3010 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3007 3011
3008 3012 author = relationship('User', lazy='joined')
3009 3013 repo = relationship('Repository')
3010 3014 comment = relationship('ChangesetComment', lazy='joined')
3011 3015 pull_request = relationship('PullRequest', lazy='joined')
3012 3016
3013 3017 def __unicode__(self):
3014 3018 return u"<%s('%s[%s]:%s')>" % (
3015 3019 self.__class__.__name__,
3016 3020 self.status, self.version, self.author
3017 3021 )
3018 3022
3019 3023 @classmethod
3020 3024 def get_status_lbl(cls, value):
3021 3025 return dict(cls.STATUSES).get(value)
3022 3026
3023 3027 @property
3024 3028 def status_lbl(self):
3025 3029 return ChangesetStatus.get_status_lbl(self.status)
3026 3030
3027 3031
3028 3032 class _PullRequestBase(BaseModel):
3029 3033 """
3030 3034 Common attributes of pull request and version entries.
3031 3035 """
3032 3036
3033 3037 # .status values
3034 3038 STATUS_NEW = u'new'
3035 3039 STATUS_OPEN = u'open'
3036 3040 STATUS_CLOSED = u'closed'
3037 3041
3038 3042 title = Column('title', Unicode(255), nullable=True)
3039 3043 description = Column(
3040 3044 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3041 3045 nullable=True)
3042 3046 # new/open/closed status of pull request (not approve/reject/etc)
3043 3047 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3044 3048 created_on = Column(
3045 3049 'created_on', DateTime(timezone=False), nullable=False,
3046 3050 default=datetime.datetime.now)
3047 3051 updated_on = Column(
3048 3052 'updated_on', DateTime(timezone=False), nullable=False,
3049 3053 default=datetime.datetime.now)
3050 3054
3051 3055 @declared_attr
3052 3056 def user_id(cls):
3053 3057 return Column(
3054 3058 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3055 3059 unique=None)
3056 3060
3057 3061 # 500 revisions max
3058 3062 _revisions = Column(
3059 3063 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3060 3064
3061 3065 @declared_attr
3062 3066 def source_repo_id(cls):
3063 3067 # TODO: dan: rename column to source_repo_id
3064 3068 return Column(
3065 3069 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3066 3070 nullable=False)
3067 3071
3068 3072 source_ref = Column('org_ref', Unicode(255), nullable=False)
3069 3073
3070 3074 @declared_attr
3071 3075 def target_repo_id(cls):
3072 3076 # TODO: dan: rename column to target_repo_id
3073 3077 return Column(
3074 3078 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3075 3079 nullable=False)
3076 3080
3077 3081 target_ref = Column('other_ref', Unicode(255), nullable=False)
3078 3082 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3079 3083
3080 3084 # TODO: dan: rename column to last_merge_source_rev
3081 3085 _last_merge_source_rev = Column(
3082 3086 'last_merge_org_rev', String(40), nullable=True)
3083 3087 # TODO: dan: rename column to last_merge_target_rev
3084 3088 _last_merge_target_rev = Column(
3085 3089 'last_merge_other_rev', String(40), nullable=True)
3086 3090 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3087 3091 merge_rev = Column('merge_rev', String(40), nullable=True)
3088 3092
3089 3093 @hybrid_property
3090 3094 def revisions(self):
3091 3095 return self._revisions.split(':') if self._revisions else []
3092 3096
3093 3097 @revisions.setter
3094 3098 def revisions(self, val):
3095 3099 self._revisions = ':'.join(val)
3096 3100
3097 3101 @declared_attr
3098 3102 def author(cls):
3099 3103 return relationship('User', lazy='joined')
3100 3104
3101 3105 @declared_attr
3102 3106 def source_repo(cls):
3103 3107 return relationship(
3104 3108 'Repository',
3105 3109 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3106 3110
3107 3111 @property
3108 3112 def source_ref_parts(self):
3109 3113 return self.unicode_to_reference(self.source_ref)
3110 3114
3111 3115 @declared_attr
3112 3116 def target_repo(cls):
3113 3117 return relationship(
3114 3118 'Repository',
3115 3119 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3116 3120
3117 3121 @property
3118 3122 def target_ref_parts(self):
3119 3123 return self.unicode_to_reference(self.target_ref)
3120 3124
3121 3125 @property
3122 3126 def shadow_merge_ref(self):
3123 3127 return self.unicode_to_reference(self._shadow_merge_ref)
3124 3128
3125 3129 @shadow_merge_ref.setter
3126 3130 def shadow_merge_ref(self, ref):
3127 3131 self._shadow_merge_ref = self.reference_to_unicode(ref)
3128 3132
3129 3133 def unicode_to_reference(self, raw):
3130 3134 """
3131 3135 Convert a unicode (or string) to a reference object.
3132 3136 If unicode evaluates to False it returns None.
3133 3137 """
3134 3138 if raw:
3135 3139 refs = raw.split(':')
3136 3140 return Reference(*refs)
3137 3141 else:
3138 3142 return None
3139 3143
3140 3144 def reference_to_unicode(self, ref):
3141 3145 """
3142 3146 Convert a reference object to unicode.
3143 3147 If reference is None it returns None.
3144 3148 """
3145 3149 if ref:
3146 3150 return u':'.join(ref)
3147 3151 else:
3148 3152 return None
3149 3153
3150 3154 def get_api_data(self):
3151 3155 from rhodecode.model.pull_request import PullRequestModel
3152 3156 pull_request = self
3153 3157 merge_status = PullRequestModel().merge_status(pull_request)
3154 3158
3155 3159 pull_request_url = url(
3156 3160 'pullrequest_show', repo_name=self.target_repo.repo_name,
3157 3161 pull_request_id=self.pull_request_id, qualified=True)
3158 3162
3159 3163 merge_data = {
3160 3164 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3161 3165 'reference': (
3162 3166 pull_request.shadow_merge_ref._asdict()
3163 3167 if pull_request.shadow_merge_ref else None),
3164 3168 }
3165 3169
3166 3170 data = {
3167 3171 'pull_request_id': pull_request.pull_request_id,
3168 3172 'url': pull_request_url,
3169 3173 'title': pull_request.title,
3170 3174 'description': pull_request.description,
3171 3175 'status': pull_request.status,
3172 3176 'created_on': pull_request.created_on,
3173 3177 'updated_on': pull_request.updated_on,
3174 3178 'commit_ids': pull_request.revisions,
3175 3179 'review_status': pull_request.calculated_review_status(),
3176 3180 'mergeable': {
3177 3181 'status': merge_status[0],
3178 3182 'message': unicode(merge_status[1]),
3179 3183 },
3180 3184 'source': {
3181 3185 'clone_url': pull_request.source_repo.clone_url(),
3182 3186 'repository': pull_request.source_repo.repo_name,
3183 3187 'reference': {
3184 3188 'name': pull_request.source_ref_parts.name,
3185 3189 'type': pull_request.source_ref_parts.type,
3186 3190 'commit_id': pull_request.source_ref_parts.commit_id,
3187 3191 },
3188 3192 },
3189 3193 'target': {
3190 3194 'clone_url': pull_request.target_repo.clone_url(),
3191 3195 'repository': pull_request.target_repo.repo_name,
3192 3196 'reference': {
3193 3197 'name': pull_request.target_ref_parts.name,
3194 3198 'type': pull_request.target_ref_parts.type,
3195 3199 'commit_id': pull_request.target_ref_parts.commit_id,
3196 3200 },
3197 3201 },
3198 3202 'merge': merge_data,
3199 3203 'author': pull_request.author.get_api_data(include_secrets=False,
3200 3204 details='basic'),
3201 3205 'reviewers': [
3202 3206 {
3203 3207 'user': reviewer.get_api_data(include_secrets=False,
3204 3208 details='basic'),
3205 3209 'reasons': reasons,
3206 3210 'review_status': st[0][1].status if st else 'not_reviewed',
3207 3211 }
3208 3212 for reviewer, reasons, st in pull_request.reviewers_statuses()
3209 3213 ]
3210 3214 }
3211 3215
3212 3216 return data
3213 3217
3214 3218
3215 3219 class PullRequest(Base, _PullRequestBase):
3216 3220 __tablename__ = 'pull_requests'
3217 3221 __table_args__ = (
3218 3222 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3219 3223 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3220 3224 )
3221 3225
3222 3226 pull_request_id = Column(
3223 3227 'pull_request_id', Integer(), nullable=False, primary_key=True)
3224 3228
3225 3229 def __repr__(self):
3226 3230 if self.pull_request_id:
3227 3231 return '<DB:PullRequest #%s>' % self.pull_request_id
3228 3232 else:
3229 3233 return '<DB:PullRequest at %#x>' % id(self)
3230 3234
3231 3235 reviewers = relationship('PullRequestReviewers',
3232 3236 cascade="all, delete, delete-orphan")
3233 3237 statuses = relationship('ChangesetStatus')
3234 3238 comments = relationship('ChangesetComment',
3235 3239 cascade="all, delete, delete-orphan")
3236 3240 versions = relationship('PullRequestVersion',
3237 3241 cascade="all, delete, delete-orphan",
3238 3242 lazy='dynamic')
3239 3243
3240 3244
3241 3245 @classmethod
3242 3246 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3243 3247 internal_methods=None):
3244 3248
3245 3249 class PullRequestDisplay(object):
3246 3250 """
3247 3251 Special object wrapper for showing PullRequest data via Versions
3248 3252 It mimics PR object as close as possible. This is read only object
3249 3253 just for display
3250 3254 """
3251 3255
3252 3256 def __init__(self, attrs, internal=None):
3253 3257 self.attrs = attrs
3254 3258 # internal have priority over the given ones via attrs
3255 3259 self.internal = internal or ['versions']
3256 3260
3257 3261 def __getattr__(self, item):
3258 3262 if item in self.internal:
3259 3263 return getattr(self, item)
3260 3264 try:
3261 3265 return self.attrs[item]
3262 3266 except KeyError:
3263 3267 raise AttributeError(
3264 3268 '%s object has no attribute %s' % (self, item))
3265 3269
3266 3270 def __repr__(self):
3267 3271 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3268 3272
3269 3273 def versions(self):
3270 3274 return pull_request_obj.versions.order_by(
3271 3275 PullRequestVersion.pull_request_version_id).all()
3272 3276
3273 3277 def is_closed(self):
3274 3278 return pull_request_obj.is_closed()
3275 3279
3276 3280 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3277 3281
3278 3282 attrs.author = StrictAttributeDict(
3279 3283 pull_request_obj.author.get_api_data())
3280 3284 if pull_request_obj.target_repo:
3281 3285 attrs.target_repo = StrictAttributeDict(
3282 3286 pull_request_obj.target_repo.get_api_data())
3283 3287 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3284 3288
3285 3289 if pull_request_obj.source_repo:
3286 3290 attrs.source_repo = StrictAttributeDict(
3287 3291 pull_request_obj.source_repo.get_api_data())
3288 3292 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3289 3293
3290 3294 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3291 3295 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3292 3296 attrs.revisions = pull_request_obj.revisions
3293 3297
3294 3298 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3295 3299
3296 3300 return PullRequestDisplay(attrs, internal=internal_methods)
3297 3301
3298 3302 def is_closed(self):
3299 3303 return self.status == self.STATUS_CLOSED
3300 3304
3301 3305 def __json__(self):
3302 3306 return {
3303 3307 'revisions': self.revisions,
3304 3308 }
3305 3309
3306 3310 def calculated_review_status(self):
3307 3311 from rhodecode.model.changeset_status import ChangesetStatusModel
3308 3312 return ChangesetStatusModel().calculated_review_status(self)
3309 3313
3310 3314 def reviewers_statuses(self):
3311 3315 from rhodecode.model.changeset_status import ChangesetStatusModel
3312 3316 return ChangesetStatusModel().reviewers_statuses(self)
3313 3317
3314 3318
3315 3319 class PullRequestVersion(Base, _PullRequestBase):
3316 3320 __tablename__ = 'pull_request_versions'
3317 3321 __table_args__ = (
3318 3322 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3319 3323 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3320 3324 )
3321 3325
3322 3326 pull_request_version_id = Column(
3323 3327 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3324 3328 pull_request_id = Column(
3325 3329 'pull_request_id', Integer(),
3326 3330 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3327 3331 pull_request = relationship('PullRequest')
3328 3332
3329 3333 def __repr__(self):
3330 3334 if self.pull_request_version_id:
3331 3335 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3332 3336 else:
3333 3337 return '<DB:PullRequestVersion at %#x>' % id(self)
3334 3338
3335 3339 @property
3336 3340 def reviewers(self):
3337 3341 return self.pull_request.reviewers
3338 3342
3339 3343 @property
3340 3344 def versions(self):
3341 3345 return self.pull_request.versions
3342 3346
3343 3347 def is_closed(self):
3344 3348 # calculate from original
3345 3349 return self.pull_request.status == self.STATUS_CLOSED
3346 3350
3347 3351 def calculated_review_status(self):
3348 3352 return self.pull_request.calculated_review_status()
3349 3353
3350 3354 def reviewers_statuses(self):
3351 3355 return self.pull_request.reviewers_statuses()
3352 3356
3353 3357
3354 3358 class PullRequestReviewers(Base, BaseModel):
3355 3359 __tablename__ = 'pull_request_reviewers'
3356 3360 __table_args__ = (
3357 3361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3358 3362 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3359 3363 )
3360 3364
3361 3365 def __init__(self, user=None, pull_request=None, reasons=None):
3362 3366 self.user = user
3363 3367 self.pull_request = pull_request
3364 3368 self.reasons = reasons or []
3365 3369
3366 3370 @hybrid_property
3367 3371 def reasons(self):
3368 3372 if not self._reasons:
3369 3373 return []
3370 3374 return self._reasons
3371 3375
3372 3376 @reasons.setter
3373 3377 def reasons(self, val):
3374 3378 val = val or []
3375 3379 if any(not isinstance(x, basestring) for x in val):
3376 3380 raise Exception('invalid reasons type, must be list of strings')
3377 3381 self._reasons = val
3378 3382
3379 3383 pull_requests_reviewers_id = Column(
3380 3384 'pull_requests_reviewers_id', Integer(), nullable=False,
3381 3385 primary_key=True)
3382 3386 pull_request_id = Column(
3383 3387 "pull_request_id", Integer(),
3384 3388 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3385 3389 user_id = Column(
3386 3390 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3387 3391 _reasons = Column(
3388 3392 'reason', MutationList.as_mutable(
3389 3393 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3390 3394
3391 3395 user = relationship('User')
3392 3396 pull_request = relationship('PullRequest')
3393 3397
3394 3398
3395 3399 class Notification(Base, BaseModel):
3396 3400 __tablename__ = 'notifications'
3397 3401 __table_args__ = (
3398 3402 Index('notification_type_idx', 'type'),
3399 3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3400 3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3401 3405 )
3402 3406
3403 3407 TYPE_CHANGESET_COMMENT = u'cs_comment'
3404 3408 TYPE_MESSAGE = u'message'
3405 3409 TYPE_MENTION = u'mention'
3406 3410 TYPE_REGISTRATION = u'registration'
3407 3411 TYPE_PULL_REQUEST = u'pull_request'
3408 3412 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3409 3413
3410 3414 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3411 3415 subject = Column('subject', Unicode(512), nullable=True)
3412 3416 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3413 3417 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3414 3418 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3415 3419 type_ = Column('type', Unicode(255))
3416 3420
3417 3421 created_by_user = relationship('User')
3418 3422 notifications_to_users = relationship('UserNotification', lazy='joined',
3419 3423 cascade="all, delete, delete-orphan")
3420 3424
3421 3425 @property
3422 3426 def recipients(self):
3423 3427 return [x.user for x in UserNotification.query()\
3424 3428 .filter(UserNotification.notification == self)\
3425 3429 .order_by(UserNotification.user_id.asc()).all()]
3426 3430
3427 3431 @classmethod
3428 3432 def create(cls, created_by, subject, body, recipients, type_=None):
3429 3433 if type_ is None:
3430 3434 type_ = Notification.TYPE_MESSAGE
3431 3435
3432 3436 notification = cls()
3433 3437 notification.created_by_user = created_by
3434 3438 notification.subject = subject
3435 3439 notification.body = body
3436 3440 notification.type_ = type_
3437 3441 notification.created_on = datetime.datetime.now()
3438 3442
3439 3443 for u in recipients:
3440 3444 assoc = UserNotification()
3441 3445 assoc.notification = notification
3442 3446
3443 3447 # if created_by is inside recipients mark his notification
3444 3448 # as read
3445 3449 if u.user_id == created_by.user_id:
3446 3450 assoc.read = True
3447 3451
3448 3452 u.notifications.append(assoc)
3449 3453 Session().add(notification)
3450 3454
3451 3455 return notification
3452 3456
3453 3457 @property
3454 3458 def description(self):
3455 3459 from rhodecode.model.notification import NotificationModel
3456 3460 return NotificationModel().make_description(self)
3457 3461
3458 3462
3459 3463 class UserNotification(Base, BaseModel):
3460 3464 __tablename__ = 'user_to_notification'
3461 3465 __table_args__ = (
3462 3466 UniqueConstraint('user_id', 'notification_id'),
3463 3467 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3464 3468 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3465 3469 )
3466 3470 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3467 3471 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3468 3472 read = Column('read', Boolean, default=False)
3469 3473 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3470 3474
3471 3475 user = relationship('User', lazy="joined")
3472 3476 notification = relationship('Notification', lazy="joined",
3473 3477 order_by=lambda: Notification.created_on.desc(),)
3474 3478
3475 3479 def mark_as_read(self):
3476 3480 self.read = True
3477 3481 Session().add(self)
3478 3482
3479 3483
3480 3484 class Gist(Base, BaseModel):
3481 3485 __tablename__ = 'gists'
3482 3486 __table_args__ = (
3483 3487 Index('g_gist_access_id_idx', 'gist_access_id'),
3484 3488 Index('g_created_on_idx', 'created_on'),
3485 3489 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3486 3490 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3487 3491 )
3488 3492 GIST_PUBLIC = u'public'
3489 3493 GIST_PRIVATE = u'private'
3490 3494 DEFAULT_FILENAME = u'gistfile1.txt'
3491 3495
3492 3496 ACL_LEVEL_PUBLIC = u'acl_public'
3493 3497 ACL_LEVEL_PRIVATE = u'acl_private'
3494 3498
3495 3499 gist_id = Column('gist_id', Integer(), primary_key=True)
3496 3500 gist_access_id = Column('gist_access_id', Unicode(250))
3497 3501 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3498 3502 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3499 3503 gist_expires = Column('gist_expires', Float(53), nullable=False)
3500 3504 gist_type = Column('gist_type', Unicode(128), nullable=False)
3501 3505 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3502 3506 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3503 3507 acl_level = Column('acl_level', Unicode(128), nullable=True)
3504 3508
3505 3509 owner = relationship('User')
3506 3510
3507 3511 def __repr__(self):
3508 3512 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3509 3513
3510 3514 @classmethod
3511 3515 def get_or_404(cls, id_):
3512 3516 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3513 3517 if not res:
3514 3518 raise HTTPNotFound
3515 3519 return res
3516 3520
3517 3521 @classmethod
3518 3522 def get_by_access_id(cls, gist_access_id):
3519 3523 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3520 3524
3521 3525 def gist_url(self):
3522 3526 import rhodecode
3523 3527 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3524 3528 if alias_url:
3525 3529 return alias_url.replace('{gistid}', self.gist_access_id)
3526 3530
3527 3531 return url('gist', gist_id=self.gist_access_id, qualified=True)
3528 3532
3529 3533 @classmethod
3530 3534 def base_path(cls):
3531 3535 """
3532 3536 Returns base path when all gists are stored
3533 3537
3534 3538 :param cls:
3535 3539 """
3536 3540 from rhodecode.model.gist import GIST_STORE_LOC
3537 3541 q = Session().query(RhodeCodeUi)\
3538 3542 .filter(RhodeCodeUi.ui_key == URL_SEP)
3539 3543 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3540 3544 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3541 3545
3542 3546 def get_api_data(self):
3543 3547 """
3544 3548 Common function for generating gist related data for API
3545 3549 """
3546 3550 gist = self
3547 3551 data = {
3548 3552 'gist_id': gist.gist_id,
3549 3553 'type': gist.gist_type,
3550 3554 'access_id': gist.gist_access_id,
3551 3555 'description': gist.gist_description,
3552 3556 'url': gist.gist_url(),
3553 3557 'expires': gist.gist_expires,
3554 3558 'created_on': gist.created_on,
3555 3559 'modified_at': gist.modified_at,
3556 3560 'content': None,
3557 3561 'acl_level': gist.acl_level,
3558 3562 }
3559 3563 return data
3560 3564
3561 3565 def __json__(self):
3562 3566 data = dict(
3563 3567 )
3564 3568 data.update(self.get_api_data())
3565 3569 return data
3566 3570 # SCM functions
3567 3571
3568 3572 def scm_instance(self, **kwargs):
3569 3573 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3570 3574 return get_vcs_instance(
3571 3575 repo_path=safe_str(full_repo_path), create=False)
3572 3576
3573 3577
3574 3578 class ExternalIdentity(Base, BaseModel):
3575 3579 __tablename__ = 'external_identities'
3576 3580 __table_args__ = (
3577 3581 Index('local_user_id_idx', 'local_user_id'),
3578 3582 Index('external_id_idx', 'external_id'),
3579 3583 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3580 3584 'mysql_charset': 'utf8'})
3581 3585
3582 3586 external_id = Column('external_id', Unicode(255), default=u'',
3583 3587 primary_key=True)
3584 3588 external_username = Column('external_username', Unicode(1024), default=u'')
3585 3589 local_user_id = Column('local_user_id', Integer(),
3586 3590 ForeignKey('users.user_id'), primary_key=True)
3587 3591 provider_name = Column('provider_name', Unicode(255), default=u'',
3588 3592 primary_key=True)
3589 3593 access_token = Column('access_token', String(1024), default=u'')
3590 3594 alt_token = Column('alt_token', String(1024), default=u'')
3591 3595 token_secret = Column('token_secret', String(1024), default=u'')
3592 3596
3593 3597 @classmethod
3594 3598 def by_external_id_and_provider(cls, external_id, provider_name,
3595 3599 local_user_id=None):
3596 3600 """
3597 3601 Returns ExternalIdentity instance based on search params
3598 3602
3599 3603 :param external_id:
3600 3604 :param provider_name:
3601 3605 :return: ExternalIdentity
3602 3606 """
3603 3607 query = cls.query()
3604 3608 query = query.filter(cls.external_id == external_id)
3605 3609 query = query.filter(cls.provider_name == provider_name)
3606 3610 if local_user_id:
3607 3611 query = query.filter(cls.local_user_id == local_user_id)
3608 3612 return query.first()
3609 3613
3610 3614 @classmethod
3611 3615 def user_by_external_id_and_provider(cls, external_id, provider_name):
3612 3616 """
3613 3617 Returns User instance based on search params
3614 3618
3615 3619 :param external_id:
3616 3620 :param provider_name:
3617 3621 :return: User
3618 3622 """
3619 3623 query = User.query()
3620 3624 query = query.filter(cls.external_id == external_id)
3621 3625 query = query.filter(cls.provider_name == provider_name)
3622 3626 query = query.filter(User.user_id == cls.local_user_id)
3623 3627 return query.first()
3624 3628
3625 3629 @classmethod
3626 3630 def by_local_user_id(cls, local_user_id):
3627 3631 """
3628 3632 Returns all tokens for user
3629 3633
3630 3634 :param local_user_id:
3631 3635 :return: ExternalIdentity
3632 3636 """
3633 3637 query = cls.query()
3634 3638 query = query.filter(cls.local_user_id == local_user_id)
3635 3639 return query
3636 3640
3637 3641
3638 3642 class Integration(Base, BaseModel):
3639 3643 __tablename__ = 'integrations'
3640 3644 __table_args__ = (
3641 3645 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3642 3646 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3643 3647 )
3644 3648
3645 3649 integration_id = Column('integration_id', Integer(), primary_key=True)
3646 3650 integration_type = Column('integration_type', String(255))
3647 3651 enabled = Column('enabled', Boolean(), nullable=False)
3648 3652 name = Column('name', String(255), nullable=False)
3649 3653 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3650 3654 default=False)
3651 3655
3652 3656 settings = Column(
3653 3657 'settings_json', MutationObj.as_mutable(
3654 3658 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3655 3659 repo_id = Column(
3656 3660 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3657 3661 nullable=True, unique=None, default=None)
3658 3662 repo = relationship('Repository', lazy='joined')
3659 3663
3660 3664 repo_group_id = Column(
3661 3665 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3662 3666 nullable=True, unique=None, default=None)
3663 3667 repo_group = relationship('RepoGroup', lazy='joined')
3664 3668
3665 3669 @property
3666 3670 def scope(self):
3667 3671 if self.repo:
3668 3672 return repr(self.repo)
3669 3673 if self.repo_group:
3670 3674 if self.child_repos_only:
3671 3675 return repr(self.repo_group) + ' (child repos only)'
3672 3676 else:
3673 3677 return repr(self.repo_group) + ' (recursive)'
3674 3678 if self.child_repos_only:
3675 3679 return 'root_repos'
3676 3680 return 'global'
3677 3681
3678 3682 def __repr__(self):
3679 3683 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3680 3684
3681 3685
3682 3686 class RepoReviewRuleUser(Base, BaseModel):
3683 3687 __tablename__ = 'repo_review_rules_users'
3684 3688 __table_args__ = (
3685 3689 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3686 3690 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3687 3691 )
3688 3692 repo_review_rule_user_id = Column(
3689 3693 'repo_review_rule_user_id', Integer(), primary_key=True)
3690 3694 repo_review_rule_id = Column("repo_review_rule_id",
3691 3695 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3692 3696 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3693 3697 nullable=False)
3694 3698 user = relationship('User')
3695 3699
3696 3700
3697 3701 class RepoReviewRuleUserGroup(Base, BaseModel):
3698 3702 __tablename__ = 'repo_review_rules_users_groups'
3699 3703 __table_args__ = (
3700 3704 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3701 3705 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3702 3706 )
3703 3707 repo_review_rule_users_group_id = Column(
3704 3708 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3705 3709 repo_review_rule_id = Column("repo_review_rule_id",
3706 3710 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3707 3711 users_group_id = Column("users_group_id", Integer(),
3708 3712 ForeignKey('users_groups.users_group_id'), nullable=False)
3709 3713 users_group = relationship('UserGroup')
3710 3714
3711 3715
3712 3716 class RepoReviewRule(Base, BaseModel):
3713 3717 __tablename__ = 'repo_review_rules'
3714 3718 __table_args__ = (
3715 3719 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3716 3720 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3717 3721 )
3718 3722
3719 3723 repo_review_rule_id = Column(
3720 3724 'repo_review_rule_id', Integer(), primary_key=True)
3721 3725 repo_id = Column(
3722 3726 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3723 3727 repo = relationship('Repository', backref='review_rules')
3724 3728
3725 3729 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3726 3730 default=u'*') # glob
3727 3731 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3728 3732 default=u'*') # glob
3729 3733
3730 3734 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3731 3735 nullable=False, default=False)
3732 3736 rule_users = relationship('RepoReviewRuleUser')
3733 3737 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3734 3738
3735 3739 @hybrid_property
3736 3740 def branch_pattern(self):
3737 3741 return self._branch_pattern or '*'
3738 3742
3739 3743 def _validate_glob(self, value):
3740 3744 re.compile('^' + glob2re(value) + '$')
3741 3745
3742 3746 @branch_pattern.setter
3743 3747 def branch_pattern(self, value):
3744 3748 self._validate_glob(value)
3745 3749 self._branch_pattern = value or '*'
3746 3750
3747 3751 @hybrid_property
3748 3752 def file_pattern(self):
3749 3753 return self._file_pattern or '*'
3750 3754
3751 3755 @file_pattern.setter
3752 3756 def file_pattern(self, value):
3753 3757 self._validate_glob(value)
3754 3758 self._file_pattern = value or '*'
3755 3759
3756 3760 def matches(self, branch, files_changed):
3757 3761 """
3758 3762 Check if this review rule matches a branch/files in a pull request
3759 3763
3760 3764 :param branch: branch name for the commit
3761 3765 :param files_changed: list of file paths changed in the pull request
3762 3766 """
3763 3767
3764 3768 branch = branch or ''
3765 3769 files_changed = files_changed or []
3766 3770
3767 3771 branch_matches = True
3768 3772 if branch:
3769 3773 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3770 3774 branch_matches = bool(branch_regex.search(branch))
3771 3775
3772 3776 files_matches = True
3773 3777 if self.file_pattern != '*':
3774 3778 files_matches = False
3775 3779 file_regex = re.compile(glob2re(self.file_pattern))
3776 3780 for filename in files_changed:
3777 3781 if file_regex.search(filename):
3778 3782 files_matches = True
3779 3783 break
3780 3784
3781 3785 return branch_matches and files_matches
3782 3786
3783 3787 @property
3784 3788 def review_users(self):
3785 3789 """ Returns the users which this rule applies to """
3786 3790
3787 3791 users = set()
3788 3792 users |= set([
3789 3793 rule_user.user for rule_user in self.rule_users
3790 3794 if rule_user.user.active])
3791 3795 users |= set(
3792 3796 member.user
3793 3797 for rule_user_group in self.rule_user_groups
3794 3798 for member in rule_user_group.users_group.members
3795 3799 if member.user.active
3796 3800 )
3797 3801 return users
3798 3802
3799 3803 def __repr__(self):
3800 3804 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3801 3805 self.repo_review_rule_id, self.repo)
3802 3806
3803 3807
3804 3808 class DbMigrateVersion(Base, BaseModel):
3805 3809 __tablename__ = 'db_migrate_version'
3806 3810 __table_args__ = (
3807 3811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3808 3812 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3809 3813 )
3810 3814 repository_id = Column('repository_id', String(250), primary_key=True)
3811 3815 repository_path = Column('repository_path', Text)
3812 3816 version = Column('version', Integer)
3813 3817
3814 3818
3815 3819 class DbSession(Base, BaseModel):
3816 3820 __tablename__ = 'db_session'
3817 3821 __table_args__ = (
3818 3822 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3819 3823 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3820 3824 )
3821 3825
3822 3826 def __repr__(self):
3823 3827 return '<DB:DbSession({})>'.format(self.id)
3824 3828
3825 3829 id = Column('id', Integer())
3826 3830 namespace = Column('namespace', String(255), primary_key=True)
3827 3831 accessed = Column('accessed', DateTime, nullable=False)
3828 3832 created = Column('created', DateTime, nullable=False)
3829 3833 data = Column('data', PickleType, nullable=False)
@@ -1,70 +1,74 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import colander
24 24
25 25 from rhodecode.translation import _
26 26 from rhodecode.model.validation_schema import preparers
27 27 from rhodecode.model.validation_schema import types
28 28
29 29
30 30 @colander.deferred
31 31 def deferred_lifetime_validator(node, kw):
32 32 options = kw.get('lifetime_options', [])
33 33 return colander.All(
34 34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
35 35 colander.OneOf([x for x in options]))
36 36
37 37
38 38 def unique_gist_validator(node, value):
39 39 from rhodecode.model.db import Gist
40 40 existing = Gist.get_by_access_id(value)
41 41 if existing:
42 42 msg = _(u'Gist with name {} already exists').format(value)
43 43 raise colander.Invalid(node, msg)
44 44
45 45
46 46 def filename_validator(node, value):
47 47 if value != os.path.basename(value):
48 48 msg = _(u'Filename {} cannot be inside a directory').format(value)
49 49 raise colander.Invalid(node, msg)
50 50
51 51
52 52 comment_types = ['note', 'todo']
53 53
54 54
55 55 class CommentSchema(colander.MappingSchema):
56 from rhodecode.model.db import ChangesetComment
56 from rhodecode.model.db import ChangesetComment, ChangesetStatus
57 57
58 58 comment_body = colander.SchemaNode(colander.String())
59 59 comment_type = colander.SchemaNode(
60 60 colander.String(),
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES))
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
62 missing=ChangesetComment.COMMENT_TYPE_NOTE)
62 63
63 64 comment_file = colander.SchemaNode(colander.String(), missing=None)
64 65 comment_line = colander.SchemaNode(colander.String(), missing=None)
65 status_change = colander.SchemaNode(colander.String(), missing=None)
66 status_change = colander.SchemaNode(
67 colander.String(), missing=None,
68 validator=colander.OneOf([x[0] for x in ChangesetStatus.STATUSES]))
66 69 renderer_type = colander.SchemaNode(colander.String())
67 70
68 # do those ?
71 resolves_comment_id = colander.SchemaNode(colander.Integer(), missing=None)
72
69 73 user = colander.SchemaNode(types.StrOrIntType())
70 74 repo = colander.SchemaNode(types.StrOrIntType())
@@ -1,527 +1,528 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 .comments {
8 8 width: 100%;
9 9 }
10 10
11 11 tr.inline-comments div {
12 12 max-width: 100%;
13 13
14 14 p {
15 15 white-space: normal;
16 16 }
17 17
18 18 code, pre, .code, dd {
19 19 overflow-x: auto;
20 20 width: 1062px;
21 21 }
22 22
23 23 dd {
24 24 width: auto;
25 25 }
26 26 }
27 27
28 28 #injected_page_comments {
29 29 .comment-previous-link,
30 30 .comment-next-link,
31 31 .comment-links-divider {
32 32 display: none;
33 33 }
34 34 }
35 35
36 36 .add-comment {
37 37 margin-bottom: 10px;
38 38 }
39 39 .hide-comment-button .add-comment {
40 40 display: none;
41 41 }
42 42
43 43 .comment-bubble {
44 44 color: @grey4;
45 45 margin-top: 4px;
46 46 margin-right: 30px;
47 47 visibility: hidden;
48 48 }
49 49
50 50 .comment-label {
51 51 float: left;
52 52
53 53 padding: 0.4em 0.4em;
54 54 margin: 2px 5px 0px -10px;
55 55 display: inline-block;
56 56 min-height: 0;
57 57
58 58 text-align: center;
59 59 font-size: 10px;
60 60 line-height: .8em;
61 61
62 62 font-family: @text-italic;
63 63 background: #fff none;
64 64 color: @grey4;
65 65 border: 1px solid @grey4;
66 66 white-space: nowrap;
67 67
68 68 text-transform: uppercase;
69 min-width: 40px;
69 70
70 71 &.todo {
71 72 color: @color5;
72 73 font-family: @text-bold-italic;
73 74 }
74 75 }
75 76
76 77
77 78 .comment {
78 79
79 80 &.comment-general {
80 81 border: 1px solid @grey5;
81 82 padding: 5px 5px 5px 5px;
82 83 }
83 84
84 85 margin: @padding 0;
85 86 padding: 4px 0 0 0;
86 87 line-height: 1em;
87 88
88 89 .rc-user {
89 90 min-width: 0;
90 91 margin: 0px .5em 0 0;
91 92
92 93 .user {
93 94 display: inline;
94 95 }
95 96 }
96 97
97 98 .meta {
98 99 position: relative;
99 100 width: 100%;
100 101 border-bottom: 1px solid @grey5;
101 102 margin: -5px 0px;
102 103 line-height: 24px;
103 104
104 105 &:hover .permalink {
105 106 visibility: visible;
106 107 color: @rcblue;
107 108 }
108 109 }
109 110
110 111 .author,
111 112 .date {
112 113 display: inline;
113 114
114 115 &:after {
115 116 content: ' | ';
116 117 color: @grey5;
117 118 }
118 119 }
119 120
120 121 .author-general img {
121 122 top: 3px;
122 123 }
123 124 .author-inline img {
124 125 top: 3px;
125 126 }
126 127
127 128 .status-change,
128 129 .permalink,
129 130 .changeset-status-lbl {
130 131 display: inline;
131 132 }
132 133
133 134 .permalink {
134 135 visibility: hidden;
135 136 }
136 137
137 138 .comment-links-divider {
138 139 display: inline;
139 140 }
140 141
141 142 .comment-links-block {
142 143 float:right;
143 144 text-align: right;
144 145 min-width: 85px;
145 146
146 147 [class^="icon-"]:before,
147 148 [class*=" icon-"]:before {
148 149 margin-left: 0;
149 150 margin-right: 0;
150 151 }
151 152 }
152 153
153 154 .comment-previous-link {
154 155 display: inline-block;
155 156
156 157 .arrow_comment_link{
157 158 cursor: pointer;
158 159 i {
159 160 font-size:10px;
160 161 }
161 162 }
162 163 .arrow_comment_link.disabled {
163 164 cursor: default;
164 165 color: @grey5;
165 166 }
166 167 }
167 168
168 169 .comment-next-link {
169 170 display: inline-block;
170 171
171 172 .arrow_comment_link{
172 173 cursor: pointer;
173 174 i {
174 175 font-size:10px;
175 176 }
176 177 }
177 178 .arrow_comment_link.disabled {
178 179 cursor: default;
179 180 color: @grey5;
180 181 }
181 182 }
182 183
183 184 .flag_status {
184 185 display: inline-block;
185 186 margin: -2px .5em 0 .25em
186 187 }
187 188
188 189 .delete-comment {
189 190 display: inline-block;
190 191 color: @rcblue;
191 192
192 193 &:hover {
193 194 cursor: pointer;
194 195 }
195 196 }
196 197
197 198
198 199 .text {
199 200 clear: both;
200 201 .border-radius(@border-radius);
201 202 .box-sizing(border-box);
202 203
203 204 .markdown-block p,
204 205 .rst-block p {
205 206 margin: .5em 0 !important;
206 207 // TODO: lisa: This is needed because of other rst !important rules :[
207 208 }
208 209 }
209 210
210 211 .pr-version {
211 212 float: left;
212 213 margin: 0px 4px;
213 214 }
214 215 .pr-version-inline {
215 216 float: left;
216 217 margin: 0px 4px;
217 218 }
218 219 .pr-version-num {
219 220 font-size: 10px;
220 221 }
221 222
222 223 }
223 224
224 225 @comment-padding: 5px;
225 226
226 227 .inline-comments {
227 228 border-radius: @border-radius;
228 229 .comment {
229 230 margin: 0;
230 231 border-radius: @border-radius;
231 232 }
232 233 .comment-outdated {
233 234 opacity: 0.5;
234 235 }
235 236
236 237 .comment-inline {
237 238 background: white;
238 239 padding: @comment-padding @comment-padding;
239 240 border: @comment-padding solid @grey6;
240 241
241 242 .text {
242 243 border: none;
243 244 }
244 245 .meta {
245 246 border-bottom: 1px solid @grey6;
246 247 margin: -5px 0px;
247 248 line-height: 24px;
248 249 }
249 250 }
250 251 .comment-selected {
251 252 border-left: 6px solid @comment-highlight-color;
252 253 }
253 254 .comment-inline-form {
254 255 padding: @comment-padding;
255 256 display: none;
256 257 }
257 258 .cb-comment-add-button {
258 259 margin: @comment-padding;
259 260 }
260 261 /* hide add comment button when form is open */
261 262 .comment-inline-form-open ~ .cb-comment-add-button {
262 263 display: none;
263 264 }
264 265 .comment-inline-form-open {
265 266 display: block;
266 267 }
267 268 /* hide add comment button when form but no comments */
268 269 .comment-inline-form:first-child + .cb-comment-add-button {
269 270 display: none;
270 271 }
271 272 /* hide add comment button when no comments or form */
272 273 .cb-comment-add-button:first-child {
273 274 display: none;
274 275 }
275 276 /* hide add comment button when only comment is being deleted */
276 277 .comment-deleting:first-child + .cb-comment-add-button {
277 278 display: none;
278 279 }
279 280 }
280 281
281 282
282 283 .show-outdated-comments {
283 284 display: inline;
284 285 color: @rcblue;
285 286 }
286 287
287 288 // Comment Form
288 289 div.comment-form {
289 290 margin-top: 20px;
290 291 }
291 292
292 293 .comment-form strong {
293 294 display: block;
294 295 margin-bottom: 15px;
295 296 }
296 297
297 298 .comment-form textarea {
298 299 width: 100%;
299 300 height: 100px;
300 301 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
301 302 }
302 303
303 304 form.comment-form {
304 305 margin-top: 10px;
305 306 margin-left: 10px;
306 307 }
307 308
308 309 .comment-inline-form .comment-block-ta,
309 310 .comment-form .comment-block-ta,
310 311 .comment-form .preview-box {
311 312 .border-radius(@border-radius);
312 313 .box-sizing(border-box);
313 314 background-color: white;
314 315 }
315 316
316 317 .comment-form-submit {
317 318 margin-top: 5px;
318 319 margin-left: 525px;
319 320 }
320 321
321 322 .file-comments {
322 323 display: none;
323 324 }
324 325
325 326 .comment-form .preview-box.unloaded,
326 327 .comment-inline-form .preview-box.unloaded {
327 328 height: 50px;
328 329 text-align: center;
329 330 padding: 20px;
330 331 background-color: white;
331 332 }
332 333
333 334 .comment-footer {
334 335 position: relative;
335 336 width: 100%;
336 337 min-height: 42px;
337 338
338 339 .status_box,
339 340 .cancel-button {
340 341 float: left;
341 342 display: inline-block;
342 343 }
343 344
344 345 .action-buttons {
345 346 float: right;
346 347 display: inline-block;
347 348 }
348 349 }
349 350
350 351 .comment-form {
351 352
352 353 .comment {
353 354 margin-left: 10px;
354 355 }
355 356
356 357 .comment-help {
357 358 color: @grey4;
358 359 padding: 5px 0 5px 0;
359 360 }
360 361
361 362 .comment-title {
362 363 padding: 5px 0 5px 0;
363 364 }
364 365
365 366 .comment-button {
366 367 display: inline-block;
367 368 }
368 369
369 .comment-button .comment-button-input {
370 .comment-button-input {
370 371 margin-right: 0;
371 372 }
372 373
373 374 .comment-footer {
374 375 margin-bottom: 110px;
375 376 margin-top: 10px;
376 377 }
377 378 }
378 379
379 380
380 381 .comment-form-login {
381 382 .comment-help {
382 383 padding: 0.9em; //same as the button
383 384 }
384 385
385 386 div.clearfix {
386 387 clear: both;
387 388 width: 100%;
388 389 display: block;
389 390 }
390 391 }
391 392
392 393 .comment-type {
393 394 margin: 0px;
394 395 border-radius: inherit;
395 396 border-color: @grey6;
396 397 }
397 398
398 399 .preview-box {
399 400 min-height: 105px;
400 401 margin-bottom: 15px;
401 402 background-color: white;
402 403 .border-radius(@border-radius);
403 404 .box-sizing(border-box);
404 405 }
405 406
406 407 .add-another-button {
407 408 margin-left: 10px;
408 409 margin-top: 10px;
409 410 margin-bottom: 10px;
410 411 }
411 412
412 413 .comment .buttons {
413 414 float: right;
414 415 margin: -1px 0px 0px 0px;
415 416 }
416 417
417 418 // Inline Comment Form
418 419 .injected_diff .comment-inline-form,
419 420 .comment-inline-form {
420 421 background-color: white;
421 422 margin-top: 10px;
422 423 margin-bottom: 20px;
423 424 }
424 425
425 426 .inline-form {
426 427 padding: 10px 7px;
427 428 }
428 429
429 430 .inline-form div {
430 431 max-width: 100%;
431 432 }
432 433
433 434 .overlay {
434 435 display: none;
435 436 position: absolute;
436 437 width: 100%;
437 438 text-align: center;
438 439 vertical-align: middle;
439 440 font-size: 16px;
440 441 background: none repeat scroll 0 0 white;
441 442
442 443 &.submitting {
443 444 display: block;
444 445 opacity: 0.5;
445 446 z-index: 100;
446 447 }
447 448 }
448 449 .comment-inline-form .overlay.submitting .overlay-text {
449 450 margin-top: 5%;
450 451 }
451 452
452 453 .comment-inline-form .clearfix,
453 454 .comment-form .clearfix {
454 455 .border-radius(@border-radius);
455 456 margin: 0px;
456 457 }
457 458
458 459 .comment-inline-form .comment-footer {
459 460 margin: 10px 0px 0px 0px;
460 461 }
461 462
462 463 .hide-inline-form-button {
463 464 margin-left: 5px;
464 465 }
465 466 .comment-button .hide-inline-form {
466 467 background: white;
467 468 }
468 469
469 470 .comment-area {
470 471 padding: 8px 12px;
471 472 border: 1px solid @grey5;
472 473 .border-radius(@border-radius);
473 474 }
474 475
475 476 .comment-area-header .nav-links {
476 477 display: flex;
477 478 flex-flow: row wrap;
478 479 -webkit-flex-flow: row wrap;
479 480 width: 100%;
480 481 }
481 482
482 483 .comment-area-footer {
483 484 display: flex;
484 485 }
485 486
486 487 .comment-footer .toolbar {
487 488
488 489 }
489 490
490 491 .nav-links {
491 492 padding: 0;
492 493 margin: 0;
493 494 list-style: none;
494 495 height: auto;
495 496 border-bottom: 1px solid @grey5;
496 497 }
497 498 .nav-links li {
498 499 display: inline-block;
499 500 }
500 501 .nav-links li:before {
501 502 content: "";
502 503 }
503 504 .nav-links li a.disabled {
504 505 cursor: not-allowed;
505 506 }
506 507
507 508 .nav-links li.active a {
508 509 border-bottom: 2px solid @rcblue;
509 510 color: #000;
510 511 font-weight: 600;
511 512 }
512 513 .nav-links li a {
513 514 display: inline-block;
514 515 padding: 0px 10px 5px 10px;
515 516 margin-bottom: -1px;
516 517 font-size: 14px;
517 518 line-height: 28px;
518 519 color: #8f8f8f;
519 520 border-bottom: 2px solid transparent;
520 521 }
521 522
522 523 .toolbar-text {
523 524 float: left;
524 525 margin: -5px 0px 0px 0px;
525 526 font-size: 12px;
526 527 }
527 528
@@ -1,55 +1,56 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
18 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
19 19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
20 20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
21 21 pyroutes.register('gists', '/_admin/gists', []);
22 22 pyroutes.register('new_gist', '/_admin/gists/new', []);
23 23 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
24 24 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
25 25 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
26 26 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
27 27 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
28 28 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
29 29 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
30 30 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
31 31 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
32 32 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
33 33 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
34 34 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
35 35 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
36 36 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
37 37 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
38 38 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
39 39 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
40 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
40 41 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
41 42 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
42 43 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
43 44 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
44 45 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
45 46 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 47 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 48 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 49 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
49 50 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 51 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
51 52 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
52 53 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
53 54 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
54 55 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
55 56 }
@@ -1,666 +1,798 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 // returns a node from given html;
29 29 var fromHTML = function(html){
30 30 var _html = document.createElement('element');
31 31 _html.innerHTML = html;
32 32 return _html;
33 33 };
34 34
35 35 var tableTr = function(cls, body){
36 36 var _el = document.createElement('div');
37 37 var _body = $(body).attr('id');
38 38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 39 var id = 'comment-tr-{0}'.format(comment_id);
40 40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 42 '<td></td>'+
43 43 '<td></td>'+
44 44 '<td></td>'+
45 45 '<td>{2}</td>'+
46 46 '</tr></tbody></table>').format(id, cls, body);
47 47 $(_el).html(_html);
48 48 return _el.children[0].children[0].children[0];
49 49 };
50 50
51 51 function bindDeleteCommentButtons() {
52 52 $('.delete-comment').one('click', function() {
53 53 var comment_id = $(this).data("comment-id");
54 54
55 55 if (comment_id){
56 56 deleteComment(comment_id);
57 57 }
58 58 });
59 59 }
60 60
61 61 var deleteComment = function(comment_id) {
62 62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 63 var postData = {
64 64 '_method': 'delete',
65 65 'csrf_token': CSRF_TOKEN
66 66 };
67 67
68 68 var success = function(o) {
69 69 window.location.reload();
70 70 };
71 71 ajaxPOST(url, postData, success);
72 72 };
73 73
74 74
75 75 var bindToggleButtons = function() {
76 76 $('.comment-toggle').on('click', function() {
77 77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 78 });
79 79 };
80 80
81 81 var linkifyComments = function(comments) {
82 /* TODO: dan: remove this - it should no longer needed */
82 /* TODO: marcink: remove this - it should no longer needed */
83 83 for (var i = 0; i < comments.length; i++) {
84 84 var comment_id = $(comments[i]).data('comment-id');
85 85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87 87
88 88 // place next/prev links
89 89 if (prev_comment_id) {
90 90 $('#prev_c_' + comment_id).show();
91 91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 93 }
94 94 if (next_comment_id) {
95 95 $('#next_c_' + comment_id).show();
96 96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 98 }
99 /* TODO(marcink): end removal here */
100
99 101 // place a first link to the total counter
100 102 if (i === 0) {
101 103 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 104 }
103 105 }
104 106
105 107 };
106 108
107 109
108 110 /* Comment form for main and inline comments */
109 var CommentForm = (function() {
111
112 (function(mod) {
113 if (typeof exports == "object" && typeof module == "object") // CommonJS
114 module.exports = mod();
115 else // Plain browser env
116 (this || window).CommentForm = mod();
117
118 })(function() {
110 119 "use strict";
111 120
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
121 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
122 if (!(this instanceof CommentForm)) {
123 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
124 }
125
126 // bind the element instance to our Form
127 $(formElement).get(0).CommentForm = this;
113 128
114 129 this.withLineNo = function(selector) {
115 130 var lineNo = this.lineNo;
116 131 if (lineNo === undefined) {
117 132 return selector
118 133 } else {
119 134 return selector + '_' + lineNo;
120 135 }
121 136 };
122 137
123 138 this.commitId = commitId;
124 139 this.pullRequestId = pullRequestId;
125 140 this.lineNo = lineNo;
126 141 this.initAutocompleteActions = initAutocompleteActions;
127 142
128 143 this.previewButton = this.withLineNo('#preview-btn');
129 144 this.previewContainer = this.withLineNo('#preview-container');
130 145
131 146 this.previewBoxSelector = this.withLineNo('#preview-box');
132 147
133 148 this.editButton = this.withLineNo('#edit-btn');
134 149 this.editContainer = this.withLineNo('#edit-container');
135 150 this.cancelButton = this.withLineNo('#cancel-btn');
136 151 this.commentType = this.withLineNo('#comment_type');
137 152
153 this.resolvesId = null;
154 this.resolvesActionId = null;
155
138 156 this.cmBox = this.withLineNo('#text');
139 157 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
140 158
141 159 this.statusChange = '#change_status';
142 160
143 161 this.submitForm = formElement;
144 162 this.submitButton = $(this.submitForm).find('input[type="submit"]');
145 163 this.submitButtonText = this.submitButton.val();
146 164
147 165 this.previewUrl = pyroutes.url('changeset_comment_preview',
148 166 {'repo_name': templateContext.repo_name});
149 167
150 // based on commitId, or pullReuqestId decide where do we submit
168 if (resolvesCommentId){
169 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
170 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
171 $(this.commentType).prop('disabled', true);
172 $(this.commentType).addClass('disabled');
173
174 var resolvedInfo = (
175 '<li class="">' +
176 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
177 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
178 '</li>'
179 ).format(resolvesCommentId, _gettext('resolve comment'));
180 $(resolvedInfo).insertAfter($(this.commentType).parent());
181 }
182
183 // based on commitId, or pullRequestId decide where do we submit
151 184 // out data
152 185 if (this.commitId){
153 186 this.submitUrl = pyroutes.url('changeset_comment',
154 187 {'repo_name': templateContext.repo_name,
155 188 'revision': this.commitId});
189 this.selfUrl = pyroutes.url('changeset_home',
190 {'repo_name': templateContext.repo_name,
191 'revision': this.commitId});
156 192
157 193 } else if (this.pullRequestId) {
158 194 this.submitUrl = pyroutes.url('pullrequest_comment',
159 195 {'repo_name': templateContext.repo_name,
160 196 'pull_request_id': this.pullRequestId});
197 this.selfUrl = pyroutes.url('pullrequest_show',
198 {'repo_name': templateContext.repo_name,
199 'pull_request_id': this.pullRequestId});
161 200
162 201 } else {
163 202 throw new Error(
164 203 'CommentForm requires pullRequestId, or commitId to be specified.')
165 204 }
166 205
167 206 this.getCmInstance = function(){
168 207 return this.cm
169 208 };
170 209
210 this.setPlaceholder = function(placeholder) {
211 var cm = this.getCmInstance();
212 if (cm){
213 cm.setOption('placeholder', placeholder);
214 }
215 };
216
171 217 var self = this;
172 218
173 219 this.getCommentStatus = function() {
174 220 return $(this.submitForm).find(this.statusChange).val();
175 221 };
176 222 this.getCommentType = function() {
177 223 return $(this.submitForm).find(this.commentType).val();
178 224 };
225
226 this.getResolvesId = function() {
227 return $(this.submitForm).find(this.resolvesId).val() || null;
228 };
229 this.markCommentResolved = function(resolvedCommentId){
230 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
232 };
233
179 234 this.isAllowedToSubmit = function() {
180 235 return !$(this.submitButton).prop('disabled');
181 236 };
182 237
183 238 this.initStatusChangeSelector = function(){
184 239 var formatChangeStatus = function(state, escapeMarkup) {
185 240 var originalOption = state.element;
186 241 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
187 242 '<span>' + escapeMarkup(state.text) + '</span>';
188 243 };
189 244 var formatResult = function(result, container, query, escapeMarkup) {
190 245 return formatChangeStatus(result, escapeMarkup);
191 246 };
192 247
193 248 var formatSelection = function(data, container, escapeMarkup) {
194 249 return formatChangeStatus(data, escapeMarkup);
195 250 };
196 251
197 252 $(this.submitForm).find(this.statusChange).select2({
198 253 placeholder: _gettext('Status Review'),
199 254 formatResult: formatResult,
200 255 formatSelection: formatSelection,
201 256 containerCssClass: "drop-menu status_box_menu",
202 257 dropdownCssClass: "drop-menu-dropdown",
203 258 dropdownAutoWidth: true,
204 259 minimumResultsForSearch: -1
205 260 });
206 261 $(this.submitForm).find(this.statusChange).on('change', function() {
207 262 var status = self.getCommentStatus();
208 if (status && !self.lineNo) {
263 if (status && self.lineNo == 'general') {
209 264 $(self.submitButton).prop('disabled', false);
210 265 }
211 //todo, fix this name
266
212 267 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
213 self.cm.setOption('placeholder', placeholderText);
268 self.setPlaceholder(placeholderText)
214 269 })
215 270 };
216 271
217 272 // reset the comment form into it's original state
218 273 this.resetCommentFormState = function(content) {
219 274 content = content || '';
220 275
221 276 $(this.editContainer).show();
222 277 $(this.editButton).parent().addClass('active');
223 278
224 279 $(this.previewContainer).hide();
225 280 $(this.previewButton).parent().removeClass('active');
226 281
227 282 this.setActionButtonsDisabled(true);
228 283 self.cm.setValue(content);
229 284 self.cm.setOption("readOnly", false);
230 285 };
231 286
232 287 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
233 288 failHandler = failHandler || function() {};
234 289 var postData = toQueryString(postData);
235 290 var request = $.ajax({
236 291 url: url,
237 292 type: 'POST',
238 293 data: postData,
239 294 headers: {'X-PARTIAL-XHR': true}
240 295 })
241 296 .done(function(data) {
242 297 successHandler(data);
243 298 })
244 299 .fail(function(data, textStatus, errorThrown){
245 300 alert(
246 301 "Error while submitting comment.\n" +
247 302 "Error code {0} ({1}).".format(data.status, data.statusText));
248 303 failHandler()
249 304 });
250 305 return request;
251 306 };
252 307
253 308 // overwrite a submitHandler, we need to do it for inline comments
254 309 this.setHandleFormSubmit = function(callback) {
255 310 this.handleFormSubmit = callback;
256 311 };
257 312
258 313 // default handler for for submit for main comments
259 314 this.handleFormSubmit = function() {
260 315 var text = self.cm.getValue();
261 316 var status = self.getCommentStatus();
262 317 var commentType = self.getCommentType();
318 var resolvesCommentId = self.getResolvesId();
263 319
264 320 if (text === "" && !status) {
265 321 return;
266 322 }
267 323
268 324 var excludeCancelBtn = false;
269 325 var submitEvent = true;
270 326 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
271 327 self.cm.setOption("readOnly", true);
272 328 var postData = {
273 329 'text': text,
274 330 'changeset_status': status,
275 331 'comment_type': commentType,
276 332 'csrf_token': CSRF_TOKEN
277 333 };
278
334 if (resolvesCommentId){
335 postData['resolves_comment_id'] = resolvesCommentId;
336 }
279 337 var submitSuccessCallback = function(o) {
280 338 if (status) {
281 339 location.reload(true);
282 340 } else {
283 341 $('#injected_page_comments').append(o.rendered_text);
284 342 self.resetCommentFormState();
285 343 bindDeleteCommentButtons();
286 344 timeagoActivate();
345
346 //mark visually which comment was resolved
347 if (resolvesCommentId) {
348 this.markCommentResolved(resolvesCommentId);
349 }
287 350 }
288 351 };
289 352 var submitFailCallback = function(){
290 self.resetCommentFormState(text)
353 self.resetCommentFormState(text);
291 354 };
292 355 self.submitAjaxPOST(
293 356 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
294 357 };
295 358
296 359 this.previewSuccessCallback = function(o) {
297 360 $(self.previewBoxSelector).html(o);
298 361 $(self.previewBoxSelector).removeClass('unloaded');
299 362
300 363 // swap buttons, making preview active
301 364 $(self.previewButton).parent().addClass('active');
302 365 $(self.editButton).parent().removeClass('active');
303 366
304 367 // unlock buttons
305 368 self.setActionButtonsDisabled(false);
306 369 };
307 370
308 371 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
309 372 excludeCancelBtn = excludeCancelBtn || false;
310 373 submitEvent = submitEvent || false;
311 374
312 375 $(this.editButton).prop('disabled', state);
313 376 $(this.previewButton).prop('disabled', state);
314 377
315 378 if (!excludeCancelBtn) {
316 379 $(this.cancelButton).prop('disabled', state);
317 380 }
318 381
319 382 var submitState = state;
320 383 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
321 384 // if the value of commit review status is set, we allow
322 385 // submit button, but only on Main form, lineNo means inline
323 386 submitState = false
324 387 }
325 388 $(this.submitButton).prop('disabled', submitState);
326 389 if (submitEvent) {
327 390 $(this.submitButton).val(_gettext('Submitting...'));
328 391 } else {
329 392 $(this.submitButton).val(this.submitButtonText);
330 393 }
331 394
332 395 };
333 396
334 397 // lock preview/edit/submit buttons on load, but exclude cancel button
335 398 var excludeCancelBtn = true;
336 399 this.setActionButtonsDisabled(true, excludeCancelBtn);
337 400
338 401 // anonymous users don't have access to initialized CM instance
339 402 if (this.cm !== undefined){
340 403 this.cm.on('change', function(cMirror) {
341 404 if (cMirror.getValue() === "") {
342 405 self.setActionButtonsDisabled(true, excludeCancelBtn)
343 406 } else {
344 407 self.setActionButtonsDisabled(false, excludeCancelBtn)
345 408 }
346 409 });
347 410 }
348 411
349 412 $(this.editButton).on('click', function(e) {
350 413 e.preventDefault();
351 414
352 415 $(self.previewButton).parent().removeClass('active');
353 416 $(self.previewContainer).hide();
354 417
355 418 $(self.editButton).parent().addClass('active');
356 419 $(self.editContainer).show();
357 420
358 421 });
359 422
360 423 $(this.previewButton).on('click', function(e) {
361 424 e.preventDefault();
362 425 var text = self.cm.getValue();
363 426
364 427 if (text === "") {
365 428 return;
366 429 }
367 430
368 431 var postData = {
369 432 'text': text,
370 'renderer': DEFAULT_RENDERER,
433 'renderer': templateContext.visual.default_renderer,
371 434 'csrf_token': CSRF_TOKEN
372 435 };
373 436
374 437 // lock ALL buttons on preview
375 438 self.setActionButtonsDisabled(true);
376 439
377 440 $(self.previewBoxSelector).addClass('unloaded');
378 441 $(self.previewBoxSelector).html(_gettext('Loading ...'));
379 442
380 443 $(self.editContainer).hide();
381 444 $(self.previewContainer).show();
382 445
383 446 // by default we reset state of comment preserving the text
384 447 var previewFailCallback = function(){
385 448 self.resetCommentFormState(text)
386 449 };
387 450 self.submitAjaxPOST(
388 451 self.previewUrl, postData, self.previewSuccessCallback,
389 452 previewFailCallback);
390 453
391 454 $(self.previewButton).parent().addClass('active');
392 455 $(self.editButton).parent().removeClass('active');
393 456 });
394 457
395 458 $(this.submitForm).submit(function(e) {
396 459 e.preventDefault();
397 460 var allowedToSubmit = self.isAllowedToSubmit();
398 461 if (!allowedToSubmit){
399 462 return false;
400 463 }
401 464 self.handleFormSubmit();
402 465 });
403 466
404 467 }
405 468
406 469 return CommentForm;
407 })();
470 });
408 471
409 var CommentsController = function() { /* comments controller */
472 /* comments controller */
473 var CommentsController = function() {
474 var mainComment = '#text';
410 475 var self = this;
411 476
412 477 this.cancelComment = function(node) {
413 478 var $node = $(node);
414 479 var $td = $node.closest('td');
415 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
480 $node.closest('.comment-inline-form').remove();
416 481 return false;
417 482 };
418 483
419 484 this.getLineNumber = function(node) {
420 485 var $node = $(node);
421 486 return $node.closest('td').attr('data-line-number');
422 487 };
423 488
424 489 this.scrollToComment = function(node, offset, outdated) {
425 490 var outdated = outdated || false;
426 491 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
427 492
428 493 if (!node) {
429 494 node = $('.comment-selected');
430 495 if (!node.length) {
431 496 node = $('comment-current')
432 497 }
433 498 }
434 499 $comment = $(node).closest(klass);
435 500 $comments = $(klass);
436 501
437 502 $('.comment-selected').removeClass('comment-selected');
438 503
439 504 var nextIdx = $(klass).index($comment) + offset;
440 505 if (nextIdx >= $comments.length) {
441 506 nextIdx = 0;
442 507 }
443 508 var $next = $(klass).eq(nextIdx);
444 509 var $cb = $next.closest('.cb');
445 510 $cb.removeClass('cb-collapsed');
446 511
447 512 var $filediffCollapseState = $cb.closest('.filediff').prev();
448 513 $filediffCollapseState.prop('checked', false);
449 514 $next.addClass('comment-selected');
450 515 scrollToElement($next);
451 516 return false;
452 517 };
453 518
454 519 this.nextComment = function(node) {
455 520 return self.scrollToComment(node, 1);
456 521 };
457 522
458 523 this.prevComment = function(node) {
459 524 return self.scrollToComment(node, -1);
460 525 };
461 526
462 527 this.nextOutdatedComment = function(node) {
463 528 return self.scrollToComment(node, 1, true);
464 529 };
465 530
466 531 this.prevOutdatedComment = function(node) {
467 532 return self.scrollToComment(node, -1, true);
468 533 };
469 534
470 535 this.deleteComment = function(node) {
471 536 if (!confirm(_gettext('Delete this comment?'))) {
472 537 return false;
473 538 }
474 539 var $node = $(node);
475 540 var $td = $node.closest('td');
476 541 var $comment = $node.closest('.comment');
477 542 var comment_id = $comment.attr('data-comment-id');
478 543 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
479 544 var postData = {
480 545 '_method': 'delete',
481 546 'csrf_token': CSRF_TOKEN
482 547 };
483 548
484 549 $comment.addClass('comment-deleting');
485 550 $comment.hide('fast');
486 551
487 552 var success = function(response) {
488 553 $comment.remove();
489 554 return false;
490 555 };
491 556 var failure = function(data, textStatus, xhr) {
492 557 alert("error processing request: " + textStatus);
493 558 $comment.show('fast');
494 559 $comment.removeClass('comment-deleting');
495 560 return false;
496 561 };
497 562 ajaxPOST(url, postData, success, failure);
498 563 };
499 564
500 565 this.toggleWideMode = function (node) {
501 566 if ($('#content').hasClass('wrapper')) {
502 567 $('#content').removeClass("wrapper");
503 568 $('#content').addClass("wide-mode-wrapper");
504 569 $(node).addClass('btn-success');
505 570 } else {
506 571 $('#content').removeClass("wide-mode-wrapper");
507 572 $('#content').addClass("wrapper");
508 573 $(node).removeClass('btn-success');
509 574 }
510 575 return false;
511 576 };
512 577
513 578 this.toggleComments = function(node, show) {
514 579 var $filediff = $(node).closest('.filediff');
515 580 if (show === true) {
516 581 $filediff.removeClass('hide-comments');
517 582 } else if (show === false) {
518 583 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
519 584 $filediff.addClass('hide-comments');
520 585 } else {
521 586 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
522 587 $filediff.toggleClass('hide-comments');
523 588 }
524 589 return false;
525 590 };
526 591
527 592 this.toggleLineComments = function(node) {
528 593 self.toggleComments(node, true);
529 594 var $node = $(node);
530 595 $node.closest('tr').toggleClass('hide-line-comments');
531 596 };
532 597
533 this.createComment = function(node) {
598 this.createComment = function(node, resolutionComment) {
599 var resolvesCommentId = resolutionComment || null;
534 600 var $node = $(node);
535 601 var $td = $node.closest('td');
536 602 var $form = $td.find('.comment-inline-form');
537 603
538 604 if (!$form.length) {
539 605 var tmpl = $('#cb-comment-inline-form-template').html();
540 606 var $filediff = $node.closest('.filediff');
541 607 $filediff.removeClass('hide-comments');
542 608 var f_path = $filediff.attr('data-f-path');
543 609 var lineno = self.getLineNumber(node);
544 610
545 611 tmpl = tmpl.format(f_path, lineno);
546 612 $form = $(tmpl);
547 613
548 614 var $comments = $td.find('.inline-comments');
549 615 if (!$comments.length) {
550 616 $comments = $(
551 617 $('#cb-comments-inline-container-template').html());
552 618 $td.append($comments);
553 619 }
554 620
555 621 $td.find('.cb-comment-add-button').before($form);
556 622
557 623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
558 624 var commitId = templateContext.commit_data.commit_id;
559 var _form = $form[0];
560 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
625 var _form = $($form[0]).find('form');
626
627 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false, resolvesCommentId);
561 628 var cm = commentForm.getCmInstance();
562 629
563 630 // set a CUSTOM submit handler for inline comments.
564 631 commentForm.setHandleFormSubmit(function(o) {
565 632 var text = commentForm.cm.getValue();
566 633 var commentType = commentForm.getCommentType();
634 var resolvesCommentId = commentForm.getResolvesId();
567 635
568 636 if (text === "") {
569 637 return;
570 638 }
571 639
572 640 if (lineno === undefined) {
573 641 alert('missing line !');
574 642 return;
575 643 }
576 644 if (f_path === undefined) {
577 645 alert('missing file path !');
578 646 return;
579 647 }
580 648
581 649 var excludeCancelBtn = false;
582 650 var submitEvent = true;
583 651 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
584 652 commentForm.cm.setOption("readOnly", true);
585 653 var postData = {
586 654 'text': text,
587 655 'f_path': f_path,
588 656 'line': lineno,
589 657 'comment_type': commentType,
590 658 'csrf_token': CSRF_TOKEN
591 659 };
660 if (resolvesCommentId){
661 postData['resolves_comment_id'] = resolvesCommentId;
662 }
663
592 664 var submitSuccessCallback = function(json_data) {
593 665 $form.remove();
594 666 try {
595 667 var html = json_data.rendered_text;
596 668 var lineno = json_data.line_no;
597 669 var target_id = json_data.target_id;
598 670
599 671 $comments.find('.cb-comment-add-button').before(html);
600 672
673 //mark visually which comment was resolved
674 if (resolvesCommentId) {
675 commentForm.markCommentResolved(resolvesCommentId);
676 }
677
601 678 } catch (e) {
602 679 console.error(e);
603 680 }
604 681
605 682 // re trigger the linkification of next/prev navigation
606 683 linkifyComments($('.inline-comment-injected'));
607 684 timeagoActivate();
608 685 bindDeleteCommentButtons();
609 686 commentForm.setActionButtonsDisabled(false);
610 687
611 688 };
612 689 var submitFailCallback = function(){
613 690 commentForm.resetCommentFormState(text)
614 691 };
615 692 commentForm.submitAjaxPOST(
616 693 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
617 694 });
618 695
696 if (resolvesCommentId){
697 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
698
699 } else {
700 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
701 }
702
619 703 setTimeout(function() {
620 704 // callbacks
621 705 if (cm !== undefined) {
622 cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno));
706 commentForm.setPlaceholder(placeholderText);
623 707 cm.focus();
624 708 cm.refresh();
625 709 }
626 710 }, 10);
627 711
628 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
629 form: _form,
630 parent: $td[0],
631 lineno: lineno,
632 f_path: f_path}
633 );
712 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
713 form: _form,
714 parent: $td[0],
715 lineno: lineno,
716 f_path: f_path}
717 );
718
719 // trigger hash
720 if (resolvesCommentId){
721 var resolveAction = $(commentForm.resolvesActionId);
722 setTimeout(function() {
723 $('body, html').animate({ scrollTop: resolveAction.offset().top }, 10);
724 }, 100);
725 }
634 726 }
635 727
636 728 $form.addClass('comment-inline-form-open');
637 729 };
638 730
731 this.createResolutionComment = function(commentId){
732 // hide the trigger text
733 $('#resolve-comment-{0}'.format(commentId)).hide();
734
735 var comment = $('#comment-'+commentId);
736 var commentData = comment.data();
737
738 if (commentData.commentInline) {
739 var resolutionComment = true;
740 this.createComment(comment, commentId)
741 } else {
742
743 this.createComment(comment, commentId)
744
745 console.log('TODO')
746 console.log(commentId)
747 }
748
749 return false;
750 };
751
752 this.submitResolution = function(commentId){
753 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
754 var commentForm = form.get(0).CommentForm;
755
756 var cm = commentForm.getCmInstance();
757 var renderer = templateContext.visual.default_renderer;
758 if (renderer == 'rst'){
759 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
760 } else if (renderer == 'markdown') {
761 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
762 } else {
763 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
764 }
765
766 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
767 form.submit();
768 return false;
769 };
770
639 771 this.renderInlineComments = function(file_comments) {
640 772 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
641 773
642 774 for (var i = 0; i < file_comments.length; i++) {
643 775 var box = file_comments[i];
644 776
645 777 var target_id = $(box).attr('target_id');
646 778
647 779 // actually comments with line numbers
648 780 var comments = box.children;
649 781
650 782 for (var j = 0; j < comments.length; j++) {
651 783 var data = {
652 784 'rendered_text': comments[j].outerHTML,
653 785 'line_no': $(comments[j]).attr('line'),
654 786 'target_id': target_id
655 787 };
656 788 }
657 789 }
658 790
659 791 // since order of injection is random, we're now re-iterating
660 792 // from correct order and filling in links
661 793 linkifyComments($('.inline-comment-injected'));
662 794 bindDeleteCommentButtons();
663 795 firefoxAnchorFix();
664 796 };
665 797
666 798 };
@@ -1,310 +1,353 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11 11
12 12 <div class="comment
13 13 ${'comment-inline' if inline else 'comment-general'}
14 14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 15 id="comment-${comment.comment_id}"
16 16 line="${comment.line_no}"
17 17 data-comment-id="${comment.comment_id}"
18 data-comment-type="${comment.comment_type}"
19 data-comment-inline=${h.json.dumps(inline)}
18 20 style="${'display: none;' if outdated_at_ver else ''}">
19 21
20 22 <div class="meta">
21 <div class="comment-type-label tooltip">
22 <div class="comment-label ${comment.comment_type or 'note'}">
23 ${comment.comment_type or 'note'}
23 <div class="comment-type-label">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 % if comment.comment_type == 'todo':
26 % if comment.resolved:
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 </div>
30 % else:
31 <div class="resolved tooltip" style="display: none">
32 <span>${comment.comment_type}</span>
33 </div>
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 ${comment.comment_type}
36 </div>
37 % endif
38 % else:
39 % if comment.resolved_comment:
40 fix
41 % else:
42 ${comment.comment_type or 'note'}
43 % endif
44 % endif
24 45 </div>
25 46 </div>
26 47
27 48 <div class="author ${'author-inline' if inline else 'author-general'}">
28 49 ${base.gravatar_with_user(comment.author.email, 16)}
29 50 </div>
30 51 <div class="date">
31 52 ${h.age_component(comment.modified_at, time_is_local=True)}
32 53 </div>
33 54 % if inline:
34 55 <span></span>
35 56 % else:
36 57 <div class="status-change">
37 58 % if comment.pull_request:
38 59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
39 60 % if comment.status_change:
40 61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
41 62 % else:
42 63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
43 64 % endif
44 65 </a>
45 66 % else:
46 67 % if comment.status_change:
47 68 ${_('Status change on commit')}:
48 69 % endif
49 70 % endif
50 71 </div>
51 72 % endif
52 73
53 74 % if comment.status_change:
54 75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
55 76 <div title="${_('Commit status')}" class="changeset-status-lbl">
56 77 ${comment.status_change[0].status_lbl}
57 78 </div>
58 79 % endif
59 80
60 81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
61 82
62 83 <div class="comment-links-block">
63 84
64 85 % if inline:
65 86 % if outdated_at_ver:
66 87 <div class="pr-version-inline">
67 88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
68 89 <code class="pr-version-num">
69 90 outdated ${'v{}'.format(pr_index_ver)}
70 91 </code>
71 92 </a>
72 93 </div>
73 94 |
74 95 % endif
75 96 % else:
76 97 % if comment.pull_request_version_id and pr_index_ver:
77 98 |
78 99 <div class="pr-version">
79 100 % if comment.outdated:
80 101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
81 102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
82 103 </a>
83 104 % else:
84 105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
85 106 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
86 107 <code class="pr-version-num">
87 108 ${'v{}'.format(pr_index_ver)}
88 109 </code>
89 110 </a>
90 111 </div>
91 112 % endif
92 113 </div>
93 114 % endif
94 115 % endif
95 116
96 117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
97 118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
98 119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
99 120 ## permissions to delete
100 121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
101 122 ## TODO: dan: add edit comment here
102 123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
103 124 %else:
104 125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
105 126 %endif
106 127 %else:
107 128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
108 129 %endif
109 130
110 131 %if not outdated_at_ver:
111 132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
112 133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
113 134 %endif
114 135
115 136 </div>
116 137 </div>
117 138 <div class="text">
118 139 ${comment.render(mentions=True)|n}
119 140 </div>
120 141
121 142 </div>
122 143 </%def>
144
123 145 ## generate main comments
124 146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
125 147 <div id="comments">
126 148 %for comment in c.comments:
127 149 <div id="comment-tr-${comment.comment_id}">
128 150 ## only render comments that are not from pull request, or from
129 151 ## pull request and a status change
130 152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
131 153 ${comment_block(comment)}
132 154 %endif
133 155 </div>
134 156 %endfor
135 157 ## to anchor ajax comments
136 158 <div id="injected_page_comments"></div>
137 159 </div>
138 160 </%def>
139 161
140 ## MAIN COMMENT FORM
162
141 163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
142 164
143 %if is_compare:
144 <% form_id = "comments_form_compare" %>
145 %else:
146 <% form_id = "comments_form" %>
147 %endif
148
149
165 ## merge status, and merge action
150 166 %if is_pull_request:
151 167 <div class="pull-request-merge">
152 168 %if c.allowed_to_merge:
153 169 <div class="pull-request-wrap">
154 170 <div class="pull-right">
155 171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
156 172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
157 173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
158 174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
159 175 ${h.end_form()}
160 176 </div>
161 177 </div>
162 178 %else:
163 179 <div class="pull-request-wrap">
164 180 <div class="pull-right">
165 181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
166 182 </div>
167 183 </div>
168 184 %endif
169 185 </div>
170 186 %endif
187
171 188 <div class="comments">
172 189 <%
173 190 if is_pull_request:
174 191 placeholder = _('Leave a comment on this Pull Request.')
175 192 elif is_compare:
176 193 placeholder = _('Leave a comment on all commits in this range.')
177 194 else:
178 195 placeholder = _('Leave a comment on this Commit.')
179 196 %>
197
180 198 % if c.rhodecode_user.username != h.DEFAULT_USER:
181 199 <div class="comment-form ac">
182 ${h.secure_form(post_url, id_=form_id)}
183 <div class="comment-area">
184 <div class="comment-area-header">
185 <ul class="nav-links clearfix">
186 <li class="active">
187 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
188 </li>
189 <li class="">
190 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
191 </li>
192 <li class="pull-right">
193 <select class="comment-type" id="comment_type" name="comment_type">
194 % for val in c.visual.comment_types:
195 <option value="${val}">${val.upper()}</option>
196 % endfor
197 </select>
198 </li>
199 </ul>
200 </div>
201
202 <div class="comment-area-write" style="display: block;">
203 <div id="edit-container">
204 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
205 </div>
206 <div id="preview-container" class="clearfix" style="display: none;">
207 <div id="preview-box" class="preview-box"></div>
208 </div>
209 </div>
200 ## inject form here
201 ${comment_form(form_type='general', form_id='general_comment', lineno_id='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 </div>
203 <script type="text/javascript">
204 // init active elements of commentForm
205 var commitId = templateContext.commit_data.commit_id;
206 var pullRequestId = templateContext.pull_request_data.pull_request_id;
207 var lineNo = 'general';
208 var resolvesCommitId = null;
210 209
211 <div class="comment-area-footer">
212 <div class="toolbar">
213 <div class="toolbar-text">
214 ${(_('Comments parsed using %s syntax with %s support.') % (
215 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
216 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
217 )
218 )|n}
219 </div>
220 </div>
221 </div>
222 </div>
210 var mainCommentForm = new CommentForm(
211 "#general_comment", commitId, pullRequestId, lineNo, true, resolvesCommitId);
212 mainCommentForm.setPlaceholder("${placeholder}");
213 mainCommentForm.initStatusChangeSelector();
214 </script>
223 215
224 <div id="comment_form_extras">
225 %if form_extras and isinstance(form_extras, (list, tuple)):
226 % for form_ex_el in form_extras:
227 ${form_ex_el|n}
228 % endfor
229 %endif
230 </div>
231 <div class="comment-footer">
232 %if change_status:
233 <div class="status_box">
234 <select id="change_status" name="changeset_status">
235 <option></option> # Placeholder
236 %for status,lbl in c.commit_statuses:
237 <option value="${status}" data-status="${status}">${lbl}</option>
238 %if is_pull_request and change_status and status in ('approved', 'rejected'):
239 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
240 %endif
241 %endfor
242 </select>
243 </div>
244 %endif
245 <div class="action-buttons">
246 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
247 </div>
248 </div>
249 ${h.end_form()}
250 </div>
216
251 217 % else:
218 ## form state when not logged in
252 219 <div class="comment-form ac">
253 220
254 221 <div class="comment-area">
255 222 <div class="comment-area-header">
256 223 <ul class="nav-links clearfix">
257 224 <li class="active">
258 225 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
259 226 </li>
260 227 <li class="">
261 228 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
262 229 </li>
263 230 </ul>
264 231 </div>
265 232
266 233 <div class="comment-area-write" style="display: block;">
267 234 <div id="edit-container">
268 235 <div style="padding: 40px 0">
269 236 ${_('You need to be logged in to leave comments.')}
270 237 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
271 238 </div>
272 239 </div>
273 240 <div id="preview-container" class="clearfix" style="display: none;">
274 241 <div id="preview-box" class="preview-box"></div>
275 242 </div>
276 243 </div>
277 244
278 245 <div class="comment-area-footer">
279 246 <div class="toolbar">
280 247 <div class="toolbar-text">
281 248 </div>
282 249 </div>
283 250 </div>
284 251 </div>
285 252
286 253 <div class="comment-footer">
287 254 </div>
288 255
289 256 </div>
290 257 % endif
291 258
259 <script type="text/javascript">
260 bindToggleButtons();
261 </script>
292 262 </div>
263 </%def>
264
265
266 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
267 ## comment injected based on assumption that user is logged in
268
269 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
293 270
294 <script>
295 // init active elements of commentForm
296 var commitId = templateContext.commit_data.commit_id;
297 var pullRequestId = templateContext.pull_request_data.pull_request_id;
298 var lineNo;
271 <div class="comment-area">
272 <div class="comment-area-header">
273 <ul class="nav-links clearfix">
274 <li class="active">
275 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
276 </li>
277 <li class="">
278 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
279 </li>
280 <li class="pull-right">
281 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
282 % for val in c.visual.comment_types:
283 <option value="${val}">${val.upper()}</option>
284 % endfor
285 </select>
286 </li>
287 </ul>
288 </div>
289
290 <div class="comment-area-write" style="display: block;">
291 <div id="edit-container_${lineno_id}">
292 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
293 </div>
294 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
295 <div id="preview-box_${lineno_id}" class="preview-box"></div>
296 </div>
297 </div>
299 298
300 var mainCommentForm = new CommentForm(
301 "#${form_id}", commitId, pullRequestId, lineNo, true);
299 <div class="comment-area-footer">
300 <div class="toolbar">
301 <div class="toolbar-text">
302 ${(_('Comments parsed using %s syntax with %s support.') % (
303 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
304 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
305 )
306 )|n}
307 </div>
308 </div>
309 </div>
310 </div>
311
312 <div class="comment-footer">
302 313
303 if (mainCommentForm.cm){
304 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
305 }
314 % if review_statuses:
315 <div class="status_box">
316 <select id="change_status" name="changeset_status">
317 <option></option> ## Placeholder
318 % for status, lbl in review_statuses:
319 <option value="${status}" data-status="${status}">${lbl}</option>
320 %if is_pull_request and change_status and status in ('approved', 'rejected'):
321 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
322 %endif
323 % endfor
324 </select>
325 </div>
326 % endif
306 327
307 mainCommentForm.initStatusChangeSelector();
308 bindToggleButtons();
309 </script>
310 </%def>
328 ## inject extra inputs into the form
329 % if form_extras and isinstance(form_extras, (list, tuple)):
330 <div id="comment_form_extras">
331 % for form_ex_el in form_extras:
332 ${form_ex_el|n}
333 % endfor
334 </div>
335 % endif
336
337 <div class="action-buttons">
338 ## inline for has a file, and line-number together with cancel hide button.
339 % if form_type == 'inline':
340 <input type="hidden" name="f_path" value="{0}">
341 <input type="hidden" name="line" value="${lineno_id}">
342 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
343 ${_('Cancel')}
344 </button>
345 % endif
346 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
347
348 </div>
349 </div>
350
351 </form>
352
353 </%def> No newline at end of file
@@ -1,718 +1,668 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
1 3 <%def name="diff_line_anchor(filename, line, type)"><%
2 4 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 5 %></%def>
4 6
5 7 <%def name="action_class(action)">
6 8 <%
7 9 return {
8 10 '-': 'cb-deletion',
9 11 '+': 'cb-addition',
10 12 ' ': 'cb-context',
11 13 }.get(action, 'cb-empty')
12 14 %>
13 15 </%def>
14 16
15 17 <%def name="op_class(op_id)">
16 18 <%
17 19 return {
18 20 DEL_FILENODE: 'deletion', # file deleted
19 21 BIN_FILENODE: 'warning' # binary diff hidden
20 22 }.get(op_id, 'addition')
21 23 %>
22 24 </%def>
23 25
24 26 <%def name="link_for(**kw)">
25 27 <%
26 28 new_args = request.GET.mixed()
27 29 new_args.update(kw)
28 30 return h.url('', **new_args)
29 31 %>
30 32 </%def>
31 33
32 34 <%def name="render_diffset(diffset, commit=None,
33 35
34 36 # collapse all file diff entries when there are more than this amount of files in the diff
35 37 collapse_when_files_over=20,
36 38
37 39 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 40 lines_changed_limit=500,
39 41
40 42 # add a ruler at to the output
41 43 ruler_at_chars=0,
42 44
43 45 # show inline comments
44 46 use_comments=False,
45 47
46 48 # disable new comments
47 49 disable_new_comments=False,
48 50
49 51 # special file-comments that were deleted in previous versions
50 52 # it's used for showing outdated comments for deleted files in a PR
51 53 deleted_files_comments=None
52 54
53 55 )">
54 56
55 57 %if use_comments:
56 58 <div id="cb-comments-inline-container-template" class="js-template">
57 59 ${inline_comments_container([])}
58 60 </div>
59 61 <div class="js-template" id="cb-comment-inline-form-template">
60 62 <div class="comment-inline-form ac">
61 63
62 64 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 ${h.form('#', method='get')}
64 <div class="comment-area">
65 <div class="comment-area-header">
66 <ul class="nav-links clearfix">
67 <li class="active">
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 </li>
70 <li class="">
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 </li>
73 <li class="pull-right">
74 <select class="comment-type" id="comment_type_{1}" name="comment_type">
75 % for val in c.visual.comment_types:
76 <option value="${val}">${val.upper()}</option>
77 % endfor
78 </select>
79 </li>
80 </ul>
81 </div>
82
83 <div class="comment-area-write" style="display: block;">
84 <div id="edit-container_{1}">
85 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
86 </div>
87 <div id="preview-container_{1}" class="clearfix" style="display: none;">
88 <div id="preview-box_{1}" class="preview-box"></div>
89 </div>
90 </div>
91
92 <div class="comment-area-footer">
93 <div class="toolbar">
94 <div class="toolbar-text">
95 ${(_('Comments parsed using %s syntax with %s support.') % (
96 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
97 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
98 )
99 )|n}
100 </div>
101 </div>
102 </div>
103 </div>
104
105 <div class="comment-footer">
106 <div class="action-buttons">
107 <input type="hidden" name="f_path" value="{0}">
108 <input type="hidden" name="line" value="{1}">
109 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
110 ${_('Cancel')}
111 </button>
112 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
113 </div>
114 ${h.end_form()}
115 </div>
65 ## render template for inline comments
66 ${commentblock.comment_form(form_type='inline')}
116 67 %else:
117 68 ${h.form('', class_='inline-form comment-form-login', method='get')}
118 69 <div class="pull-left">
119 70 <div class="comment-help pull-right">
120 71 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
121 72 </div>
122 73 </div>
123 74 <div class="comment-button pull-right">
124 75 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
125 76 ${_('Cancel')}
126 77 </button>
127 78 </div>
128 79 <div class="clearfix"></div>
129 80 ${h.end_form()}
130 81 %endif
131 82 </div>
132 83 </div>
133 84
134 85 %endif
135 86 <%
136 87 collapse_all = len(diffset.files) > collapse_when_files_over
137 88 %>
138 89
139 90 %if c.diffmode == 'sideside':
140 91 <style>
141 92 .wrapper {
142 93 max-width: 1600px !important;
143 94 }
144 95 </style>
145 96 %endif
146 97
147 98 %if ruler_at_chars:
148 99 <style>
149 100 .diff table.cb .cb-content:after {
150 101 content: "";
151 102 border-left: 1px solid blue;
152 103 position: absolute;
153 104 top: 0;
154 105 height: 18px;
155 106 opacity: .2;
156 107 z-index: 10;
157 108 //## +5 to account for diff action (+/-)
158 109 left: ${ruler_at_chars + 5}ch;
159 110 </style>
160 111 %endif
161 112
162 113 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
163 114 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
164 115 %if commit:
165 116 <div class="pull-right">
166 117 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
167 118 ${_('Browse Files')}
168 119 </a>
169 120 </div>
170 121 %endif
171 122 <h2 class="clearinner">
172 123 %if commit:
173 124 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
174 125 ${h.age_component(commit.date)} -
175 126 %endif
176 127 %if diffset.limited_diff:
177 128 ${_('The requested commit is too big and content was truncated.')}
178 129
179 130 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
180 131 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
181 132 %else:
182 133 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
183 134 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
184 135 %endif
185 136
186 137 <% at_ver = getattr(c, 'at_version_pos', None) %>
187 138 % if at_ver:
188 139 <div class="pull-right">
189 140 ${_('Showing changes at version %d') % at_ver}
190 141 </div>
191 142 % endif
192 143
193 144 </h2>
194 145 </div>
195 146
196 147 %if not diffset.files:
197 148 <p class="empty_data">${_('No files')}</p>
198 149 %endif
199 150
200 151 <div class="filediffs">
201 152 %for i, filediff in enumerate(diffset.files):
202 153
203 154 <%
204 155 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
205 156 over_lines_changed_limit = lines_changed > lines_changed_limit
206 157 %>
207 158 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
208 159 <div
209 160 class="filediff"
210 161 data-f-path="${filediff['patch']['filename']}"
211 162 id="a_${h.FID('', filediff['patch']['filename'])}">
212 163 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
213 164 <div class="filediff-collapse-indicator"></div>
214 165 ${diff_ops(filediff)}
215 166 </label>
216 167 ${diff_menu(filediff, use_comments=use_comments)}
217 168 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
218 169 %if not filediff.hunks:
219 170 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
220 171 <tr>
221 172 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
222 173 %if op_id == DEL_FILENODE:
223 174 ${_('File was deleted')}
224 175 %elif op_id == BIN_FILENODE:
225 176 ${_('Binary file hidden')}
226 177 %else:
227 178 ${op_text}
228 179 %endif
229 180 </td>
230 181 </tr>
231 182 %endfor
232 183 %endif
233 184 %if filediff.patch['is_limited_diff']:
234 185 <tr class="cb-warning cb-collapser">
235 186 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 187 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
237 188 </td>
238 189 </tr>
239 190 %else:
240 191 %if over_lines_changed_limit:
241 192 <tr class="cb-warning cb-collapser">
242 193 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
243 194 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
244 195 <a href="#" class="cb-expand"
245 196 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
246 197 </a>
247 198 <a href="#" class="cb-collapse"
248 199 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
249 200 </a>
250 201 </td>
251 202 </tr>
252 203 %endif
253 204 %endif
254 205
255 206 %for hunk in filediff.hunks:
256 207 <tr class="cb-hunk">
257 208 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
258 209 ## TODO: dan: add ajax loading of more context here
259 210 ## <a href="#">
260 211 <i class="icon-more"></i>
261 212 ## </a>
262 213 </td>
263 214 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
264 215 @@
265 216 -${hunk.source_start},${hunk.source_length}
266 217 +${hunk.target_start},${hunk.target_length}
267 218 ${hunk.section_header}
268 219 </td>
269 220 </tr>
270 221 %if c.diffmode == 'unified':
271 222 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
272 223 %elif c.diffmode == 'sideside':
273 224 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
274 225 %else:
275 226 <tr class="cb-line">
276 227 <td>unknown diff mode</td>
277 228 </tr>
278 229 %endif
279 230 %endfor
280 231
281 232 ## outdated comments that do not fit into currently displayed lines
282 233 % for lineno, comments in filediff.left_comments.items():
283 234
284 235 %if c.diffmode == 'unified':
285 236 <tr class="cb-line">
286 237 <td class="cb-data cb-context"></td>
287 238 <td class="cb-lineno cb-context"></td>
288 239 <td class="cb-lineno cb-context"></td>
289 240 <td class="cb-content cb-context">
290 241 ${inline_comments_container(comments)}
291 242 </td>
292 243 </tr>
293 244 %elif c.diffmode == 'sideside':
294 245 <tr class="cb-line">
295 246 <td class="cb-data cb-context"></td>
296 247 <td class="cb-lineno cb-context"></td>
297 248 <td class="cb-content cb-context"></td>
298 249
299 250 <td class="cb-data cb-context"></td>
300 251 <td class="cb-lineno cb-context"></td>
301 252 <td class="cb-content cb-context">
302 253 ${inline_comments_container(comments)}
303 254 </td>
304 255 </tr>
305 256 %endif
306 257
307 258 % endfor
308 259
309 260 </table>
310 261 </div>
311 262 %endfor
312 263
313 264 ## outdated comments that are made for a file that has been deleted
314 265 % for filename, comments_dict in (deleted_files_comments or {}).items():
315 266
316 267 <div class="filediffs filediff-outdated" style="display: none">
317 268 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
318 269 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
319 270 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
320 271 <div class="filediff-collapse-indicator"></div>
321 272 <span class="pill">
322 273 ## file was deleted
323 274 <strong>${filename}</strong>
324 275 </span>
325 276 <span class="pill-group" style="float: left">
326 277 ## file op, doesn't need translation
327 278 <span class="pill" op="removed">removed in this version</span>
328 279 </span>
329 280 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
330 281 <span class="pill-group" style="float: right">
331 282 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
332 283 </span>
333 284 </label>
334 285
335 286 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
336 287 <tr>
337 288 % if c.diffmode == 'unified':
338 289 <td></td>
339 290 %endif
340 291
341 292 <td></td>
342 293 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
343 294 ${_('File was deleted in this version, and outdated comments were made on it')}
344 295 </td>
345 296 </tr>
346 297 %if c.diffmode == 'unified':
347 298 <tr class="cb-line">
348 299 <td class="cb-data cb-context"></td>
349 300 <td class="cb-lineno cb-context"></td>
350 301 <td class="cb-lineno cb-context"></td>
351 302 <td class="cb-content cb-context">
352 303 ${inline_comments_container(comments_dict['comments'])}
353 304 </td>
354 305 </tr>
355 306 %elif c.diffmode == 'sideside':
356 307 <tr class="cb-line">
357 308 <td class="cb-data cb-context"></td>
358 309 <td class="cb-lineno cb-context"></td>
359 310 <td class="cb-content cb-context"></td>
360 311
361 312 <td class="cb-data cb-context"></td>
362 313 <td class="cb-lineno cb-context"></td>
363 314 <td class="cb-content cb-context">
364 315 ${inline_comments_container(comments_dict['comments'])}
365 316 </td>
366 317 </tr>
367 318 %endif
368 319 </table>
369 320 </div>
370 321 </div>
371 322 % endfor
372 323
373 324 </div>
374 325 </div>
375 326 </%def>
376 327
377 328 <%def name="diff_ops(filediff)">
378 329 <%
379 330 stats = filediff['patch']['stats']
380 331 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
381 332 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
382 333 %>
383 334 <span class="pill">
384 335 %if filediff.source_file_path and filediff.target_file_path:
385 336 %if filediff.source_file_path != filediff.target_file_path:
386 337 ## file was renamed
387 338 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
388 339 %else:
389 340 ## file was modified
390 341 <strong>${filediff.source_file_path}</strong>
391 342 %endif
392 343 %else:
393 344 %if filediff.source_file_path:
394 345 ## file was deleted
395 346 <strong>${filediff.source_file_path}</strong>
396 347 %else:
397 348 ## file was added
398 349 <strong>${filediff.target_file_path}</strong>
399 350 %endif
400 351 %endif
401 352 </span>
402 353 <span class="pill-group" style="float: left">
403 354 %if filediff.patch['is_limited_diff']:
404 355 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
405 356 %endif
406 357 %if RENAMED_FILENODE in stats['ops']:
407 358 <span class="pill" op="renamed">renamed</span>
408 359 %endif
409 360
410 361 %if NEW_FILENODE in stats['ops']:
411 362 <span class="pill" op="created">created</span>
412 363 %if filediff['target_mode'].startswith('120'):
413 364 <span class="pill" op="symlink">symlink</span>
414 365 %else:
415 366 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
416 367 %endif
417 368 %endif
418 369
419 370 %if DEL_FILENODE in stats['ops']:
420 371 <span class="pill" op="removed">removed</span>
421 372 %endif
422 373
423 374 %if CHMOD_FILENODE in stats['ops']:
424 375 <span class="pill" op="mode">
425 376 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
426 377 </span>
427 378 %endif
428 379 </span>
429 380
430 381 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
431 382
432 383 <span class="pill-group" style="float: right">
433 384 %if BIN_FILENODE in stats['ops']:
434 385 <span class="pill" op="binary">binary</span>
435 386 %if MOD_FILENODE in stats['ops']:
436 387 <span class="pill" op="modified">modified</span>
437 388 %endif
438 389 %endif
439 390 %if stats['added']:
440 391 <span class="pill" op="added">+${stats['added']}</span>
441 392 %endif
442 393 %if stats['deleted']:
443 394 <span class="pill" op="deleted">-${stats['deleted']}</span>
444 395 %endif
445 396 </span>
446 397
447 398 </%def>
448 399
449 400 <%def name="nice_mode(filemode)">
450 401 ${filemode.startswith('100') and filemode[3:] or filemode}
451 402 </%def>
452 403
453 404 <%def name="diff_menu(filediff, use_comments=False)">
454 405 <div class="filediff-menu">
455 406 %if filediff.diffset.source_ref:
456 407 %if filediff.patch['operation'] in ['D', 'M']:
457 408 <a
458 409 class="tooltip"
459 410 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
460 411 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 412 >
462 413 ${_('Show file before')}
463 414 </a> |
464 415 %else:
465 416 <span
466 417 class="tooltip"
467 418 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
468 419 >
469 420 ${_('Show file before')}
470 421 </span> |
471 422 %endif
472 423 %if filediff.patch['operation'] in ['A', 'M']:
473 424 <a
474 425 class="tooltip"
475 426 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
476 427 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 428 >
478 429 ${_('Show file after')}
479 430 </a> |
480 431 %else:
481 432 <span
482 433 class="tooltip"
483 434 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
484 435 >
485 436 ${_('Show file after')}
486 437 </span> |
487 438 %endif
488 439 <a
489 440 class="tooltip"
490 441 title="${h.tooltip(_('Raw diff'))}"
491 442 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
492 443 >
493 444 ${_('Raw diff')}
494 445 </a> |
495 446 <a
496 447 class="tooltip"
497 448 title="${h.tooltip(_('Download diff'))}"
498 449 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
499 450 >
500 451 ${_('Download diff')}
501 452 </a>
502 453 % if use_comments:
503 454 |
504 455 % endif
505 456
506 457 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
507 458 %if hasattr(c, 'ignorews_url'):
508 459 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
509 460 %endif
510 461 %if hasattr(c, 'context_url'):
511 462 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
512 463 %endif
513 464
514 465 %if use_comments:
515 466 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
516 467 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
517 468 </a>
518 469 %endif
519 470 %endif
520 471 </div>
521 472 </%def>
522 473
523 474
524 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
525 475 <%def name="inline_comments_container(comments)">
526 476 <div class="inline-comments">
527 477 %for comment in comments:
528 478 ${commentblock.comment_block(comment, inline=True)}
529 479 %endfor
530 480
531 481 % if comments and comments[-1].outdated:
532 482 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
533 483 style="display: none;}">
534 484 ${_('Add another comment')}
535 485 </span>
536 486 % else:
537 487 <span onclick="return Rhodecode.comments.createComment(this)"
538 488 class="btn btn-secondary cb-comment-add-button">
539 489 ${_('Add another comment')}
540 490 </span>
541 491 % endif
542 492
543 493 </div>
544 494 </%def>
545 495
546 496
547 497 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
548 498 %for i, line in enumerate(hunk.sideside):
549 499 <%
550 500 old_line_anchor, new_line_anchor = None, None
551 501 if line.original.lineno:
552 502 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
553 503 if line.modified.lineno:
554 504 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
555 505 %>
556 506
557 507 <tr class="cb-line">
558 508 <td class="cb-data ${action_class(line.original.action)}"
559 509 data-line-number="${line.original.lineno}"
560 510 >
561 511 <div>
562 512 %if line.original.comments:
563 513 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
564 514 %endif
565 515 </div>
566 516 </td>
567 517 <td class="cb-lineno ${action_class(line.original.action)}"
568 518 data-line-number="${line.original.lineno}"
569 519 %if old_line_anchor:
570 520 id="${old_line_anchor}"
571 521 %endif
572 522 >
573 523 %if line.original.lineno:
574 524 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
575 525 %endif
576 526 </td>
577 527 <td class="cb-content ${action_class(line.original.action)}"
578 528 data-line-number="o${line.original.lineno}"
579 529 >
580 530 %if use_comments and line.original.lineno:
581 531 ${render_add_comment_button()}
582 532 %endif
583 533 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
584 534 %if use_comments and line.original.lineno and line.original.comments:
585 535 ${inline_comments_container(line.original.comments)}
586 536 %endif
587 537 </td>
588 538 <td class="cb-data ${action_class(line.modified.action)}"
589 539 data-line-number="${line.modified.lineno}"
590 540 >
591 541 <div>
592 542 %if line.modified.comments:
593 543 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
594 544 %endif
595 545 </div>
596 546 </td>
597 547 <td class="cb-lineno ${action_class(line.modified.action)}"
598 548 data-line-number="${line.modified.lineno}"
599 549 %if new_line_anchor:
600 550 id="${new_line_anchor}"
601 551 %endif
602 552 >
603 553 %if line.modified.lineno:
604 554 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
605 555 %endif
606 556 </td>
607 557 <td class="cb-content ${action_class(line.modified.action)}"
608 558 data-line-number="n${line.modified.lineno}"
609 559 >
610 560 %if use_comments and line.modified.lineno:
611 561 ${render_add_comment_button()}
612 562 %endif
613 563 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
614 564 %if use_comments and line.modified.lineno and line.modified.comments:
615 565 ${inline_comments_container(line.modified.comments)}
616 566 %endif
617 567 </td>
618 568 </tr>
619 569 %endfor
620 570 </%def>
621 571
622 572
623 573 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
624 574 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
625 575 <%
626 576 old_line_anchor, new_line_anchor = None, None
627 577 if old_line_no:
628 578 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
629 579 if new_line_no:
630 580 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
631 581 %>
632 582 <tr class="cb-line">
633 583 <td class="cb-data ${action_class(action)}">
634 584 <div>
635 585 %if comments:
636 586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
637 587 %endif
638 588 </div>
639 589 </td>
640 590 <td class="cb-lineno ${action_class(action)}"
641 591 data-line-number="${old_line_no}"
642 592 %if old_line_anchor:
643 593 id="${old_line_anchor}"
644 594 %endif
645 595 >
646 596 %if old_line_anchor:
647 597 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
648 598 %endif
649 599 </td>
650 600 <td class="cb-lineno ${action_class(action)}"
651 601 data-line-number="${new_line_no}"
652 602 %if new_line_anchor:
653 603 id="${new_line_anchor}"
654 604 %endif
655 605 >
656 606 %if new_line_anchor:
657 607 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
658 608 %endif
659 609 </td>
660 610 <td class="cb-content ${action_class(action)}"
661 611 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
662 612 >
663 613 %if use_comments:
664 614 ${render_add_comment_button()}
665 615 %endif
666 616 <span class="cb-code">${action} ${content or '' | n}</span>
667 617 %if use_comments and comments:
668 618 ${inline_comments_container(comments)}
669 619 %endif
670 620 </td>
671 621 </tr>
672 622 %endfor
673 623 </%def>
674 624
675 625 <%def name="render_add_comment_button()">
676 626 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
677 627 <span><i class="icon-comment"></i></span>
678 628 </button>
679 629 </%def>
680 630
681 631 <%def name="render_diffset_menu()">
682 632
683 633 <div class="diffset-menu clearinner">
684 634 <div class="pull-right">
685 635 <div class="btn-group">
686 636
687 637 <a
688 638 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
689 639 title="${_('View side by side')}"
690 640 href="${h.url_replace(diffmode='sideside')}">
691 641 <span>${_('Side by Side')}</span>
692 642 </a>
693 643 <a
694 644 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
695 645 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
696 646 <span>${_('Unified')}</span>
697 647 </a>
698 648 </div>
699 649 </div>
700 650
701 651 <div class="pull-left">
702 652 <div class="btn-group">
703 653 <a
704 654 class="btn"
705 655 href="#"
706 656 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
707 657 <a
708 658 class="btn"
709 659 href="#"
710 660 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
711 661 <a
712 662 class="btn"
713 663 href="#"
714 664 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
715 665 </div>
716 666 </div>
717 667 </div>
718 668 </%def>
General Comments 0
You need to be logged in to leave comments. Login now