##// END OF EJS Templates
comments: allow submitting id of comment which submitted comment resolved....
marcink -
r1325:b4535cc7 default
parent child Browse files
Show More

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

@@ -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 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -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